目录
1. 双向链表的结构
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
带头双向循环链表:
注意:这里的“带头”指的是“哨兵位”,哨兵位不存储任何的有效元素。
哨兵位的意义在于,遍历循环链表避免出现死循环。
当链表中只有哨兵位时,称该链表为空链表。
2. 双向链表的实现
根据上图,可以写出双向链表的结构:
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;//后继
struct ListNode* prev;//前驱
LTDataType data;
}LTNode;
初始化哨兵位:
老样子使用 malloc() 开辟空间,哨兵位的初始值可以是随便的数据。
双向链表在初始的时候只有哨兵位 phead 一个节点,哨兵位也有它的前驱指针和后继指针。因为双向链表是循环的,所以有:
使用一级指针,形参的改变不能影响实参。使用二级指针的目的就是对实参产生影响。
为了将哨兵位的初始值定义为-1,所以使用二级指针。
具体代码为:
//List.h
//定义双向链表中节点的结构
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
//注意,双向链表是带有哨兵位的,使用之前必须初始化一个哨兵位
void LTInit(LTNode** pphead);
//List.c
void LTInit(LTNode** pphead)
{
*pphead = (LTNode*)malloc(sizeof(LTNode));
if (*pphead == NULL)
{
perror("malloc fail");
exit(1);
}
(*pphead)->data = -1;//初始化一个随便的值
(*pphead)->next = (*pphead)->prev = *pphead;
}
测试初始化:
这是传参数的写法,如果不传参数呢?
环形链表的约瑟夫问题 中,通过返回值的形式将链表的头/尾返回。
同样的,在进行双向链表的初始化时,就可以不传参数,通过返回值的形式把创建好的哨兵位返回。
//List.h
LTNode* LTInit();
//List.c
//创建新节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL) {
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
//创建哨兵位
LTNode* LTInit()
{
LTNode* phead = LTBuyNode(-1);
return phead;
}
打印链表:
//List.c
void LTPrint(LTNode* phead)
{
assert(phead);
//遍历链表
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
注意:这里pcur 不指向哨兵位是为了避免死循环,所以选择指向哨兵位的下一位。
尾插:
首先要注意,不管插入还是删除都不能影响到哨兵位。
所以,这里将使用一级指针。
//List.h
//不需要改变哨兵位,则不需要传二级指针
//如果需要修改哨兵位的话,则传二级指针
void LTPushBack(LTNode* phead, LTDataType x);
尾插示意图:
很显然,尾插影响到的节点有:newnode、ptail、哨兵位。
从图中可知,双向链表的尾节点 ptail 就是 head->prev 。
所以,和单链表不同的是,双向链表找尾节点不需要遍历链表!
示例代码:
//List.c
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//顺序为 phead phead->prev(ptail) newnode
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
测试尾插:
头插:
注意:头插指的是在第一个有效节点之前插入新节点!
如果在哨兵位之前插入,就相当于尾插!
头插示意图:
很显然,头插影响到的节点有:哨兵位、newnode、head->next。
先将 newnode->next 指向 head->next,newnode->prev 指向 head;
然后再改变 head->next 指向 newnode,head->next->prev 指向 newnode。
让 newnode 成为新的 head->next。
//List.h
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//List.c
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//phead newnode phead->next
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
测试头插:
尾删:
尾删示意图:
很显然,尾删影响到的节点有:ptail(head->prev)、ptail->prev、哨兵位。
先将 head->prev->prev(图中就是d2)->next 指向 head;
然后将 head->prev 指向 head->prev->prev;
最后将原来的尾节点(d3)释放。
//List.h
//尾删
void LTPopBack(LTNode* phead);
//List.c
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//链表为空:只有一个哨兵位节点
assert(phead->next != phead);
LTNode* del = phead->prev;
LTNode* prev = del->prev;
prev->next = phead;
phead->prev = prev;
free(del);
del = NULL;
}
测试尾删:
头删:
一样的,头删删除的是第一个有效的节点!
头删示意图:
很显然,头删影响到的节点有:head->next->next、head->next、哨兵位。
先将 head->next->next(就是d2)->prev 指向 head;
然后将 head->next 指向 head->next->next;
最后将原来的第一个节点(d1)释放。
//List.h
//头删
void LTPopFront(LTNode* phead);
//List.c
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* del = phead->next;
LTNode* next = del->next;
next->prev = phead;
phead->next = next;
free(del);
del = NULL;
}
测试头删:
查找:
很简单,遍历链表,有则返回,无则NULL。
//List.h
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//List.c
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x) {
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
测试查找:
在指定位置之后插入数据:
插入示意图:
很显然,插入影响到的节点有:pos、pos->next。
但是要注意,如果先将 pos->next 指向 newnode,然后 pos->next->prev 指向 newnode,这个顺序是错误的,会导致找不到原来的 pos->next。
示例代码:
//List.h
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//List.c
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
测试插入:
删除目标位置的数据:
删除示意图:
很显然,删除影响到的节点有:pos、pos->next、pos->prev。
示例代码:
//List.h
//删除pos位置的数据
void LTErase(LTNode* pos);
//List.c
//删除pos位置的数据
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
测试删除:
销毁链表:
链表的销毁其实就是把链表里的数据一个个销毁。
多了要将哨兵位销毁的操作。
示例代码:
//List.h
//销毁
void LTDesTroy(LTNode* phead);
//List.c
void LTDesTroy(LTNode* phead)
{
//哨兵位不能为空
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//链表中只有一个哨兵位
free(phead);
phead = NULL;
}
自行调试。