自学内容网 自学内容网

数据结构:双向带头链表

概念与结构

双向带头链表,分开来就是双向、带头、链表
之前了解了单链表,他只能由上一个节点找到下一个节点,不能由这个节点找到上一个,这个就是单向。
双向就是它的每一个节点都有两个指针,分别为前指针与后指针,分别指向上一个和下一个节点。头节点的前指针指向尾节点,尾节点的后指针指向头节点(哨兵位)。
带头就是它默认有一个头,也就是哨兵位,他是不存放有效数据的。
具体的结构如图:
在这里插入图片描述
head: 哨兵位
dn:有效节点
OK,话不多说,直接开始写代码。

链表实现

对小编个人来说,感觉比单链表还是要简单一些的。
因为一个一个详细介绍篇幅会太长,除了第一个函数以外,其他的就通过画图的方式来帮助理解代码,因为原理是相似的,所以就不再做过多文字介绍了。

源码链接:https://gitee.com/xiao-li-learning-c-language/c-language-exercisehomework/tree/master/10-11%E5%8F%8C%E5%90%91%E5%B8%A6%E5%A4%B4%E9%93%BE%E8%A1%A8/%E5%8F%8C%E5%90%91%E5%B8%A6%E5%A4%B4%E9%93%BE%E8%A1%A8

链表头文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType val;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
LTNode* LTNodeInit();//链表初始化
bool LTEmpty(LTNode* phead);//检查是否只剩哨兵位
void LTNodePushBack(LTNode* phead, LTDataType x);//尾插
void LTNodePrint(LTNode* phead);//打印
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTNodePopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);
//删除指定位置节点
void LTErase(LTNode* pos);
//销毁链表(包含哨兵位)
void LTDesTroy(LTNode** pphead);
void LTDesTroy2(LTNode* phead);//传一级,需要手动将plist置为NULL

链表初始化

初始化就是给我们的结构体赋一个初值,也就是让链表现有一个哨兵位。
这里我们要借助一个函数实现新链表的创建以及赋值
这里哨兵位的存放的是无效地址,我就用-1来代替这个无效数据。

LTNode* BuyNode(LTDataType x)//创建新节点
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
assert(newnode);
newnode->val = x;
newnode->next = newnode->prev = newnode;

return newnode;
}

LTNode* LTNodeInit()//为我们的链表创建哨兵位
{
LTNode* phead = BuyNode(-1);
return phead;
}

链表尾插

思路:
对链表尾插需要的数据有新节点、哨兵位的节点地址、原末尾节点的地址。
通过画图来遍历思考

在这里插入图片描述
如图,做标记的地方是我们代码要实现修改的地方,首先我们先修改新节点的两个指针,这样不会扰乱原链表。
将新节点的prev指向d3,将新节点的next指向head,形成环。
接下来将d3的next指向新节点,将head的prev指向新节点。
代码:

void LTNodePushBack(LTNode* phead,LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新节点
//新节点指针修改
newnode->next = phead;
newnode->prev = phead->prev;
//修改原末尾节点的next指向
phead->prev->next = newnode;
//修改哨兵位的prev指向
phead->prev = newnode;
}

链表头插

思路:
在这里插入图片描述

void LTPushFront(LTNode* phead, LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新链表
//修改新节点
newnode->next = phead->next;
newnode->prev = phead;
//原头节点修改
phead->next->prev = newnode;
//哨兵位修改
phead->next = newnode;
}

链表尾删

思路:
在这里插入图片描述
这里主要是修改哨兵位的前指针以及新尾节点的后指针,同时释放原尾节点。
注意:当链表只剩下哨兵位时,是不能再删除的。

代码:

bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next != phead;//判断下一个节点是不是自己,是自己就返回flase,不是就返回ture
}
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));//判断是否只剩下哨兵位
//暂存原末尾节点
LTNode* tmp = phead->prev;

phead->prev = phead->prev->prev;
phead->prev->next = phead;
free(tmp);
tmp = NULL;
}

链表头删

思路:
在这里插入图片描述

代码:

void LTNodePopFront(LTNode* phead)
{
assert(phead);
assert(LTEmpty(phead));//检查链表是否只剩下哨兵位

LTNode* tmp = phead->next;//暂存头节点地址
phead->next = phead->next->next;//让哨兵位指向第二个节点
phead->next->prev = phead;//让第二节点前指针指向哨兵位

free(tmp);//释放头节点
tmp = NULL;
}

