【带你了解C++标准库为何在八大链表结构中选择了它】双向循环带头链表的实质性操作

简介: 【带你了解C++标准库为何在八大链表结构中选择了它】双向循环带头链表的实质性操作

在这里插入图片描述

文章目录

✨逻辑实现text.c
✨头文件List.h
✨函数实现List.c


🚀八大链表结构为何选择了它

C++的STL库选择的最终链表结构为双向循环带头链表
为什么选择了它呢,是因为它的结构更优,虽然形式看似复杂,但的它便利性相比其他链表好得多

C++标准库中把list设计为带头节点的双向循环链表是很合理的,不信你往后看它的操作实现过程相比于单链表来说有多简单,当然你也可以用其他的六种结果来对比,结果一定是双向循环带头链表更胜一筹
认识八种链表的类型
  1. 单向带头非循环链表
  2. 单向不带头循环链表
  3. 单向不带头非循环链表
  4. 双向带头循环链表
  5. 双向带头非循环链表
  6. 双向不带头循环链表
  7. 双向不带头非循环链表
  8. 双向循环带头链表✨
一些链表图示
image.png

常用的两类链表结构
我们在单链表的一系列操作中讲过单链表是在一些OJ题的常考题

单链表只能单向循环链表
循环双向链表就是一个环形,可以逆时针走,可以顺时针走,而双向链表是一个链,只能双向遍历。image.png

🚀初始化和打印

初始化

我们的 双向循环链表采用的是 带哨兵位的头结点,它的初始化为了避免要传二级指针,可以 设置用返回值的函数带回地址
下面一张图再次带你认识 带哨兵位的头结点和 不带哨兵位的头结点区别

image.png

要注意双向循环带头链表的初始化跟单链表有所区别,双向循环链表是不会指向NULL的,它没有节点的时候是下面的这种形式>

image.png

其实就是自己的头和尾都指向自己,这才能体现它的循环结构

在初始的时候是需要创建新节点作为头结点的,而且后续的头插,尾插等等都需要创建新节点,为了避免重复操作,直接建立一个BuyListNode函数用来创建新节点

//创建新节点
LTNode* BuyListNode(LTDateType x)
{
    LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
    if (newNode == NULL)
    {
        exit(-1);
    }
    newNode->date = x;
    newNode->next = NULL;
    newNode->prev = NULL;

    return newNode;
}
//初始化
LTNode* ListInit()
{
    LTNode* phead = BuyListNode(0);

    phead->prev = phead;
    phead->next = phead;
    
    return phead;
}
//用一个指针接收地址
LTNode* list = ListInit();

打印

打印过程图解
image.png
//打印
void ListPrint(LTNode* phead)
{
    LTNode* cur = phead->next;
    while (cur != phead)
    {
        printf("%d ", cur->date);
        cur = cur->next;
    }
    printf("\n");
}

🚀尾插和尾删

尾插

双向循环带头链表的 尾插实在比单链表简单得多了,因为它是循环双向链表,你看 尾节点的next不就指向 头结点的phead吗,那 头结点的prev不就指向 尾节点
是不是就直接省去了像单链表哪样遍历链表找尾的过程,直接将 时间复杂度优化到O(1)
那找到尾之后的操作就是连接节点之间的关系,比较简单,直接看图理解
image.png
//尾插
void ListPushBack(LTNode* phead,LTDateType x)
{
    assert(phead);
    //新节点
    LTNode* newNode = BuyListNode(x);
    //找尾
    LTNode* tail = phead->prev;
    //连接
    tail->next = newNode;
    newNode->prev = tail;
    newNode->next = phead;
    phead->prev = newNode;

}

尾删

尾删也不难,同样的找尾步骤,注意的是要记录好尾节点的前一个节点,让它来当新的尾节点

动画演示>在这里插入图片描述

//尾删
void ListPopBack(LTNode* phead)
{
    assert(phead);
    assert(phead->next != phead);
    LTNode* tail = phead->prev;
    LTNode* tailPrev = tail->prev;
    free(tail);
    tailPrev->next = phead;
    phead->prev = tailPrev;
}

🚀头插和头删

头插

头插要注意的是不是在哨兵位的头前面插入,而是在哨兵位的头结点的后一个插入,因为哨兵位只是是不存放有效数据的,但它一定是在最前的

动画演示>
在这里插入图片描述

