练就基本功之双向带头循环链表【数据结构】【C语言实现】

0.前言

hello 大家好啊,好久不见。最近几天偷懒了,去补学校的课了,跟不上了/(ㄒoㄒ)/~~

今天复习的是双向带头循环链表。

👉戳我了解上一篇单链表

👉戳我了解上上篇顺序表

👉戳我了解复杂度

🐱🐱🐱

话不多说进入正题。

420f4875220b451ab61fa46c79fe122c

1.双向带头循环链表🐺

image-20220327174259485

**乍一看,双向带头循环链表怎么这么复杂啊?**实现起来一定巨麻烦吧?非也非也。

其结构虽然复杂,但是操作反而简单,这也正是其结构优势。

头结点(也叫哨兵位节点)是多开辟的节点,方便链表的一系列操作。

**注意:**有些书上也许会说让头结点存储链表的节点个数,看起来完美的利用了头结点是不是?

非也非也。如果链表元素类型是char呢?链表超过一定长度之后,头结点存储的数据就不准确了。书上这么写是偏理论的,没考虑到实际工程需要。

如果数据类型是double呢?如果是指针呢?
那显然是不合适的。

链表定义

typedef struct ListNode
{
	struct ListNode* next;//指向后面节点
	struct ListNode* prev;//指向前面节点
	LTDataType data;//存储数据
}ListNode;

话不多说,接下来看链表的代码实现吧:

👇👇👇

2.代码实现🐕

2.1初始化1

双向带头循环链表有2种初始化化方式,主要区别在于是否传参以及是否有返回值。

image-20220327175437627

这种方式的初始化,初始化之后需要返回创建的头结点,但也不需要传参了。

创建的头结点如图所示。

注意:OJ的链表一般都是这样的,最后传一个头结点出去。

ListNode *ListInit()
{
    ListNode *pHead = CreatListNode(0); //任意一个值都行
    //指向自己,方便插入第一个节点
    pHead->next = pHead;
    pHead->prev = pHead;
    return pHead;
}

2.2初始化2

要修改哨兵位的值,注意传二级指针或者传引用

void ListInit2(ListNode *&pHead)
{
    //要修改哨兵位头结点,因此传二级指针或者传引用
    assert(pHead);
    pHead = CreatListNode(0);
    pHead->next = pHead;
    pHead->prev = pHead;
}

**注意:**初始化方式不同,测试代码也要相应改变。

ListNode *pList;
ListInit2(pList);
ListNode *pList = ListInit();

2.3创建节点

由于链表的增删查改涉及到创建节点的问题,因此,为了简化代码,我们把创建节点的函数单独写出来,要用到的时候就去调用。

ListNode *CreatListNode(LTDataType x)
{
    ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
    assert(newNode); //暴力判空
    // if (node == NULL) //温柔判空
    // {
    //     printf("CreatListNode Fail\n");
    //     exit(-1);
    // }
    newNode->data = x;
    newNode->next = NULL;
    newNode->prev = NULL;
    return newNode;
}

2.4打印

为了更加直观得查看数据,我们写一个打印函数。

链表为空时,只有头结点,pHead->next就是自己,自己==自己,不进循环,就只打印头即可。

void ListPrint(ListNode *pHead)
{
    assert(pHead);
    ListNode *cur = pHead->next;
    printf("Head ");
    //cur走到pHead时就结束
    while (cur != pHead)
    {
        printf("<-> %d ", cur->data);
        cur = cur->next;
    }
    printf("<-> Head\n");
}

效果展示:

image-20220327180508872

2.5尾插

万事俱备,那我们就先实现一个最简单的尾插把。

双向带头循环链表的尾插是非常高效的,时间复杂度O(1)。

实现代码时,要先画图,想清楚极端情况,再去写代码。

考虑链表为空,也就是只有一个头结点的情况,也是适用的。

image-20220324185455648

要改变哨兵位这个结构体,传的是结构体的地址(指针)。

无需改变实参pList,因此无需传二级指针或传引用。

这是哨兵位头结点的功劳,如果单链表也带上哨兵位头结点,那么也可以用一级指针解决。

void ListPushBack(ListNode *pHead, LTDataType x)
{
    assert(pHead);
    ListNode *tail = pHead->prev;
    ListNode *newNode = CreatListNode(x);
    tail->next = newNode;
    newNode->prev = tail;
    newNode->next = pHead;
    pHead->prev = newN ode;
}

