【数据结构】双向循环链表

简介: 双向循环带头链表的实现

1. 前言

上一篇博客我们介绍了无头单向链表,但它一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。但是如果我们想要用链表来存储数据,又该怎么办呢?接下来我们就要学习一种新的链表——带头双向循环链表,它是链表中结构最复杂的,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。不过虽然该链表的结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
image.png


2. 链表的实现

List.h

❤️ 头文件的包含、结构体类型的定义以及各个接口函数的声明

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int LTDataType;
typedef struct ListNode {
   
   
    LTDataType data;
    struct ListNode* prev;
    struct ListNode* next;
}LTNode;

//初始化双向循环带头链表
LTNode* LTInit();

//开辟新结点
LTNode* BuyLTNode(LTDataType x);

//双向链表的打印
void LTPrint(LTNode* phead);

//双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x);

//双向链表的尾删
void LTPopBack(LTNode* phead);

//双向链表头删
void LTPopFront(LTNode* phead);

//双向链表头插
void LTPushFront(LTNode* phead, LTDataType x);

//双向链表查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos之前插入x
void LTInsert(LTNode* pos, LTDataType x);

//删除pos节点
void LTErase(LTNode* pos);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//返回链表中的节点个数
size_t LTSize(LTNode* phead);

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

List.c

💖 所有接口函数的实现

🐶 初始化双向循环带头链表

LTNode* LTInit()
{
   
   
    LTNode* DummyNode = (LTNode*)malloc(sizeof(LTNode));
    if (!DummyNode) {
   
   
        perror("malloc fail::");
        exit(-1);
    }
    DummyNode->next = DummyNode;
    DummyNode->prev = DummyNode;
    return DummyNode;
}

首先先创建一个哨兵位的头节点,该节点不存储任何数据,这样我们在主函数中实现别的接口函数传参时就不需要传递二级指针,直接通过返回的哨兵位头节点来实现结构体指针的初始化,然后后续通过哨兵位头节的索引来改变结构体中的内容。这里我们先开辟一个头节点,然后将头节点的两个指针都指向自己。


🐱 开辟新结点

LTNode* BuyLTNode(LTDataType x)
{
   
   
    LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    if (!newnode) {
   
   
        perror("malloc fail::");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    newnode->prev = NULL;
    return newnode;
}

因为我们实现链表的插入时会经常开辟一个新的节点,然后将节点的数据域赋值为想要插入的数据,所以这里我们直接将该功能打包成一个函数,返回值为结构体指针类型,以便我们需要的时候直接调用即可。在这里我们需要注意的是最好将结构体中的两个指针全部置为空,防止野指针的出现。


🐭 双向链表的打印

void LTPrint(LTNode* phead)
{
   
   
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead)
    {
   
   
        printf("%d ", cur->data);
        cur = cur->next;
    }
    printf("\n");
}

我们要打印这个链表,就要遍历这个链表中除了头节点的每一个节点,但我们又不能单链表一样,找到它的最后一个节点为空,所以这里我们需要从哨兵位的下一个节点开始遍历,直到下一个节点等于哨兵位为止停止遍历。在遍历的过程中,将每一个节点的数据打印出来即可。


🐹 双向链表尾插尾删

//双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
   
   
    assert(phead);
    LTNode* newnode = BuyLTNode(x);
    LTNode* tail = phead->prev;
    tail->next = newnode;
    newnode->prev = tail;
    newnode->next = phead;
    phead->prev = newnode;
}
//双向链表的尾删
void LTPopBack(LTNode* phead)
{
   
   
    assert(phead);
    assert(phead->next != phead);
    LTNode* tail = phead->prev;
    tail->prev->next = phead;
    phead->prev = tail->prev;
    free(tail);
}

要进行尾插尾删我们就要找最后一个节点,这里比较方便的是我们不用去找尾节点,因为头节点的prev指针指向的就是尾节点。最后只需要将指针间的对应关系进行链接即可。


🐰 双向链表头插头删

//双向链表头插
void LTPushFront(LTNode* phead, LTDataType x)
{
   
   
    assert(phead);
    LTNode* headNext = phead->next;
    LTNode* newnode = BuyLTNode(x);
    newnode->next = headNext;
    headNext->prev = newnode;
    phead->next = newnode;
    newnode->prev = phead;
}
//双向链表头删
void LTPopFront(LTNode* phead)
{
   
   
    assert(phead);
    assert(phead->next != phead);
    LTNode* headNext = phead->next;
    phead->next = headNext->next;
    headNext->next->prev = phead;
    free(headNext);
}

要进行头插头删也是非常容易的,注意在头删的时候链表不能只有哨兵位的
头节点,那样会把哨兵位的头节点也删掉,在后续要进行其他的操作时就会出错。


🦊 双向链表查找

