数据结构---手撕图解双向循环链表

简介: 数据结构---手撕图解双向循环链表

[toc]

写在前面

在前面学完单链表后,我们思考这样一个问题,单链表和顺序表比起来,功能确实相当强大,有很多优势,但是于此同时,我们也应思考下面的问题

==单链表有什么不足的地方?==

如果你把单链表的各个函数都自己实现过,那么下面的问题你一定有相同的感悟

  1. 单链表实现尾插尾删等操作,需要寻找尾部节点,而寻找尾部节点是一个相当繁琐的过程,需要从前向后寻找尾
  2. 单链表实现插入功能,对于单链表来说,通常实现的是在pos后插入,而不在pos前插入,原因就在于pos向前插入需要寻找前面的节点,而这个节点只能从前向后遍历寻找
  3. 单链表在实现函数时,要时时刻刻想到链表为空的情况,对于链表为空的情况要有特殊的处理方式

==那么能不能想办法解决下面这些问题?==

  1. 找尾部节点方便
  2. 找一个节点的上一个节点方便
  3. 不需要考虑空链表的空指针问题

于是数据结构中对于双向循环链表就解决了这个问题


双向循环链表的构造布局

带有哨兵位的布局

==首先介绍什么是哨兵位==

和它的名字一样,所谓哨兵位就是一个站哨的位置,它并不属于真实的队伍中的成员,而在链表中也是如此,在前面总结单链表所学的知识时,没有引入哨兵位的链表,而是直接用空链表进行写入,这样的目的不仅仅在于方便后续的OJ练习,同时也是适应特殊情况,为后续的c++学习做铺垫

而在双向循环链表中我们引入哨兵位的概念,作为哨兵的位置,它本身并不属于链表中的一部分,只是充当一个头的位置

==哨兵位怎么设置?==

链表的每一个节点我们都是通过结构体创建出来的,而很明显,哨兵位也是链表的节点,就意味着哨兵位也有指针部分和数据部分,那么对于数据部分我们应该怎么对它定义?

在一部分书中,哨兵位的数字部分会定义的链表的长度,也就是链表中元素的个数,这样乍一看似乎还不错,借助这个哨兵位还能获取到链表的长度

但是这样真的没问题吗?

事实上这是存在一定的问题的,假设这里选取的是char类型的数据类型,对于char类型的数据范围是从-128到127(char类型占1个字节决定的) 那么这里就有了一个新的问题,假设链表中存储的是char类型的数据,那么假设链表的长度为128呢?那么就会导致链表的实际长度和存储长度有很大差距

于是这里对于哨兵位我并不对它的数据部分有特殊的意义,单纯给它赋予一个值-1

==带哨兵位的双向循环链表如下==

在这里插入图片描述
上面就是双向循环链表的示意图,从中可以看出,每一个节点可以轻松找到它的下一个节点,以及最后一个节点和头节点是循环在一起的

既然是双向的链表,那么在定义结构体的过程就和单链表有所不同,这里的指针部分应该有两个,prev和next部分,那么结构体的定义如下

typedef int LTDataType;
typedef struct ListNode
{
   
   
    LTDataType _data;
    struct ListNode* _next;
    struct ListNode* _prev;
}ListNode;

这样就实现了对链表的定义,prev和next均有

和单链表相同,双向循环链表也离不开增删查改的基本操作,那么这里我们一个一个来实现这些功能

链表的构建

链表的构建就是构建一个phead的哨兵位节点,这个过程还是很简单的

ListNode* ListCreate()
{
   
   
    ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
    if (phead == NULL)
    {
   
   
        perror("malloc fail");
    }
    assert(phead);
    phead->_data = -1;
    phead->_next = phead;
    phead->_prev = phead;
    return phead;
}

链表的销毁

有创建就离不开销毁,销毁的过程和单链表基本相似,都是通过指针把一个节点单独拿出来,free后再置空,代码实现如下

void ListDestory(ListNode* pHead)
{
   
   
    assert(pHead);

    ListNode* cur = pHead->_next;
    while (cur != pHead)
    {
   
   
        ListNode* next = cur->_next;
        free(cur);
        cur = next;
    }
}

链表的输出

链表要打印在屏幕上的基本思路也和单链表基本一致,但需要注意的是,单链表的截止条件是当指针访问到空,而双向循环链表的条件是指针访问到头

void ListPrint(ListNode* pHead)
{
   
   
    assert(pHead);
    ListNode* cur = pHead->_next;
    printf("guard<==>");
    while (cur != pHead)
    {
   
   
        printf("%d<==>", cur->_data);
        cur = cur->_next;
    }
    printf("\n");
}