//头插
void ListPushFront(LTNode* phead, LTDateType x)
{
    assert(phead);
    LTNode* newNode = BuyListNode(x);
    //连接
    LTNode* next = phead->next;
    phead->next = newNode;
    newNode->prev = phead;

    newNode->next = next;
    next->prev = newNode;

}

头删

头删也是像尾删一样要记录好要删节点的下一个节点

在这里插入图片描述

//头删
void ListPopFront(LTNode* phead)
{
    assert(phead);
    //链表为空
    assert(phead->next != phead);

    LTNode* next = phead->next;
    LTNode* nextNext = next->next;
    free(next);
    phead->next = nextNext;
    nextNext->prev = phead;

}

🚀查找和插入

查找

查找就是遍历链表,跟单链表一样的操作,找到了就返回改节点的地址,找不到就返回NULL
//查找
LTNode* ListFind(LTNode* phead, LTDateType x)
{
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->date == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

插入

插入是往该数据的前一个位置插入
插入就跟单链表的操作大大不同了, 双向循环带头链表的插入比起 单链表来说是非常容易的,你把你要在哪个位置插入的指针传给它,它可以直接找到 前一个节点,而单链表却没有这个性质

插入功能是和查找功能搭配使用的,用之前先用查找获取把该数据的指针,然后传给插入函数
那你们想想,如果我把 phead的指针传给插入函数,那phead的前一个不就是尾节点吗,那不就是相当于 尾插吗,同样如果我们把 phead的next传给插入函数,哪不就是相当于 头插吗!!
所以我们的头插和尾插是不是可以改造成这样子
image.png

这样之后我们写头插和尾插就省去了很大时间

//插入
void ListInsert(LTNode* pos, LTDateType x)
{
    assert(pos);

    LTNode* newNode = BuyListNode(x);


    LTNode* posPrev = pos->prev;

    posPrev->next = newNode;
    newNode->prev = posPrev;
    newNode->next = pos;
    pos->prev = newNode;
}

🚀删除和销毁

删除

删除也是搭配查找函数一起使用,把要的地址传过去即可删除

在这里插入图片描述

//删除
void ListErase(LTNode* pos)
{
    assert(pos);

    LTNode* posPrev = pos->prev;
    LTNode* posNext = pos->next;

    posPrev->next = posNext;
    posNext->prev = posPrev;
    free(pos);
}

销毁

销毁就是遍历链表一个一个的free即可
//销毁链表
void ListDestroy(LTNode* phead)
{
    LTNode* cur = phead->next;

    while (cur != phead)
    {
        LTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
}
//记得最后把销毁链表,还要把list置空
    ListDestroy(list);
    list = NULL;

🚀小结
如果你动手实操了,一定感受到双向循环链表中的一些操作实现是不用考虑像单链表那样的分情况讨论,比如单链表在实现头插的时候不就是要分链表为空和不为空的情况吗,而它却不必考虑这些顾虑,还有很多区别于其他链表的优势,只能你自己去一 一感受!!

✨链表功能动画演示

在这里插入图片描述

✨逻辑实现text.c
text.c为工程整体逻辑实现
void menu()
{
    printf("*******************************\n");
    printf("       1.尾插   2.尾删         \n");
    printf("       3.头插   4.头删         \n");
    printf("       5.插入   6查找          \n");
    printf("       7.删除   -1.退出        \n");
    printf("       8.打印                  \n");
    printf("*******************************\n");
    printf(" 请选择>:   \n");

}
void Textmenu()
{
    LTNode* list = ListInit();
    int input = 0;
    LTDateType x1 = 0;
    LTNode* pos = NULL;
    while(input != -1)
    {
        menu();
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("请输入要尾插的数据,以-1为结束\n");
            scanf("%d", &x1);
            while(x1 != -1)
            {
                ListPushBack(list, x1);
                scanf("%d", &x1);
            }
            printf("尾插成功\n");
            break;
        case 2:
            ListPopBack(list);
            printf("尾删成功\n");
            break;
        case 3:
            printf("请输入要头插的数据,以-1为结束\n");
            scanf("%d", &x1);
            while (x1 != -1)
            {
                ListPushFront(list, x1);
                scanf("%d", &x1);
            }
            printf("头插成功\n");
            break;
        case 4:
            ListPopFront(list);
            printf("头删成功\n");
            break;
        case 5:
            printf("请输入你要在哪里个数据前插入>:\n");
            scanf("%d", &x1);
             pos= ListFind(list, x1);
             if (pos != NULL)
             {
                 printf("请输入你要插入的数据>:\n");
                 scanf("%d", &x1);
                 ListInsert(pos, x1);
                 printf("插入成功\n");
             }
             else
             {
                 printf("无此数据\n");
             }
            break;
        case 6:
            printf("请输入你要查找的数据\n");
            scanf("%d", &x1);
            pos=ListFind(list, x1);
            if (pos != NULL)
            {
                printf("%d的地址为%p\n", x1, pos);
            }
            else
            {
                printf("链表没有此数据\n");
            }
            break;
        case 7:
            printf("请输入你要删除的数据\n");
            scanf("%d", &x1);
            pos = ListFind(list, x1);
            if (pos != NULL)
            {
                ListErase(pos);
                printf("删除成功\n");
            }
            else
            {
                printf("链表中没有此数据\n");
            }
            break;
        case 8:
            printf("链表数据为:");
            ListPrint(list);
            break;
        case -1:
            printf("退出成功\n");
            break;
        default:
            printf("无此选项,请重新输入!\n");
            break;
        }
    }
    //记得最后把销毁链表,还要把list置空
    ListDestroy(list);
    list = NULL;
    
}

int main()
{
    Textmenu();
}

✨头文件List.h
List.h为工程头文件(头文件和函数声明)
#pragma once

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

typedef int LTDateType;

typedef struct ListNode
{
    LTDateType date;
    struct ListNode* next;
    struct ListNode* prev;
}LTNode;

//初始化
LTNode* ListInit();

//打印
void ListPrint(LTNode* phead);

//创建新节点
LTNode* BuyListNode(LTDateType x);

//尾插
void ListPushBack(LTNode* phead, LTDateType x);

//尾删
void ListPopBack(LTNode* phead);

//头插
void ListPushFront(LTNode* phead, LTDateType x);

//头删
void ListPopFront(LTNode* phead);

//查找,找到返回对应数据地址,找不到返回NULL
LTNode* ListFind(LTNode* phead, LTDateType x);

//插入,在pos位置之前插入
void ListInsert(LTNode* pos, LTDateType x);

//删除指定位置
void ListErase(LTNode* pos);

//销毁链表
void ListDestroy(LTNode* phead);

✨函数实现List.c
List.c为工程源文件(函数实现)
#define _CRT_SECURE_NO_WARNINGS
#include"List.h"

//初始化
LTNode* ListInit()
{
    LTNode* phead = BuyListNode(0);

    phead->prev = phead;
    phead->next = phead;
    
    return phead;
}
//打印
void ListPrint(LTNode* phead)
{
    LTNode* cur = phead->next;
    while (cur != phead)
    {
        printf("%d ", cur->date);
        cur = cur->next;
    }
    printf("\n");
}

//尾插
void ListPushBack(LTNode* phead,LTDateType x)
{
    assert(phead);
    //新节点
    //LTNode* newNode = BuyListNode(x);
    //找尾
    //LTNode* tail = phead->prev;
    //连接
    //tail->next = newNode;
    //newNode->prev = tail;
    //newNode->next = phead;
    //phead->prev = newNode;
    
    //可用插入函数代替尾插
    ListInsert(phead, x);
}
//尾删
void ListPopBack(LTNode* phead)
{
    assert(phead);
    assert(phead->next != phead);
    //LTNode* tail = phead->prev;
    //LTNode* tailPrev = tail->prev;
    //free(tail);
    //tailPrev->next = phead;
    //phead->prev = tailPrev;

    //可用删除函数代替尾删
    ListErase(phead->prev);
}

//头插
void ListPushFront(LTNode* phead, LTDateType x)
{
    assert(phead);
    LTNode* newNode = BuyListNode(x);
    //连接
    LTNode* next = phead->next;
    phead->next = newNode;
    newNode->prev = phead;

    newNode->next = next;
    next->prev = newNode;

    //可用插入函数代替头插
    //ListInsert(phead->next, x);

}

//头删
void ListPopFront(LTNode* phead)
{
    assert(phead);
    //链表为空
    assert(phead->next != phead);

    LTNode* next = phead->next;
    LTNode* nextNext = next->next;
    free(next);
    phead->next = nextNext;
    nextNext->prev = phead;

    //可用删除函数代替头删
    //ListErase(phead->next);

}

//查找
LTNode* ListFind(LTNode* phead, LTDateType x)
{
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->date == x)
            return cur;
        cur = cur->next;
    }
    return NULL;
}

