我要成为数据结构与算法高手(三)之双向循环链表

1、双循环链表概念

双循环链表是具有前驱后继 指向,且头尾双向循环的链表

2、头结点

  • 头结点概念

头结点是虚拟出来的节点,不保存数据,但作为整个链表的起始结点,头结点不是链表所必需的

  • 头结点作用

对链表进行插入、删除操作时,可以统一节点的操作,使得第一个真正数据结点的操作更加方便

3、双向循环链表的操作

  1. 数据结构定义 DCListNode
  2. 初始化双链表 InitDCList()
  3. 尾插法创建链表 createDCList()
  4. 打印双链表 printDCList()
  5. 查找结点 findDCList()
  6. 在 pos 结点后插入 insertDCListBack()
  7. 在 pos 结点前插入 insertDCListFront()
  8. 删除结点 deleteDCList()
  9. 反转链表 reverseDCList()
  10. 排序 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;
}

功能 :创建并返回一个空的双向循环链表的头结点。空链表只有一个头结点,其 prevnext 指针都指向自身。

  • 过程:
    1. 使用 malloc 分配内存创建一个头结点 s。
    2. sprev 指针指向 s 自身。
    3. snext 指针指向 s 自身。
    4. 返回头结点指针 s

文本图示

说明 : 返回的 phead 是链表的哨兵节点(头结点),它不存储实际数据,但使得链表操作(特别是头尾插入删除)更加统一和方便。