复用插入函数的写法。

这样复用了我们后面写的插入函数,如果不懂且往后看。👇👇👇

void ListPushBack(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead, x);
    //ListInsert是在pos前面的位置插入
}
//尾插其实相当于在pHead前面插

2.6头插

既然有尾插,那再来一个头插嘛。有头有尾,善始善终。(* ^ _ ^ *)

void ListPushFront(ListNode *pHead, LTDataType x)
{
    assert(pHead);
    //提前保存pHead的下一个节点比较方便
    ListNode *first = pHead->next;
    ListNode *newNode = CreatListNode(x);
    // pHead newNode first
    // 让pHead的next指向newNode,newNode的prev指向pHead
    // newNode的next指向first,first的prev指向newNode
    pHead->next = newNode;
    newNode->prev = pHead;
    newNode->next = first;
    first->prev = newNode;
}

复用插入函数的写法。

void ListPushFront(ListNode* pHead, LTDataType x)
{
    assert(pHead);
	LIstInsert(pHead->next, x);
}
//头插相当于在phead->next的前面去插入

2.7头删

void ListPopFront(ListNode *pHead)
{
    assert(pHead);
    assert(pHead != pHead->next); //只有一个头节点没法删
    //提前保存好要删除的节点
    ListNode *toDelete = pHead->next;
    ListNode *first = toDelete->next;
    // pHead toDelete first
    free(toDelete);
    toDelete = NULL;
    //删到只剩头节点时,满足自己指向自己
    pHead->next = first;
    first->prev = pHead;
}

复用删除函数的写法。

void ListPopFront(ListNode* pHead)
{
    assert(pHead);
    assert(pHead->next != pHead);//只剩自己时不能删除
	ListErase(pHead->next);
}

2.8尾删

image-20220324194807688

先找到尾节点和倒数第二个节点,然后释放尾节点,再让头结点和倒数第二个节点链接。

考虑删得只剩一个节点的情况(不包括头结点),删完恰好符合头结点的情况,自己指向自己。

image-20220324194943496

void ListPopBack(ListNode* pHead)
{
    assert(pHead);
    assert(pHead != pHead->next);//只有一个哨兵位时无法删除
    //找尾结点和倒数第二个结点
    ListNode* tail = pHead->prev;
    ListNode* tailPrev = tail->prev;
    free(tail);
    tail = NULL;
    tailPrev->next = pHead;
    pHead->prev = tailPrev;
}

复用删除函数的写法。

void ListPopBack(ListNode* pHead)
{
    assert(pHead);
    assert(pHead->next != pHead);//只剩一个哨兵位时不能删除
    ListErase(pHead->prev);
}

2.9查找

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

2.10插入🤭

接下来实现的时链表操作的最重要的函数,任意位置插入数据的插入函数。

注意:这里实现的插入为了向标准看齐,是在pos前面进行插入的。

注意链接顺序。

image-20220327181840685

如果pos是尾,或者pos是头,也是照样可以的,这里深深体现了双向带头循环的优势。

pos无论是头结点还是尾结点,它的前后均不会为空。

//在pos之前插入
void LIstInsert(ListNode *pos, LTDataType x)
{
    assert(pos);
    ListNode *newNode = CreatListNode(x);
    // prev newNode pos
    // 先让pos的prev的next指向newNode
    // 再让newNode的prev指向pos的prev
    pos->prev->next = newNode;
    newNode->prev = pos->prev;
    newNode->next = pos;
    pos->prev = newNode;
}

第二种写法,提前保存好pos的前一个位置。

个人比较推荐这种写法。

//在pos之前插入
void LIstInsert2(ListNode *pos, LTDataType x)
{
    assert(pos);
    //提前保存pos的前一个位置
    ListNode* posPrev = pos->prev;
    ListNode *newNode = CreatListNode(x);
    // posPrev newNode pos
    posPrev->next = newNode;
    newNode->prev = posPrev;
    newNode->next = pos;
    pos->prev = newNode;
}

有了插入函数,我们头插尾插就可以复用插入函数实现了,大大简化代码。

关于头插尾插的复用请看上面👆👆👆

2.11删除👻

删除当前节点。

image-20220327182356432

pos即使头或者尾也没关系,双向带头循环的优势体现。

