【数据结构】C语言版本的带哨兵位双向循环链表的快速实现方法

简介: 我们在之前学双向带头循环链表时,结尾部分简单讲解了快速实现的方法。本篇博客将详细讲解如何迅速实现,通过思路草图的方法轻松写出带头双向循环链表,甚至都可以直接用注释画草图。本篇博客是对 "从零开始逐步实现带哨兵位循环双向链表" 的补充,之前在写那篇博客的时候不小心忘记实现销毁接口了,这里正好能进行一个补充。

前言


我们在之前学双向带头循环链表时,结尾部分简单讲解了快速实现的方法。本篇博客将详细讲解如何迅速实现,通过思路草图的方法轻松写出带头双向循环链表,甚至都可以直接用注释画草图。本篇博客是对 "从零开始逐步实现带哨兵位循环双向链表" 的补充,之前在写那篇博客的时候不小心忘记实现销毁接口了,这里正好能进行一个补充。


一、 代码讲解


如果有人叫你快速实现一个链表,我们当然首选带头双向循环链表,因为他足够简单,虽然结构很复杂,但是就是因为结构复杂,反而让我们能够轻轻松松地实现。


我们只需要把插入和删除两个接口实现,就可以把链表的头插尾插头删尾删都搞定。直接复用插入和删除两个接口即可轻松搞定。再写一些链表常见的功能就可以大功告成了!


要实现的功能如下:


链表的初始化和销毁,链表的打印和查找,链表的插入和删除,头删尾插头插尾插。


0x00 定义链表

💬 创建 DList.h 文件:


#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int DListNodeDataType;
typedef struct DoubleListNode {
    DListNodeDataType data;  // 用来存放结点的数据
    struct DoubleListNode* next;  // 指向后继节点的指针
    struct DoubleListNode* prev;  // 指向前驱节点的指针
} DLNode;  // 重命名为DLNode
DLNode* DLNodeInit();
void DLNodeDestroy(DLNode* pHead);
void DLNodePrint(DLNode* pHead);
DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x);
void DLNodeInsert(DLNode* pos, DListNodeDataType x);
void DLNodeDelete(DLNode* pos);
void DLNodePushBack(DLNode* pHead, DListNodeDataType x);
void DLNodePushFront(DLNode* pHead, DListNodeDataType x);
void DLNodePopBack(DLNode* pHead);
void DLNodePopFront(DLNode* pHead);

0x01 初始化(DLNodeInit)

💬 创建 DList.c 文件:

#include "DList.h"
DLNode* DLNodeInit() {
    DLNode* pHead = (DLNode*)malloc(sizeof(DLNode)); // 创建哨兵位头结点
    pHead->next = pHead->prev = pHead; // 让next和prev一开始都默认指向pHead
    return pHead; // 将phead作为结果返回
}

🔑 解读:这里我们使用 malloc 函数开辟一块空间作为 "哨兵位" pHead ,next 和 prev 此时都应该指向 pHead 。最后再将 pHead 作为结果返回回去,外面就可以接收到了。


0x02 销毁(DLNodeDestroy)

💬 DList.c


void DLNodeDestroy(DLNode* pHead) {
    assert(pHead); // 防止pHead为空
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead) {  // 碰到哨兵位说明遍历完一遍了
        DLNode* next = cursor->next;  // 记录,防止free掉后cursor动不了
        free(cursor);  // 释放 cursor 当前指向的结点
        cursor = next;  // 让cursor继续往后走
    }
    free(pHead); // 干掉掉哨兵
    pHead = NULL; // 置空防野指针
}

🔑 解读:


① 这里创建一个叫 cursor 的指针,用来遍历整个链表。pHead 存的不是有效数据,所以要从pHead->next 开始。


49e15ac6b2236b23b3df3beab67ced94_25910b7ba1d348ea8aa6ecee241d593d.png


② 让 cursor 遍历每一个结点,直到碰到 pHead 结束,因为碰到 pHead 说明已经遍历完一遍了。


③ 进入循环后,创建一个 next 指针,提前记录一下 cursor->next ,可以有效阻止 free 掉之后 cursor 动不了的情况。释放完 cursor 后,再将 next 交给 cursor 就能让 cursor 继续往后走了。


④ 最后释放掉哨兵位,再把它置为空即可。


0x03 打印(DLNodePrint)

💬 DList.c

void DLNodePrint(DLNode* pHead) {
    assert(pHead);
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead) {  // 碰到哨兵位说明遍历完一遍了
        printf("%d", cursor->data); // 打印 cursor 当前指向的节点的数据
        cursor = cursor->next; // 让cursor继续往后走
    }
    printf("\n"); // 换行
}

🔑 解读:创建 cursor 指针遍历整个链表来打印数据。比较简单,这里就不过多赘述了。


0x04 查找(DLNodeFind)

💬 DList.c

DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x) {
    assert(pHead != NULL);
    DLNode* cursor = pHead->next;
    while (cursor != pHead) {
        if (cursor->data == x) {
            return cursor;    
        } else {
            cursor = cursor->next;
        }
    }
    return NULL;
}

🔑 解读:创建 cursor 指针,将 cursor->data 和 x 比较即可。