LTNode* LTFind(LTNode* phead, LTDataType x)
{
   
   
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead)
    {
   
   
        if (cur->data == x) {
   
   
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

要查找指定数据,就需要遍历链表,如果可以找到直接返回找到的节点,否则返回一个空指针。


🐻 在pos之前插入x/删除pos节点

//在pos之前插入x
void LTInsert(LTNode* pos, LTDataType x)
{
   
   
    LTNode* prevPos = pos->prev;
    LTNode* newnode = BuyLTNode(x);
    newnode->next = pos;
    pos->prev = newnode;
    prevPos->next = newnode;
    newnode->prev = prevPos;
}
//删除pos节点
void LTErase(LTNode* pos)
{
   
   
    LTNode* posPrev = pos->prev;
    LTNode* posNext = pos->next;
    posPrev->next = posNext;
    posNext->prev = posPrev;
    free(pos);
}

这两个操作都是插入或者删除一个节点,和上面的头尾指针插入删除操作基本一致,这里不过多解释。


🐼 判断链表是否为空

bool LTEmpty(LTNode* phead)
{
   
   
    assert(phead);
    return phead->next == phead;
}

这里直接判断哨兵位头节点的下一个节点是不是自己即可。


🐨 返回链表中的节点个数

size_t LTSize(LTNode* phead)
{
   
   
    size_t size = 0;
    if (phead->next == phead)
        return 0;
    LTNode* cur = phead->next;
    while (cur != phead)
    {
   
   
        size++;
        cur = cur->next;
    }
    return size;
}

这里如果链表为空直接返回0,否则遍历链表统计节点个数即可。


:cow: 销毁链表

void LTDestroy(LTNode* phead)
{
   
   
    assert(phead);
    LTNode* cur = phead->next;
    while (cur != phead)
    {
   
   
        LTNode* curNext = cur->next;
        free(cur);
        cur = curNext;
    }
    free(phead);
}

遍历链表依次销毁每一个节点即可,这里我们需要注意最后需要把哨兵位的头节点也释放掉,在主函数中置为空。


Test.h

💞 测试各个接口函数的功能

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

void TestList1()
{
   
   
    LTNode* LT = LTInit();//测试尾插尾删
    LTPushBack(LT, 1);
    LTPushBack(LT, 2);
    LTPushBack(LT, 3);
    LTPushBack(LT, 4);
    LTPushBack(LT, 5);
    LTPrint(LT);
    LTPopBack(LT);
    LTPrint(LT);
    LTPopBack(LT);
    LTPrint(LT);
}
void TestList2()
{
   
   
    LTNode* LT = LTInit();//测试头插头删
    LTPushFront(LT, 1);
    LTPushFront(LT, 2);
    LTPushFront(LT, 3);
    LTPushFront(LT, 4);
    LTPushFront(LT, 5);
    LTPrint(LT);
    LTPopFront(LT);
    LTPrint(LT);
    LTPopFront(LT);
    LTPrint(LT);
    LTPopFront(LT);
    LTPrint(LT);
    LTPopFront(LT);
    LTPrint(LT);
}
void TestList3()
{
   
   
    LTNode* LT = LTInit();//测试查找指定值、指定位置插入节点
    LTPushBack(LT, 1);
    LTPushBack(LT, 2);
    LTPushBack(LT, 3);
    LTPushBack(LT, 4);
    LTPushBack(LT, 5);
    LTPrint(LT);
    LTNode* pos = LTFind(LT, 5);
    if (pos) {
   
   
        LTInsert(pos, 500);
    }
    LTPrint(LT);
    pos = LTFind(LT, 1);
    if (pos) {
   
   
        LTInsert(pos, 100);
    }
    LTPrint(LT);
}
void TestList4()
{
   
   
    LTNode* LT = LTInit();//测试删除指定节点
    LTPushBack(LT, 1);
    LTPushBack(LT, 2);
    LTPushBack(LT, 3);
    LTPushBack(LT, 4);
    LTPushBack(LT, 5);
    LTPrint(LT);
    LTNode* pos = LTFind(LT, 2);
    if (pos) {
   
   
        LTErase(pos);
    }
    LTPrint(LT);
    pos = LTFind(LT, 1);
    if (pos) {
   
   
        LTErase(pos);
    }
    LTPrint(LT);
    pos = LTFind(LT, 5);
    if (pos) {
   
   
        LTErase(pos);
    }
    LTPrint(LT);
}
void TestList5()
{
   
   
    LTNode* LT = LTInit();//测试链表判空、返回链表节点个数、销毁链表
    LTPushBack(LT, 1);
    LTPushBack(LT, 2);
    LTPushBack(LT, 3);
    LTPushBack(LT, 4);
    LTPushBack(LT, 5);
    LTPrint(LT);
    if (LTEmpty(LT)) {
   
   
        printf("链表为空\n");
    }else {
   
   
        printf("链表不为空\n");
        printf("节点个数为:%d\n", LTSize(LT));
    }
    LTDestroy(LT);
}
int main()
{
   
   
    /*TestList1();
    TestList2();
    TestList3();
    TestList4();*/
    TestList5();
    return 0;
}

3. 总结

好了,以上内容就是今天对双向循环链表的讲解了,相比单向不带头链表,它的效率更高、更适合用链表来存储数据。到这里链表的基础知识就结束了,后续会继续讲解链表的OJ练习。如果觉得有收获的话还望支持一下博主哦!


相关文章
|
29天前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
53 4
|
22天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
43 5
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
76 4
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
29天前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
47 0
|
2月前
|
存储 Java
数据结构第三篇【链表的相关知识点一及在线OJ习题】
数据结构第三篇【链表的相关知识点一及在线OJ习题】
29 7
|
2月前
|
存储 安全 Java
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
【用Java学习数据结构系列】探索顺序表和链表的无尽秘密(附带练习唔)pro
27 3
|
2月前
|
算法 Java
数据结构与算法学习五:双链表的增、删、改、查
双链表的增、删、改、查操作及其Java实现,并通过实例演示了双向链表的优势和应用。
21 0
数据结构与算法学习五:双链表的增、删、改、查
【数据结构】——双向链表详细理解和实现
【数据结构】——双向链表详细理解和实现