void ListErase2(ListNode *pos)
{
    assert(pos);
    // posPrev pos next
    ListNode *posPrev = pos->prev, *next = pos->next;
    free(pos);
    // pos = NULL; // 其实不起作用,需要在外面再手动置空
    //  让pos的prev指向next,next的prev指向pos的prev
    posPrev->next = next;
    next->prev = posPrev;
}

有了删除函数,我们头删尾删就可以复用删除函数实现了,大大简化代码。

关于头删尾删的复用请看上面👆👆👆

关于pos置空的问题

ListErase2 中的 pos置空没起作用。

image-20220324230121333

按理说pos应该置空,但这里置空了也没用,因为这里传的不是二级指针,无法修改实参,形参只是实参的一份临时拷贝。

因此可以传二级指针或者传引用或者在调用函数之后手动置空。

但如果传二级指针接口又不一致了,显得很怪异。
void ListErase(ListNode** ppos);

传引用的写法:

void ListErase(ListNode *&pos)
{
    assert(pos);
    //提前记录pos的prev
    ListNode *posPrev = pos->prev;
    ListNode *next = pos->next;
    // posPrev pos next
    //  让pos的prev指向next,next的prev指向pos的prev
    posPrev->next = next;
    next->prev = posPrev;
    free(pos);
    pos = NULL;//传引用,可以直接修改实参的值。
}

通过传引用成功把pos置空了。

image-20220327194433448

**建议:**为了保持接口的一致性,我们最好不要传二级指针或者传引用。调用函数后手动置空,标准也是函数调用后手动置空的,我们要向标准看齐。

关于删除哨兵位

注意,不能删除哨兵位节点,不然会产生访问野指针问题。

image-20220327194501450

ListErase(pList);
ListPrint(pList);

2.12判空

bool ListIsEmpty(ListNode *pHead)
{
    assert(pHead);
    return pHead->next == pHead ? true : false;
}

2.13计算大小

int ListSize(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	int size = 0;
	while (cur != pHead)
	{
		++size;
		cur = cur->next;
	}
	return size;
}

2.14销毁

// 推荐写法,标准也是函数调用后手动置空的。
void ListDestroy2(ListNode *pHead)
{
    assert(pHead);
    ListNode *cur = pHead;
    while (cur != pHead)
    {
        ListNode *next = cur->next;
        free(cur);
        cur = next;
    }
    free(pHead);//链表的摧毁是要连哨兵位都要摧毁的。
    // pHead = NULL; // 不起作用
}

关于free的问题

free一个指针,是把指针指向的那块空间释放,所谓释放,其实就是还给操作系统,但free的那个指针还是指向那块空间的,因此需要把指针置空,不然就会有野指针的问题。

某些编译器是会把(如VS)是会把那块空间置成随机值的,但标准并没有要求是否要置成随机值。

也因指针要置空的问题,修改的是指针本身,而不是指针指向的空间,因此往往需要传二级指针或者传引用。

但标准里面通常也都是设计成free之后,再手动置空。

3.源代码😀

DoubleLinkList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int LTDataType; // ListDateType
typedef struct ListNode
{
	struct ListNode *next;
	struct ListNode *prev;
	LTDataType data;
} ListNode;

ListNode *ListInit();
void ListInit2(ListNode *&pHead); //第二种初始化方式
void ListPrint(ListNode *pHead);
void ListPushBack(ListNode *pHead, LTDataType x);
void ListPushFront(ListNode *pHead, LTDataType x);
void ListPopBack(ListNode *pHead);
void ListPopFront(ListNode *pHead);
ListNode *ListFind(ListNode *pHead, LTDataType x);
void LIstInsert(ListNode *pos, LTDataType x);
void LIstInsert2(ListNode *pos, LTDataType x);
void ListErase(ListNode *&pos);
void ListErase2(ListNode *pos);
bool ListIsEmpty(ListNode *pHead);
int ListSize(ListNode *pHead);
void ListDestroy2(ListNode *pHead);
void ListDestroy(ListNode *&pHead);

DoubleLinkList.cpp

#include "DoubleLinkList.h"
ListNode *CreatListNode(LTDataType x)
{
    ListNode *newNode = (ListNode *)malloc(sizeof(ListNode));
    assert(newNode); //暴力判空
    // if (node == NULL) //温柔判空
    // {
    //     printf("CreatListNode Fail\n");
    //     exit(-1);
    // }
    newNode->data = x;
    newNode->next = NULL;
    newNode->prev = NULL;
    return newNode;
}