3. 尾插法创建链表 createDCList()

  • 功能: 从数组 a 中读取 n 个元素,依次通过"尾插法"将它们插入到以 phead 为头结点的双向循环链表中。这里的"尾插"实际上是在头结点 phead前面 插入,因为头结点的前驱是逻辑上的最后一个节点。
  • 参数:
    • phead: 链表的头结点指针。
    • a[]: 包含要插入数据的数组。
    • n: 要插入的元素个数。
  • 过程 (对数组中每个元素 a[i]):
    1. 分配一个新节点 s 并存入数据 a[i]

    2. 关键步骤 (修改 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;

  • 设置节点snext指针指向phead
  • 此时s知道它的下一个节点是phead

步骤2 : s->prev = phead->prev;

  • 在这个特殊情况下,phead->prev 是指向phead自己的(因为只有一个节点
  • 所以这步是设置sprev指针也指向phead
  • 此时sprevnext都指向了phead

步骤3 : s->prev->next = s;

  • 我们知道s->prevphead
  • 所以这步是设置pheadnext指针指向s
  • 现在pheadnext不再指向自己,而是指向了s

步骤4 : s->next->prev = s;

  • 我们知道s->nextphead
  • 所以这步是设置pheadprev指针指向s
  • 现在pheadprev不再指向自己,而是指向了s

插入第二个结点 (创建 s, data=5):

插入第三个结点(创建sdata=3 ):

以此类推......

说明 : 每次插入的新节点 s 都成为了新的最后一个节点,并且 phead->prev 始终指向最后一个节点。

4. 打印双链表 printDCList()

  • 功能: 从头结点的下一个节点开始,遍历并打印链表中所有实际数据节点的值。
  • 过程:
    1. 初始化一个指针 p 指向头结点的下一个节点 (phead->next),这是第一个数据节点。
    2. p 不等于 phead 时 (即还没循环回头部):
      • 打印 p->data
      • p 移动到下一个节点 (p = p->next)。
    3. 循环结束后打印 "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

  1. p = phead->next (指向 10)
  2. p != phead? 是. 打印 10. p = p->next (指向 20)
  3. p != phead? 是. 打印 20. p = p->next (指向 phead)
  4. p != phead? 否. 循环结束. 打印 "Over".

5. 查找结点 findDCList()

  • 功能: 在链表中查找第一个数据等于 key 的节点。
  • 过程:
    1. 初始化指针 p 指向第一个数据节点 (phead->next)。
    2. p 不等于 phead 并且 p->data 不等于 key 时,继续向后移动 p (p = p->next)。
    3. 循环结束后:
      • 如果 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;
}
  • 过程:

    1. 调用 buyDCListNode(x) 创建新节点 s
    2. 关键步骤 (修改 4 个指针将 s 插入到 pospos->next 之间):
      • s->next = pos->next; (新节点的 next 指向 pos 原来的后继)
      • s->prev = pos; (新节点的 prev 指向 pos
      • s->prev->next = s; (posnext 指向新节点 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

图示:

插入前:

  1. 调用 buyDCListNode(x) 创建新节点 s

插入过程:

2. 关键步骤 (修改 4 个指针将 s 插入到 pospos->next 之间):

  • s->next = pos->next; (新节点的 next 指向 pos 原来的后继)
  • s->prev = pos; (新节点的 prev 指向 pos
  • s->prev->next = s; (posnext 指向新节点 s) - pos->next = s
  • s->next->prev = s; (pos 原来的后继的 prev 指向新节点 s) - (pos->next)->prev = s

插入完成:

7. 在 pos 结点前插入 insertDCListFront()

  • 功能: 在指定的 pos 节点 前面 插入一个数据为 x 的新节点。

  • 过程:

    1. 调用 buyDCListNode(x) 创建新节点 s
    2. 关键步骤 (修改 4 个指针将 s 插入到 pos->prevpos 之间):
      • 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; (posprev 指向新节点 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 类似,但插入位置不同)

插入前:

  1. 调用 buyDCListNode(x) 创建新节点 s

插入过程:

  1. 关键步骤 (修改 4 个指针将 s 插入到 pos->prevpos 之间):

    • 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; (posprev 指向新节点 s) - pos->prev = s

插入完成:

8. 删除结点 deleteDCList()

  • 功能: 查找数据为 key 的第一个节点并将其从链表中删除。
  • 过程:
    1. 调用 findDCList(phead, key) 查找要删除的节点 p
    2. 如果 pNULL (没找到),直接返回。
    3. 关键步骤 (修改 2 个指针,将 p 从链表中绕过):
      • p->prev->next = p->next; (p 的前驱节点的 next 指向 p 的后继节点)
      • p->next->prev = p->prev; (p 的后继节点的 prev 指向 p 的前驱节点)
    4. 调用 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):

删除过程:

  1. p结点的前一个结点的next指针指向p结点的下一个结点
  2. p结点的下一个结点的prev指针指向p结点的前一个结点

删除完成:

9. 反转链表 reverseDCList()

  • 功能: 将链表中数据节点的顺序反转 (头结点位置不变)。该实现采用了一种"头插法"的变种来构建反转后的链表。

  • 过程:

    1. 处理空链表或单节点链表的情况 (代码中可能隐含处理了)。
    2. 取出第一个数据节点 p (phead->next) 和第二个数据节点 q (p->next)
    3. 断开第一个节点:pnext 指向 pheadpheadprev 指向 p。此时,pheadp 形成了一个只包含一个数据节点的临时反转链表。q 指向剩余原始链表的头。
    4. 循环处理剩余节点:q 不等于 phead 时:
      • 保存当前要处理的节点 p = q,并将 q 后移 (q = q->next)。
      • 将节点 p 头插到反转链表中 (插入到 phead 之后):
        • p->next = phead->next; (p 指向当前反转链表的第一个节点)
        • p->prev = phead; (pprev 指向头结点)
        • 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()

  • 功能: 对链表进行排序 (升序)。该实现采用插入排序的思想。
  • 过程:
    1. 处理空或单节点链表。
    2. 取出第一个数据节点 p,断开它,形成只包含一个节点的初始"已排序"链表: p->next = phead;``phead->prev = p;q 指向原始链表的第二个节点。
    3. 循环处理未排序节点: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;

开始执行下面四条语句:

第一次外圈循环结束:

第二次外圈循环开始:

进入循环

  1. 刚刚q指向结点1,所以p移到q的位置
  2. 因为是双向循环链表所以最后一个结点的下一个结点是头结点,所以q移到phead

cur→data < p→data (2 < 3),进入内圈循环,执行cur = cur->next; 执行到cur→data= 3 > p→data = 1,停止循环:

执行下面四条语句:

第二次外圈循环结束:

第三次循环开始:

q = phead; 不进入循环,至此排序结束

排序完成:

• 说明: 这个方法维护一个逐渐增长的有序子链表,每次从未排序部分取出一个节点,找到它在有序子链表中的正确位置并插入。

相关推荐
柯ran32 分钟前
数据结构|排序算法(一)快速排序
数据结构·算法·排序算法
pipip.37 分钟前
搜索二维矩阵
数据结构·算法·矩阵
念_ovo2 小时前
【算法/c++】利用中序遍历和后序遍历建二叉树
数据结构·c++·算法
_安晓2 小时前
数据结构 -- 图的存储
数据结构·算法
.YY001.3 小时前
数据结构第一轮复习--第六章图包含代码
数据结构·算法
星星火柴9364 小时前
数据结构:链表 (C++实现)
数据结构·c++·笔记·链表
在努力的韩小豪4 小时前
B树和B+树的区别(B Tree & B+ Tree)
数据结构·数据库·b树·b+树·索引·数据库索引
ん贤4 小时前
2024第十五届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组
c语言·数据结构·c++·经验分享·笔记·算法·蓝桥杯
Phoebe鑫5 小时前
数据结构每日一题day11(链表)★★★★★
数据结构·算法
Jay_See6 小时前
Leetcode——239. 滑动窗口最大值
java·数据结构·算法·leetcode