//插入
void ListInsert(LTNode* pos, LTDateType x)
{
    assert(pos);

    LTNode* newNode = BuyListNode(x);


    LTNode* posPrev = pos->prev;

    posPrev->next = newNode;
    newNode->prev = posPrev;
    newNode->next = pos;
    pos->prev = newNode;
}
//删除
void ListErase(LTNode* pos)
{
    assert(pos);

    LTNode* posPrev = pos->prev;
    LTNode* posNext = pos->next;

    posPrev->next = posNext;
    posNext->prev = posPrev;
    free(pos);
}
//创建新节点
LTNode* BuyListNode(LTDateType x)
{
    LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
    if (newNode == NULL)
    {
        exit(-1);
    }
    newNode->date = x;
    newNode->next = NULL;
    newNode->prev = NULL;

    return newNode;
}

//销毁链表
void ListDestroy(LTNode* phead)
{
    LTNode* cur = phead->next;

    while (cur != phead)
    {
        LTNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
}
相关文章
|
C++ 容器
C++中向量的操作vector
C++中向量的操作vector
179 6
|
9月前
|
存储 监控 算法
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
在数字化办公时代,公司监控上网软件成为企业管理网络资源和保障信息安全的关键工具。本文深入剖析C++中的链表数据结构及其在该软件中的应用。链表通过节点存储网络访问记录,具备高效插入、删除操作及节省内存的优势,助力企业实时追踪员工上网行为,提升运营效率并降低安全风险。示例代码展示了如何用C++实现链表记录上网行为,并模拟发送至服务器。链表为公司监控上网软件提供了灵活高效的数据管理方式,但实际开发还需考虑安全性、隐私保护等多方面因素。
190 0
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
|
10月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
490 5
|
C++
【C++基础】程序流程结构详解
这篇文章详细介绍了C++中程序流程的三种基本结构:顺序结构、选择结构和循环结构,包括if语句、三目运算符、switch语句、while循环、do…while循环、for循环以及跳转语句break、continue和goto的使用和示例。
312 2
|
存储 算法 C语言
C语言手撕实战代码_循环单链表和循环双链表
本文档详细介绍了用C语言实现循环单链表和循环双链表的相关算法。包括循环单链表的建立、逆转、左移、拆分及合并等操作;以及双链表的建立、遍历、排序和循环双链表的重组。通过具体示例和代码片段,展示了每种算法的实现思路与步骤,帮助读者深入理解并掌握这些数据结构的基本操作方法。
343 2
|
C++ 容器
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——AVL树
147 5
【刷题记录】链表的回文结构
【刷题记录】链表的回文结构
105 1
【数据结构OJ题】链表的回文结构
牛客题目——链表的回文结构
163 0
【数据结构OJ题】链表的回文结构
|
存储 C++
C++的list-map链表与映射表
```markdown C++ 中的`list`和`map`提供链表和映射表功能。`list`是双向链表,支持头尾插入删除(`push_front/push_back/pop_front/pop_back`),迭代器遍历及任意位置插入删除。`map`是键值对集合,自动按键排序,支持直接通过键来添加、修改和删除元素。两者均能使用范围for循环遍历,`map`的`count`函数用于统计键值出现次数。 ```
225 1
|
存储 Java 开发者
揭秘!HashMap底层结构大起底:从数组到链表,再到红黑树,Java性能优化的秘密武器!
【8月更文挑战第24天】HashMap是Java集合框架中的核心组件,以其高效的键值对存储和快速访问能力广受开发者欢迎。在JDK 1.8及以后版本中,HashMap采用了数组+链表+红黑树的混合结构,实现了高性能的同时解决了哈希冲突问题。数组作为基石确保了快速定位;链表则用于处理哈希冲突;而当链表长度达到一定阈值时,通过转换为红黑树进一步提升性能。此外,HashMap还具备动态扩容机制,当负载因子超过预设值时自动扩大容量并重新哈希,确保整体性能。通过对HashMap底层结构的深入了解,我们可以更好地利用其优势解决实际开发中的问题。
343 0
下一篇
oss云网关配置