// 2种初始化方式
//要修改哨兵位头结点,因此传二级指针或者传引用
void ListInit(ListNode *&pHead)
{
    assert(pHead);
    pHead = CreatListNode(0);
    //要指向自己
    pHead->next = pHead;
    pHead->prev = pHead;
}

ListNode *ListInit()
{
    ListNode *pHead = CreatListNode(0); //任意一个值都行
    //指向自己,方便插入第一个节点
    pHead->next = pHead;
    pHead->prev = pHead;
    return pHead;
}

void ListPushBack(ListNode *pHead, LTDataType x)
{
    assert(pHead);
    ListNode *tail = pHead->prev;
    ListNode *newNode = CreatListNode(x);
    tail->next = newNode;
    newNode->prev = tail;
    newNode->next = pHead;
    pHead->prev = newNode;
}

void ListPrint(ListNode *pHead)
{
    assert(pHead);
    ListNode *cur = pHead->next;
    printf("Head ");
    // cur走到pHead时就结束
    while (cur != pHead)
    {
        printf("<-> %d ", cur->data);
        cur = cur->next;
    }
    printf("<-> Head\n");
}

void ListPopBack(ListNode *pHead)
{
    assert(pHead);
    assert(pHead != pHead->next); //只有一个头结点时无法删除
    //找尾结点和倒数第二个结点
    ListNode *tail = pHead->prev;
    ListNode *tailPrev = tail->prev;
    free(tail);
    tail = NULL;
    tailPrev->next = pHead;
    pHead->prev = tailPrev;
}

