双向链表的讲解
正文开始:
一、什么是双向链表?
双向链表的结构:
双向链表同样由一个个的节点组成,但是每一个节点又包括:节点的数据、指向上一个节点的指针、指向下一个节点的指针
我们所说的双向链表的全称其实是“带头双向循环链表”,而单链表的全称则是“不带头单向不循环链表”
- 带头:带头指的是当双向链表为空时,此时链表只剩一个头节点;而单链表为空时,就仅仅是一个空链表(在这所谓的头节点跟单链表的头结点不是一个含义,这里的头结点是叫作哨兵位,这个哨兵位没有过多的理解,只是用来充当一个头节点,它所存在的意义是遍历循环链表避免死循环)
- 双向:可以从两个方向开始遍历链表,也就是可以从左往右,也可以从右往左(节点包含前驱指针和后继指针)
- 循环:循环就是头尾节点相连,形成一个闭合的环,也就没有了尾节点;而不循环的尾节点的next指针则为NULL
定义双向链表的节点:
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next; //指针保存下⼀个节点的地址
struct ListNode* prev; //指针保存前⼀个节点的地址
LTDataType data;//节点的数据
}LTNode;
二、双向链表的初始化
双向链表的初始化可以直接将节点数据置为零、指针置为空吗?答案是不能。那为什么不能呢?答案很简单:刚才我们已经介绍了双向链表的全称为带头双向循环链表,那既然是循环链表,就必须形成一个环。当链表为空时,链表只包含一个哨兵位,这时候我们只需要将哨兵位的前驱指针和后继指针指向自己即可成环:
代码实现:
//申请节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;//哨兵位自己指向自己,不能为空指针
return node;
}
int main()
{
LTNode* plist=NULL;
LTInit(&plist);
return 0;
}
//初始化
void LTInit(LTNode** pphead)//没有给双向链表初始化(即创建哨兵位),插入方式将断言报错
{
//给双向链表创建一个哨兵位
*pphead = LTBuyNode(-1);//因为哨兵位没有实际意义,所以随便给它赋一个值
}
三、双向链表的插入
3.1 尾插
插入节点之前我们先要了解插入时影响到了哪两个节点,确定好之后才能进行新节点的插入:
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
//申请新节点newnode
LTNode* newnode = LTBuyNode(x);
LTNode* pcur = phead->prev;
newnode->prev = pcur;
newnode->next = phead;
pcur->next = newnode;//<---带箭头的两行代码不能交换位置
phead->prev = newnode;//<--
}
3.2 头插
我们这里所说的头插指的不是从哨兵位的头部插入,哨兵位没有实际意义,所以要在除哨兵位以外的第一个有效节点之前插入,才是双向链表的头插。
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
LTNode* pcur = phead->next;
newnode->next = pcur;
newnode->prev = phead;
phead->next = newnode;
pcur->prev = newnode;
}
3.3 在pos节点之后插入
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
/*LTNode* pcur = pos->next;*///定义pos下一个节点
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
LTInsert包含了节点的头插,但是头插不包含LTInsert。
四、双向链表的删除
4.1 尾删
删除节点之前我们同样需要知道要删除的节点影响到了哪两个节点,才能进行节点的删除操作:
代码实现:
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->prev;//定义尾节点
phead->prev = pcur->prev;
pcur->prev->next = phead;
free(pcur);
pcur = NULL;
}
4.2 头删
代码实现:
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;//定义头节点
phead->next = pcur->next;
pcur->next->prev = phead;
free(pcur);
pcur = NULL;
}
4.3 删除pos节点
代码实现:
//删除pos位置数据
void LTErase(LTNode* pos)
{
//pos理论上来说不能为phead,但是没有参数phead,无法增加校验
assert(pos);
//pos->prev pos pos->next
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
五、打印
打印链表轻松很多,和单链表的打印方式其实是差不多的,只是多了一个不用打印的哨兵位:
代码实现:
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
六、查找节点
遍历链表,将数据与链表的每个节点进行一一比对,找到了就返回该节点,没找到就返回NULL:
//查找节点
LTNode* LTFind(LTNode* phead, LTDataType x)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到
return NULL;
}
七、销毁链表
通过哨兵位一个一个向后遍历,将遇到的节点逐个释放掉即可:
//销毁链表
void LTDesTroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//此时pcur指向phead,而phead还没有被销毁
free(phead);
phead = NULL;
}
感谢阅读!