寻找链表某个元素

思路:
从头节点也就是哨兵位的下一位开始循环,因为链表是循环的,当遍历到哨兵位时,就代表链表已经便利了一遍了。
代码:

LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);

LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->val == x)
{
return pcur;
}
pcur = pcur->next;
}

return NULL;
}

在某个位置之后插入节点

以在第二个节点之后插入为例
思路:
听过上面那个代码找到要插入数据的位置。
比如现在链表里面数据有1,2,3,4;
我要在2后面插入数据,就通过寻找元素2获取位置。
在这里插入图片描述
这样获取位置后就简单了,我们将该位置假设为哨兵位,是不是就跟头插的思路一样了。
代码:

void LTInsert(LTNode* pos, LTDataType x)
{
LTNode* newnode = BuyNode(x);//创建新节点
newnode->next = pos->next;//新节点指针修改
newnode->prev = pos;

pos->next->prev = newnode;//修改指定位置下一个位置的前指针,也就是图例d3
pos->next = newnode;//修改指定位置的后指针
}

删除指定位置节点

还是以删除d2为例
思路:在这里插入图片描述

这个就很简单了,我们通过寻找元素获取地址进行传参,因为这是双向链表,我们可以通过它找到他前后的节点进行修改,修改完后我们直接释放这个节点的空间就行。
代码:

void LTErase(LTNode* pos)
{
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}

销毁链表

从头节点遍历链表依次释放空间,然后释放哨兵位的空间,将指向哨兵位的指针置为NULL。

传参二级指针

void LTDesTroy(LTNode** pphead)
{
assert(pphead&&*pphead);
LTNode* pcur = (*pphead)->next;
while (pcur != *pphead)
{
LTNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
free(pcur);//这时pcur存放的就是哨兵位的地址
*pphead = NULL;//这里*pphead就是主程序里存放哨兵位地址的指针
pcur = NULL;
}

一级指针传参

因为一级指针传参只能通过这个指针找到所有的节点释放空间,但是无法对这个一级指针的实参进行修改,所以我们就需要在主程序里面对plist进行置空操作。

void LTDesTroy2(LTNode* phead)//传一级,需要手动将plist置为NULL
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* tmp = pcur;
pcur = pcur->next;
free(tmp);
}
free(pcur);
pcur = NULL;
}

打印链表有效数据

思路:
从头节点的位置进行遍历,依次打印有效数据,当指针回到哨兵位就结束循环。
代码:

void LTNodePrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
printf("plist->");
while (pcur!= phead)
{
printf("%d->", pcur->val);
pcur = pcur->next;
}
printf("plist->...\n");
}

代码测试

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include"list.h"

void test1()
{
LTNode* plist=LTNodeInit();
LTNodePrint(plist);
//LTNodePushBack(plist, 1);
LTPushFront(plist, 1);
LTNodePrint(plist);
//LTNodePushBack(plist, 2);
LTPushFront(plist, 2);
LTNodePrint(plist);
//LTNodePushBack(plist, 3);
LTPushFront(plist, 3);
LTNodePrint(plist);
//LTNodePushBack(plist, 4);
LTPushFront(plist, 4);
LTNodePrint(plist);

printf("\n");

LTNodePopFront(plist);
LTPopBack(plist);
LTNodePrint(plist);
LTNodePopFront(plist);
LTPopBack(plist);
LTNodePrint(plist);
LTPopBack(plist);
LTNodePopFront(plist);
LTNodePrint(plist);

LTNodePushBack(plist, 1);
LTNodePushBack(plist, 2);
LTNodePushBack(plist, 3);
LTNodePushBack(plist, 4);

LTNode* ret=LTFind(plist, 3);
if (ret)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
LTInsert(ret, 6);
LTNodePrint(plist);

LTDesTroy2(plist);
plist = NULL;

}
int main()
{
test1();
return 0;
}

在这里插入图片描述
结果如图,代码运行正常,plist是创建的哨兵位。


除了这些之外还有其他的一些可实现功能,这里就不再做过多介绍,基本原理都是差不多的。
谢谢各位观看,编写不易,赏赐一个三连吧。


原文地址:https://blog.csdn.net/qq_71391318/article/details/142865034

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!