ListNode *ListFind(ListNode *pHead, LTDataType x)
{
    assert(pHead);
    ListNode *cur = pHead->next;
    while (cur != pHead)
    {
        if (cur->data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

//在pos之前插入
void LIstInsert(ListNode *pos, LTDataType x)
{
    assert(pos);
    ListNode *newNode = CreatListNode(x);
    // prev newNode pos
    // 先让pos的prev的next指向newNode
    // 再让newNode的prev指向pos的prev
    pos->prev->next = newNode;
    newNode->prev = pos->prev;
    newNode->next = pos;
    pos->prev = newNode;
}

//在pos之前插入
void LIstInsert2(ListNode *pos, LTDataType x)
{
    assert(pos);
    //提前保存pos的前一个位置
    ListNode *posPrev = pos->prev;
    ListNode *newNode = CreatListNode(x);
    // posPrev newNode pos
    posPrev->next = newNode;
    newNode->prev = posPrev;
    newNode->next = pos;
    pos->prev = newNode;
}

void ListPushFront(ListNode *pHead, LTDataType x)
{
    assert(pHead);
    //提前保存pHead的下一个节点比较方便
    ListNode *first = pHead->next;
    ListNode *newNode = CreatListNode(x);
    // pHead newNode first
    // 让pHead的next指向newNode,newNode的prev指向pHead
    // newNode的next指向first,first的prev指向newNode
    pHead->next = newNode;
    newNode->prev = pHead;
    newNode->next = first;
    first->prev = newNode;
}

void ListPopFront(ListNode *pHead)
{
    assert(pHead);
    assert(pHead != pHead->next); //只有一个头节点没法删
    //提前保存好要删除的节点
    ListNode *toDelete = pHead->next;
    ListNode *first = toDelete->next;
    // pHead toDelete first
    free(toDelete);
    toDelete = NULL;
    //删到只剩头节点时,满足自己指向自己
    pHead->next = first;
    first->prev = pHead;
}

void ListErase2(ListNode *pos)
{
    assert(pos);
    // posPrev pos next
    ListNode *posPrev = pos->prev, *next = pos->next;
    free(pos);
    // pos = NULL; // 其实不起作用,需要在外面再手动置空
    //  让pos的prev指向next,next的prev指向pos的prev
    posPrev->next = next;
    next->prev = posPrev;
}

// void ListErase2(ListNode *pos)
// {
//     assert(pos);
//     //提前记录pos的prev
//     ListNode *posPrev = pos->prev;
//     ListNode *next = pos->next;
//     // posPrev pos next
//     //  让pos的prev指向next,next的prev指向pos的prev
//     posPrev->next = next;
//     next->prev = posPrev;
//     free(pos);
//     pos = NULL;
// }
void ListErase(ListNode *&pos)
{
    assert(pos);
    //提前记录pos的prev
    ListNode *posPrev = pos->prev;
    ListNode *next = pos->next;
    // posPrev pos next
    //  让pos的prev指向next,next的prev指向pos的prev
    posPrev->next = next;
    next->prev = posPrev;
    free(pos);
    pos = NULL;
}

int ListSize(ListNode *pHead)
{
    assert(pHead);
    ListNode *cur = pHead->next;
    int size = 0;
    while (cur != pHead)
    {
        ++size;
        cur = cur->next;
    }
    return size;
}

bool ListIsEmpty(ListNode *pHead)
{
    assert(pHead);
    return pHead->next == pHead ? true : false;
}

void ListDestroy2(ListNode *pHead)
{
    assert(pHead);
    ListNode *cur = pHead;
    while (cur != pHead)
    {
        ListNode *next = cur->next;
        free(cur);
        cur = next;
    }
    free(pHead); //链表的摧毁是要连哨兵位都要摧毁的。
    // pHead = NULL; // 不起作用
}

//复用Erase,且传引用
// void ListDestroy(ListNode *&pHead)
// {
//     assert(pHead);
//     ListNode *cur = pHead;
//     while (cur != pHead)
//     {
//         ListNode *next = cur->next;
//         ListErase(cur);
//         cur = next;
//     }
//     free(pHead);
//     pHead = NULL;
// }

void ListDestroy(ListNode *&pHead)
{
    assert(pHead);
    ListNode *cur = pHead;
    while (cur != pHead)
    {
        ListNode *next = cur->next;
        free(cur);
        cur = next;
    }
    free(pHead);
    pHead = NULL;
}

test.cpp

#include "DoubleLinkList.h"
void Test1()
{
    ListNode *pList = ListInit();
    ListPushBack(pList, 1);
    ListPushBack(pList, 2);
    ListPushBack(pList, 3);
    ListPushBack(pList, 4);
    ListPrint(pList);
}
void Test2()
{
    ListNode *pList;
    ListInit2(pList);
    ListPushBack(pList, 1);
    ListPushBack(pList, 2);
    ListPushBack(pList, 2);
    ListPushBack(pList, 4);
    ListPrint(pList);

    ListPopBack(pList);
    ListPrint(pList);
    ListPopBack(pList);
    ListPrint(pList);
    ListPopBack(pList);
    ListPrint(pList);
    ListPopBack(pList);
    ListPrint(pList);
}

void Test3()
{
    ListNode *pList = ListInit();
    ListPushBack(pList, 1);
    ListPushBack(pList, 2);
    ListPushBack(pList, 3);
    ListPushBack(pList, 4);
    ListPrint(pList);
    ListNode *pos = ListFind(pList, 3);
    if (pos)
    {
        LIstInsert(pos, 20);
    }
    ListPrint(pList);

    ListNode *pos2 = ListFind(pList, 4);
    if (pos2)
    {
        LIstInsert2(pos2, 40);
    }
    ListPrint(pList);
}

void Test4()
{
    ListNode *pList = ListInit();
    ListPushBack(pList, 1);
    ListPushBack(pList, 2);
    ListPushBack(pList, 3);
    ListPushBack(pList, 4);
    ListPrint(pList);
    ListPushFront(pList, 4);
    ListPushFront(pList, 3);
    ListPushFront(pList, 2);
    ListPushFront(pList, 1);
    ListPrint(pList);
    ListPopFront(pList);
    ListPopFront(pList);
    ListPrint(pList);
}
void Test5()
{
    ListNode *pList = ListInit();
    ListPushBack(pList, 1);
    ListPushBack(pList, 2);
    ListPushBack(pList, 3);
    ListPushBack(pList, 4);
    ListPrint(pList);
    ListNode *pos = ListFind(pList, 1);
    if (pos)
    {
        ListErase2(pos);
    }
    ListPrint(pList);
    printf("%d\n", ListSize(pList));
    printf("%d\n", ListIsEmpty(pList));
    ListDestroy(pList);
}
int main(int argc, char const *argv[])
{
    Test5();
    system("pause");
    return 0;
}

4.总结

以一言蔽之,写代码之前,一定要多动手画图,考虑清楚极端情况再去写代码,不要想到一点就直接上手写,尽可能地减少调试的次数。

画图我强烈推荐Windows自带的画图工具,超级好用!!!

5.尾声

🌹🌹🌹

写文不易,如果有帮助烦请点个赞~ 👍👍👍

Thanks♪(・ω・)ノ🌹🌹🌹

😘😘😘

👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接

附联系方式(2076188013)(QQ)

  • 22
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值