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 = ss->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 = ss->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 = ss->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 = ss->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 = pp->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; 不进入循环,至此排序结束
排序完成:
• 说明: 这个方法维护一个逐渐增长的有序子链表,每次从未排序部分取出一个节点,找到它在有序子链表中的正确位置并插入。