0x05 插入(DLNodeInsert)

📚 默认为 pos 位置前插入,既然是插入,我们先写好创建新结点的接口:


创建新结点(CreateNewNode)

DLNode* CreateNewNode(DListNodeDataType x) {
    DLNode* new_node = (DLNode*)malloc(sizeof(DLNode)); // 创建新结点,给一块DLNode大小的空间
    if(new_node == NULL) {  // 检查malloc
        printf("malloc failed!\n");
        exit(-1);
    }
    new_node->data = x;  // 放置数据
    new_node->next = new_node->prev = NULL;  // 默认置为空
    return new_node;
}

💬 DList.c

void DLNodeInsert(DLNode* pos, DListNodeDataType x) {
    // 思路草图: posPrev <->  new_node(待插目标) <->  pos
    assert(pos);
    DLNode* new_node = CreateNewNode(x); // 创建新节点
    DLNode* posPrev = pos->prev; // 创建pos的前驱指针
    // 把它们相互链接起来
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;
}

🔑 解读:


① 首先创建新节点。因为是在 pos 位置之前插入。为了方便,我们先标出 pos 的前驱指针 posPrev,随后我们画出草图:


思路草图: posPrev <->  new_node(待插目标) <->  pos


这么一画,思路立马就清楚了,我们就可以轻轻松松把思路转换成代码:


② 将 posPrev 与 new_node 相互链接起来:


posPrev->next = new_node;
new_node->prev = posPrev;

③ 将 new_node 与 pos 相互链接起来:

new_node->next = pos;
pos->prev = new_node;

0x06 删除(DLNodeDelete)

📚 删除默认是删除 pos 位置的节点。


💬 DList.c

void DLNodeDelete(DLNode* pos) {
    // 思路草图:  posPrev   pos(待删目标)   posNext
    //               ↓_________________________↑
    assert(pos);
    DLNode* posPrev = pos->prev;
    DLNode* posNext = pos->next;
    // 把他们互相缝合起来
    posPrev->next = posNext; 
    posNext->prev = posPrev;
    // 删除pos位置的结点(释放并置空)
    free(pos);
    pos = NULL;
}

① 既然要删除 pos 位置的节点,我们先标出 pos 的前驱指针 posPrev 和 pos 的后继指针 posNext ,随后我们画出草图:


   思路草图:  posPrev   pos(待删目标)   posNext


                            ↓_______________ ______↑


因为 pos 要被删掉了,我们需要把 posPrev 和 posNext 位置的两个节点互相缝合起来:

posPrev->next = posNext; 
posNext->prev = posPrev;

② 最后删除 pos 位置的结点,释放并置空。


0x07 尾插头插 / 尾删头删

📚 插入和删除都写好了,头插尾插和头删尾删直接复用就能轻松解决!


💬 DList.c


尾插(DLNodePushBack)
void DLNodePushBack(DLNode* pHead, DListNodeDataType x) {
    // 思路草图:  pHead   ...   end  (x)
    //            posPrev(end) <->  new_node(待插目标) <->  pos(pHead)
    assert(pHead);
    DLNodeInsert(pHead, x);
}

🔑 解读:直接调用插入接口即可。因为尾插要找到最后一个结点,所以我们传入 pHead 让它作为 pos,这样就能找到 posPrev,posPrev 就是最后一个节点。思路草图如下:

pHead   ...   end  (x)
posPrev(end) <->  new_node(待插目标) <->  pos(pHead)
头插(DLNodePushFront)
void DLNodePushFront(DLNode* pHead, DListNodeDataType x) {
    // 思路草图:  pHead     pHeadNext
    //            posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)
    assert(pHead);
    DLNodeInsert(pHead->next, x);
}

🔑 解读:因为头结点是哨兵位,存得不是有效数据,所以它的头插要在哨兵位后面(即第一个有效数据前)。思路草图如下:

pHead     pHeadNext
posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)
尾删(DLNodePopBack)
void DLNodePopBack(DLNode* pHead) {
    // 思路草图: pHead ... pos
    // 思路草图:  posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)
    //               ↓__________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->prev);
}

🔑 解读:尾删,默认删除的是 pos 位置的结点。所以我们直接传入要删除的结点即可。找到最后一个结点很简单,直接 pHead->prev 就是最后一个节点了,这就是结构的优势(而不像普通单链表要遍历链表找到尾指针)。思路草图如下:

pHead ... pos
posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)
   ↓_____________________________________↑
头删(DLNodePopFront)
void DLNodePopFront(DLNode* pHead) {
    // 思路草图: pHead  pos
    // 思路草图:  posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)
    //               ↓_______________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->next);
}

🔑 解读:头删,删除第一个有效节点,传入 pHead->next 即可。思路草图如下:

pHead  pos
posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)
     ↓________________________________________↑


二、完整代码


