1、双循环链表概念
双循环链表是具有前驱 和后继 指向,且头尾双向循环的链表
2、头结点
-
头结点概念
头结点是虚拟出来的节点,不保存数据,但作为整个链表的起始结点,头结点不是链表所必需的。
-
头结点作用
对链表进行插入、删除操作时,可以统一节点的操作,使得第一个真正数据结点的操作更加方便
3、双向循环链表的操作
- 数据结构定义
DCListNode
- 初始化双链表
InitDCList()
- 尾插法创建链表
createDCList()
- 打印双链表
printDCList()
- 查找结点
findDCList()
- 在 pos 结点后插入
insertDCListBack()
- 在 pos 结点前插入
insertDCListFront()
- 删除结点
deleteDCList()
- 反转链表
reverseDCList()
- 排序
sortDCList()
4、双向循环链表算法代码实现
1. 数据结构定义 DCListNode
C
struct DCListNode
{
int data; //数据域
DCListNode *prev; //前驱指针
DCListNode *next; //后继指针
};
功能:定义双向循环链表的节点结构。
文本图示:
text
DCListNode:
+------+------+------+
| prev | data | next |
+------+------+------+
| |
v v
指向前一节点 指向后一节点
说明:
prev
:指向前一个节点的指针data
:存储整型数据next
:指向后一个节点的指针
2. 初始化双链表 InitDCList()
C
DCListNode* InitDCList()
{
//申请头结点
DCListNode *s = (DCListNode *)malloc(sizeof(DCListNode));
s->prev = s;
s->next = s;
return s;
}
功能 :创建并返回一个空的双向循环链表的头结点。空链表只有一个头结点,其 prev
和 next
指针都指向自身。
- 过程:
- 使用
malloc
分配内存创建一个头结点 s。 - 将
s
的prev
指针指向s
自身。 - 将
s
的next
指针指向s
自身。 - 返回头结点指针
s
。
- 使用
文本图示:
说明 : 返回的
phead
是链表的哨兵节点(头结点),它不存储实际数据,但使得链表操作(特别是头尾插入删除)更加统一和方便。
3. 尾插法创建链表 createDCList()
- 功能: 从数组
a
中读取n
个元素,依次通过"尾插法"将它们插入到以phead
为头结点的双向循环链表中。这里的"尾插"实际上是在头结点phead
的 前面 插入,因为头结点的前驱是逻辑上的最后一个节点。 - 参数:
phead
: 链表的头结点指针。a[]
: 包含要插入数据的数组。n
: 要插入的元素个数。
- 过程 (对数组中每个元素
a[i]
):-
分配一个新节点
s
并存入数据a[i]
。 -
关键步骤 (修改 4 个指针将 s 插入到 phead->prev 和 phead 之间):
s->next = phead
; (新节点的next
指向头结点)s->prev = phead->prev
; (新节点的 prev 指向当前的最后一个节点)s->prev->next = s
; (当前的最后一个节点的next
指向新节点s
) - 这一步必须在s->prev
被赋值后执行s->next->prev = s
; (头结点的 prev 指向新节点 s) - 这一步必须在s->next
被赋值后执行
-
C
//尾插法
void createDCList(DCListNode *phead, int a[], int n)
{
for(int i = 0; i < n; i++)
{
DCListNode *s = (DCListNode *)malloc(sizeof(DCListNode)); //插入一个结点,需要修改四个指针
s->data = a[i];
//把s插入,先修改自身结点
s->next = phead;
s->prev = phead->prev;
s->prev->next = s;
s->next->prev = s;
}
}
图示(插入{7, 5, 3, 4, 9}为例):
插入前 (空链表):
插入过程 (创建 s, data=7):
步骤1 : s->next = phead;
- 设置节点
s
的next
指针指向phead
- 此时
s
知道它的下一个节点是phead
步骤2 : s->prev = phead->prev;
- 在这个特殊情况下,
phead->prev
是指向phead
自己的(因为只有一个节点) - 所以这步是设置
s
的prev
指针也指向phead
- 此时
s
的prev
和next
都指向了phead
步骤3 : s->prev->next = s;
- 我们知道
s->prev
是phead
- 所以这步是设置
phead
的next
指针指向s
- 现在
phead
的next
不再指向自己,而是指向了s
步骤4 : s->next->prev = s;
- 我们知道
s->next
是phead
- 所以这步是设置
phead
的prev
指针指向s
- 现在
phead
的prev
不再指向自己,而是指向了s
插入第二个结点 (创建 s
, data=5
):
插入第三个结点(创建
s
,data=3
):
以此类推......
• 说明 : 每次插入的新节点
s
都成为了新的最后一个节点,并且phead->prev
始终指向最后一个节点。
4. 打印双链表 printDCList()
- 功能: 从头结点的下一个节点开始,遍历并打印链表中所有实际数据节点的值。
- 过程:
- 初始化一个指针
p
指向头结点的下一个节点 (phead->next
),这是第一个数据节点。 - 当
p
不等于phead
时 (即还没循环回头部):- 打印
p->data
。 - 将
p
移动到下一个节点 (p = p->next
)。
- 打印
- 循环结束后打印 "Over"。
- 初始化一个指针
C
//打印双链表
void printDCList(DCListNode *phead)
{
DCListNode *p = phead->next;
while(p != phead)
{
printf("%d--> ", p->data);
p = p->next;
}
printf("Over\n");
}
图示 (假设链表为 phead <-> 10 <-> 20 <-> phead):
因为是循环的,所以最后一个结点20,下一个结点就是头结点
phead
p = phead->next
(指向 10)p != phead
? 是. 打印 10.p = p->next
(指向 20)p != phead
? 是. 打印 20.p = p->next
(指向phead
)p != phead
? 否. 循环结束. 打印 "Over".
5. 查找结点 findDCList()
- 功能: 在链表中查找第一个数据等于
key
的节点。 - 过程:
- 初始化指针
p
指向第一个数据节点 (phead->next
)。 - 当
p
不等于phead
并且p->data
不等于key
时,继续向后移动p
(p = p->next
)。 - 循环结束后:
- 如果
p
不等于phead
,说明循环是因为p->data == key
而停止的,找到了节点,返回p
。 - 如果
p
等于phead
,说明遍历完整个链表也没找到key
,返回NULL
。
- 如果
- 初始化指针
C
//查找结点
DCListNode* findDCList(DCListNode *phead, int key)
{
DCListNode *p = phead->next;
while(p != phead && p->data != key)
p = p->next;
if(p != phead)
return p; //找到了结点
return NULL; //没找到结点
}
图示 : 类似打印,指针 p
不断后移,直到找到 key
或者回到 phead
。
6. 在 pos 结点后插入 insertDCListBack()
- 功能: 在指定的
pos
节点 后面 插入一个数据为x
的新节点。 - 参数:
phead
: 头结点 。pos
: 要在其后插入新节点的节点指针。x
: 新节点的数据。
C
//封装函数(申请结点)
DCListNode* buyDCListNode(int x)
{
DCListNode *s = (DCListNode *)malloc(sizeof(DCListNode));
s->data = x;
return s;
}
//插入结点(在pos结点的后面插入一个x结点)
void insertDCListBack(DCListNode *phead, DCListNode *pos, int x)
{
//申请结点
DCListNode *s = buyDCListNode(x);
//在pos后面插入结点
s->next = pos->next;
s->prev = pos;
s->prev->next = s;
s->next->prev = s;
}
-
过程:
- 调用
buyDCListNode(x)
创建新节点s
。 - 关键步骤 (修改 4 个指针将
s
插入到pos
和pos->next
之间):s->next = pos->next
; (新节点的next
指向pos
原来的后继)s->prev = pos
; (新节点的prev
指向pos
)s->prev->next = s
; (pos
的next
指向新节点s
) - 即pos->next = s
s->next->prev = s
; (pos
原来的后继的prev
指向新节点s
) - 即(pos->next)->prev = s
- 调用
插入前需要先查找结点,查找在哪个结点后插入
C
DCListNode *p = findDCList(head, 10); //findDCList查找 5
insertDCListBack(head, p, 30); //在结点5后插入10,传入参数p使pos指着5
图示:
插入前:
- 调用
buyDCListNode(x)
创建新节点s
。
插入过程:
2. 关键步骤 (修改 4 个指针将
s
插入到 pos
和 pos->next
之间):
s->next = pos->next
; (新节点的next
指向pos
原来的后继)s->prev = pos
; (新节点的prev
指向pos
)s->prev->next = s
; (pos
的next
指向新节点s
) - 即pos->next = s
s->next->prev = s
; (pos
原来的后继的prev
指向新节点s
) - 即(pos->next)->prev = s
插入完成:
7. 在 pos 结点前插入 insertDCListFront()
-
功能: 在指定的
pos
节点 前面 插入一个数据为x
的新节点。 -
过程:
- 调用
buyDCListNode(x)
创建新节点s
。 - 关键步骤 (修改 4 个指针将 s 插入到
pos->prev
和pos
之间):s->next = pos
; (新节点的next
指向pos
)s->prev = pos->prev
; (新节点的prev
指向pos
原来的前驱)s->prev->next = s
; (pos
原来的前驱的next
指向新节点s
) - 即(pos->prev)->next = s
s->next->prev = s
; (pos
的prev
指向新节点s
) - 即pos->prev = s
- 调用
C
//封装函数(申请结点)
DCListNode* buyDCListNode(int x)
{
DCListNode *s = (DCListNode *)malloc(sizeof(DCListNode));
s->data = x;
return s;
}
//插入结点(在pos结点的前面插入一个x结点)
void insertDCListFront(DCListNode *phead, DCListNode *pos, int x)
{
//申请结点
DCListNode *s = buyDCListNode(x);
//在pos前面插入结点
s->next = pos;
s->prev = pos->prev;
s->prev->next = s;
s->next->prev = s;
}
图示: (与 insertDCListBack 类似,但插入位置不同)
插入前:
- 调用
buyDCListNode(x)
创建新节点s
。
插入过程:
-
关键步骤 (修改 4 个指针将 s 插入到
pos->prev
和pos
之间):s->next = pos
; (新节点的next
指向pos
)s->prev = pos->prev
; (新节点的prev
指向pos
原来的前驱)s->prev->next = s
; (pos
原来的前驱的next
指向新节点s
) - 即(pos->prev)->next = s
s->next->prev = s
; (pos
的prev
指向新节点s
) - 即pos->prev = s
插入完成:
8. 删除结点 deleteDCList()
- 功能: 查找数据为
key
的第一个节点并将其从链表中删除。 - 过程:
- 调用
findDCList(phead, key)
查找要删除的节点p
。 - 如果
p
为NULL
(没找到),直接返回。 - 关键步骤 (修改 2 个指针,将 p 从链表中绕过):
p->prev->next = p->next
; (p
的前驱节点的next
指向p
的后继节点)- p->next->prev = p->prev; (
p
的后继节点的prev
指向p
的前驱节点)
- 调用
free(p)
释放被删除节点的内存。
- 调用
C
//查找结点
DCListNode* findDCList(DCListNode *phead, int key)
{
DCListNode *p = phead->next;
while(p != phead && p->data != key)
p = p->next;
if(p != phead)
return p; //找到了结点
return NULL; //没找到结点
}
//删除结点
void deleteDCList(DCListNode *phead, int key)
{
//查找结点
DCListNode *p = findDCList(phead, key);
if(p == NULL)
return; //没找到结点,删除失败
//删除结点
p->prev->next = p->next;
p->next->prev = p->prev;
free(p);
}
图示:
删除前(删除结点30):
删除过程:
- p结点的前一个结点的next指针指向p结点的下一个结点
- p结点的下一个结点的prev指针指向p结点的前一个结点
删除完成:
9. 反转链表 reverseDCList()
-
功能: 将链表中数据节点的顺序反转 (头结点位置不变)。该实现采用了一种"头插法"的变种来构建反转后的链表。
-
过程:
- 处理空链表或单节点链表的情况 (代码中可能隐含处理了)。
- 取出第一个数据节点
p (phead->next)
和第二个数据节点q (p->next)
。 - 断开第一个节点: 将
p
的next
指向phead
,phead
的prev
指向p
。此时,phead
和p
形成了一个只包含一个数据节点的临时反转链表。q
指向剩余原始链表的头。 - 循环处理剩余节点: 当
q
不等于phead
时:- 保存当前要处理的节点
p = q
,并将q
后移 (q = q->next
)。 - 将节点
p
头插到反转链表中 (插入到phead
之后):p->next = phead->next
; (p
指向当前反转链表的第一个节点)p->prev = phead
; (p
的prev
指向头结点)p->prev->next = p
; (头结点的next
指向p
) - 即phead->next = p
p->next->prev = p
; (原反转链表的第一个节点的prev
指向p
)
- 保存当前要处理的节点
C
//反转链表
void reverseDCList(DCListNode *phead)
{
// 判断链表是否为空
if (phead == NULL || phead->next == phead)
return; // 空链表或只有头节点的情况
// 判断是否只有一个数据节点
if (phead->next->next == phead)
return; // 只有一个数据节点,无需反转
//断开链表
DCListNode *p = phead->next;
DCListNode *q = p->next;
p->next = phead;
phead->prev = p;
//将剩余链表的结点摘除头插
while(q!= phead)
{
p = q;
q = q->next;
p->next = phead->next;
p->prev = phead;
p->prev->next = p;
p->next->prev = p;
}
}
图示:
反转前:
反转过程:
第一次循环开始:
第一次循环完成:
第二次循环开始:
第二次循环结束:
第三次循环开始:
(q = phead)循环结束
反转完成:
• 说明: 这个方法每次从原链表剩余部分取出一个节点,然后将其插入到已反转部分的头部,最终完成整个链表的反转。
10. 排序 sortDCList()
- 功能: 对链表进行排序 (升序)。该实现采用插入排序的思想。
- 过程:
- 处理空或单节点链表。
- 取出第一个数据节点
p
,断开它,形成只包含一个节点的初始"已排序"链表:p->next = phead;``phead->prev = p;
。q
指向原始链表的第二个节点。 - 循环处理未排序节点: 当
q
不等于phead
时:- 保存当前要处理的节点
p = q
,并将q
后移 (q = q->next
)。p
现在是从原链表中取出的待插入节点。 - 在"已排序"链表中查找插入位置: 从已排序链表的头 (
phead->next
) 开始遍历,用cur
指针找到第一个cur->data >= p->data
的节点,或者遍历到phead
为止。 - 将节点
p
插入到cur
的前面: 使用与insertDCListFront
相同的逻辑,修改p
,cur
,cur->prev
之间的 4 个指针,将p
插入到cur
之前。
- 保存当前要处理的节点
C
void sortDCList(DCListNode *phead)
{
// 判断链表是否为空
if (phead == NULL || phead->next == phead)
return; // 空链表或只有头节点的情况
// 判断是否只有一个数据节点
if (phead->next->next == phead)
return; // 只有一个数据节点,无需排序
//冒泡排序
DCListNode *p = phead->next;
DCListNode *q = p->next;
p->next = phead;
phead->prev = p;
while(q!= phead)
{
p = q;
q = q->next;
//查找位置插入结点
DCListNode *cur = phead->next;
while(cur!= phead && cur->data < p->data)
cur = cur->next;
//在cur结点的前面插入
p->next = cur;
p->prev = cur->prev;
p->prev->next = p;
p->next->prev = p;
}
}
排序前:
排序过程:
第一次外圈循环:
cur→data > p→data
所以不执行cur = cur->next;
开始执行下面四条语句:
第一次外圈循环结束:
第二次外圈循环开始:
进入循环
- 刚刚
q
指向结点1,所以p
移到q
的位置 - 因为是双向循环链表所以最后一个结点的下一个结点是头结点,所以
q
移到phead
cur→data < p→data
(2 < 3),进入内圈循环,执行cur = cur->next
; 执行到cur→data= 3 > p→data = 1,停止循环:
执行下面四条语句:
第二次外圈循环结束:
第三次循环开始:
q = phead
; 不进入循环,至此排序结束
排序完成:
• 说明: 这个方法维护一个逐渐增长的有序子链表,每次从未排序部分取出一个节点,找到它在有序子链表中的正确位置并插入。