这样,双向循环链表也可以进行可视化管理了,那么下面就开始实现增删查改四大功能

链表的尾插

和单链表相似,但和它比起来更加简单,示意图如下:

在这里插入图片描述
那么代码是如何实现的?代码实现如下

ListNode* BuyListnode(LTDataType x)
{
   
   
    ListNode* phead = (ListNode*)malloc(sizeof(ListNode));
    if (phead == NULL)
    {
   
   
        perror("malloc fail");
    }
    assert(phead);
    phead->_data = x;
    phead->_next = NULL;
    phead->_prev = NULL;
    return phead;
}

void ListPushBack(ListNode* pHead, LTDataType x)
{
   
   
    assert(pHead);
    ListNode* newnode = BuyListnode(x);
    ListNode* prevnode = pHead->_prev;

    prevnode->_next = newnode;
    newnode->_prev = prevnode;

    newnode->_next = pHead;
    pHead->_prev = newnode;
}

对比单链表的尾插不难发现,这个过程比单链表简单了很多,首先对于空链表的判断就更为简单,同时双向循环链表由于存在双向箭头,导致插入是很便利的,这个过程在pos位置插入时候会体现出双向链表特有的优势

链表的尾删

在这里插入图片描述
代码实现如下

bool LTempty(ListNode* pHead)
{
   
   
    assert(pHead);
    return  pHead->_next == pHead;
}

void ListPopBack(ListNode* pHead)
{
   
   
    assert(pHead);
    assert(!LTempty(pHead));

    ListNode* tail = pHead->_prev;

    pHead->_prev = tail->_prev;
    tail->_prev->_next = pHead;

    free(tail);
    tail = NULL;
}

== 这里我们对bool返回值的函数进行补充==

bool值意为真和假,在进行尾删时,我们需要首先进行判断链表到底是否为空,但由于双向循环链表,于是我们并不能直观通过判断空指针,这里封装了一个函数,用来判断是否为空,如果为空就返回真,如果不为空就返回假,那么我们在函数体内断言只需要看它不为空即可

链表的头插

在这里插入图片描述
代码实现如下:

void ListPushFront(ListNode* pHead, LTDataType x)
{
   
   
    assert(pHead);
    ListNode* next = pHead->_next;
    ListNode* newnode = BuyListnode(x);
    assert(newnode);

    pHead->_next = newnode;
    newnode->_prev = pHead;

    next->_prev = newnode;
    newnode->_next = next;
}

链表的头删

从头删中其实就体现出了双向链表的优越性

在这里插入图片描述

void ListPopFront(ListNode* pHead)
{
   
   
    assert(pHead);
    assert(!LTempty(pHead));
    ListNode* first = pHead->_next;
    ListNode* second = first->_next;

    pHead->_next = second;
    second->_prev = pHead;

    free(first);
    first = NULL;
}

链表的查找

和单链表基本类似,这里不多介绍

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
   
   
    assert(pHead);
    ListNode* cur = pHead->_next;

    while (cur->_data != x)
    {
   
   
        cur = cur->_next;
    }

    return cur;
}

链表的插入

在pos前插入的原理如下

在这里插入图片描述

从中和单链表一对比就能够体现出双向链表的优越的地方,在单链表中我们通常不在pos前插入,原因就是pos前面的节点并不方便寻找,而双向链表就解决了这个问题

代码实现如下

void ListInsert(ListNode* pos, LTDataType x)
{
   
   
    ListNode* posprev = pos->_prev;
    ListNode* newnode = BuyListnode(x);
    assert(newnode);
    assert(pos);

    posprev->_next = newnode;
    newnode->_prev = posprev;

    newnode->_next = pos;
    pos->_prev = newnode;
}

链表的删除

在这里插入图片描述
这里和单链表的删除相似,不多描述

void ListErase(ListNode* pos)
{
   
   
    assert(pos);
    ListNode* posprev = pos->_prev;
    ListNode* posnext = pos->_next;

    posprev->_next = posnext;
    posnext->_prev = posprev;

    free(pos);
    pos = NULL;
}

自此,双向循环链表就实现完毕了,和单链表比起来,双向循环链表确实有它独特强大的地方,而真正使用什么数据结构还需要根据实际情况进行设计

相关文章
|
22天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
50 4
|
23天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
23天前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
26 7
|
1月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
25 3
|
1月前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
17 0
数据结构与算法学习五:双链表的增、删、改、查
|
22天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
40 0
【数据结构】——双向链表详细理解和实现
【数据结构】——双向链表详细理解和实现
|
1月前
|
存储 Java
【数据结构】链表
【数据结构】链表
18 1
|
1月前
|
存储 缓存
数据结构3——双向链表
数据结构3——双向链表
114 1