💬 DList.h

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int DListNodeDataType;
typedef struct DoubleListNode {
    DListNodeDataType data;  // 用来存放结点的数据
    struct DoubleListNode* next;  // 指向后继节点的指针
    struct DoubleListNode* prev;  // 指向前驱节点的指针
} DLNode;  // 重命名为DLNode
DLNode* DLNodeInit();
void DLNodeDestroy(DLNode* pHead);
void DLNodePrint(DLNode* pHead);
DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x);
void DLNodeInsert(DLNode* pos, DListNodeDataType x);
void DLNodeDelete(DLNode* pos);
void DLNodePushBack(DLNode* pHead, DListNodeDataType x);
void DLNodePushFront(DLNode* pHead, DListNodeDataType x);
void DLNodePopBack(DLNode* pHead);
void DLNodePopFront(DLNode* pHead);


💬 DList.c

#include "DList.h"
DLNode* DLNodeInit() {
    DLNode* pHead = (DLNode*)malloc(sizeof(DLNode)); // 创建哨兵位头结点
    pHead->next = pHead->prev = pHead; // 让next和prev一开始都默认指向pHead
    return pHead; // 将phead作为结果返回
}
void DLNodeDestroy(DLNode* pHead) {
    assert(pHead); // 防止pHead为空
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead) {  // 碰到哨兵位说明遍历完一遍了
        DLNode* next = cursor->next;  // 记录,防止free掉后cursor动不了
        free(cursor);  // 释放 cursor 当前指向的结点
        cursor = next;  // 让cursor继续往后走
    }
    free(pHead); // 干掉掉哨兵
    pHead = NULL; // 置空防野指针
}
void DLNodePrint(DLNode* pHead) {
    assert(pHead);
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead) {  // 碰到哨兵位说明遍历完一遍了
        printf("%d", cursor->data); // 打印 cursor 当前指向的节点的数据
        cursor = cursor->next; // 让cursor继续往后走
    }
    printf("\n"); // 换行
}
DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x) {
    assert(pHead != NULL);
    DLNode* cursor = pHead->next;
    while (cursor != pHead) {
        if (cursor->data == x) {
            return cursor;    
        } else {
            cursor = cursor->next;
        }
    }
    return NULL;
}
DLNode* CreateNewNode(DListNodeDataType x) {
    DLNode* new_node = (DLNode*)malloc(sizeof(DLNode)); // 创建新结点,给一块DLNode大小的空间
    if(new_node == NULL) {  // 检查malloc
        printf("malloc failed!\n");
        exit(-1);
    }
    new_node->data = x;  // 放置数据
    new_node->next = new_node->prev = NULL;  // 默认置为空
    return new_node;
}
void DLNodeInsert(DLNode* pos, DListNodeDataType x) {
    // 思路草图: posPrev <->  new_node(待插目标) <->  pos
    assert(pos);
    DLNode* new_node = CreateNewNode(x); // 创建新节点
    DLNode* posPrev = pos->prev; // 创建pos的前驱指针
    // 把它们相互链接起来
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;
}
void DLNodeDelete(DLNode* pos) {
    // 思路草图:  posPrev   pos(待删目标)   posNext
    //               ↓_________________________↑
    assert(pos);
    DLNode* posPrev = pos->prev;
    DLNode* posNext = pos->next;
    // 把他们互相缝合起来
    posPrev->next = posNext; 
    posNext->prev = posPrev;
    // 删除pos位置的结点(释放并置空)
    free(pos);
    pos = NULL;
}
void DLNodePushBack(DLNode* pHead, DListNodeDataType x) {
    // 思路草图:  pHead   ...   end  (x)
    //            posPrev(end) <->  new_node(待插目标) <->  pos(pHead)
    assert(pHead);
    DLNodeInsert(pHead, x);
}
void DLNodePushFront(DLNode* pHead, DListNodeDataType x) {
    // 思路草图:  pHead     pHeadNext
    //            posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)
    assert(pHead);
    DLNodeInsert(pHead->next, x);
}
void DLNodePopBack(DLNode* pHead) {
    // 思路草图: pHead ... pos
    // 思路草图:  posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)
    //               ↓__________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->prev);
}
void DLNodePopFront(DLNode* pHead) {
    // 思路草图: pHead  pos
    // 思路草图:  posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)
    //               ↓_______________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->next);
}
相关文章
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
548 1
|
10月前
|
定位技术 C语言
c语言及数据结构实现简单贪吃蛇小游戏
c语言及数据结构实现简单贪吃蛇小游戏
|
11月前
|
搜索推荐 C语言
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
|
存储 算法 C语言
【C语言】深入浅出:C语言链表的全面解析
链表是一种重要的基础数据结构,适用于频繁的插入和删除操作。通过本篇详细讲解了单链表、双向链表和循环链表的概念和实现,以及各类常用操作的示例代码。掌握链表的使用对于理解更复杂的数据结构和算法具有重要意义。
3450 6
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
359 5
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
431 1
|
3月前
|
存储 C语言
`scanf`是C语言中用于按格式读取标准输入的函数
`scanf`是C语言中用于按格式读取标准输入的函数,通过格式字符串解析输入并存入指定变量。需注意输入格式严格匹配,并建议检查返回值以确保读取成功,提升程序健壮性。
1007 0
|
11月前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
632 23
|
5月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
324 15
|
10月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
412 1
一文彻底搞清楚C语言的函数