【初阶数据结构】——带头双向循环链表(C描述)

举报
YIN_尹 发表于 2023/08/12 09:09:30 2023/08/12
【摘要】 前言上一篇文章我们学习了单链表,同时我们提到了链表其实有很多种结构:带头或不带头,循环或不循环。但其实,最常用的还是两种结构:上一篇文章我们已经学了单链表(不带头),那这篇文章,我们就来学习一下带头双向循环链表。带头双向循环链表实现1. 结构介绍首先,从结构上来说,带头双向循环链表是结构最复杂的:它带哨兵位的头结点,还是双向的,还循环。带头双向循环链表一般用来单独存储数据。实际中使用的链表数...

前言

上一篇文章我们学习了单链表,同时我们提到了链表其实有很多种结构:带头或不带头,循环或不循环。

但其实,最常用的还是两种结构:

7889cf25058a4475acc130695d8158c9.png

上一篇文章我们已经学了单链表(不带头),那这篇文章,我们就来学习一下带头双向循环链表

带头双向循环链表实现

1. 结构介绍

首先,从结构上来说,带头双向循环链表是结构最复杂的:

e42bc81c814b41e9a5449a2f4897ae34.png

它带哨兵位的头结点,还是双向的,还循环。

带头双向循环链表一般用来单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。

对于带头双向循环链表来说:

首先它是带哨兵位的头结点的,也就是说,它是空表状态的时候,也是有一个头结点存在的(当然它不存储有效数据)。

对于它的每个结点来说,首先它要能存储一个数据,然后呢?它需要有两个指针,一个存它前驱的地址,另一个存它后继的地址。

typedef int DLDataType;
typedef struct DoubleListNode
{
    struct DoubleListNode* prev;
    DLDataType data;
    struct DoubleListNode* next;
}DLNode;

它的结构虽然复杂,但是呢,使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

接下来我们就来实现一下对应的接口函数。

2. 结点创建

带头双向循环链表的每个结点:一个数据域,两个指针域。

//创建结点
DLNode* CreateNewnode(DLDataType x)
{
    DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
    if (newnode == NULL)
    {
        perror("malloc fail\n");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    newnode->prev = NULL;
    return newnode;
}

相信经过单链表的学习,这个函数就不用给大家过多解释了。

3. 初始化

大家如果看了上一篇单链表的文章会发现我们在实现单链表的时候其实没有搞初始化单链表的函数。

为什么没有搞呢?

因为不需要,我们实现的是单链表,而且不带头。

对于这样一个单链表只要有一个头指针就行了,空表怎么表示,是不是头指针指向空就行了啊,创建链表的时候是不是直接在头指针后面链接结点就行了。

根本不用写初始化的函数,定义一个头指针就完事了。


那为什么我们今天实现的带头双向循环链表还要搞一个初始化的函数呢?

因为它是带头的,所以我们应该先初始化一下,先搞一个头结点出来,就算是空的状态也应该有一个头存在的,后面要插入新结点是不是应该基于头结点进行操作啊。

那初始化函数应该怎么写?

搞一个头结点就完事了吗?首先哨兵位的头结点不存储有效数据,它的数据域我们可以随便给个值。

那指针域呢?

我们新创建的结点指针域默认赋值为NULL,是指向空的。那头结点的指针域我们需要改动吗?

我们来思考一下:

头结点的两个指针域(prev,next)

4c6b3c6df3e0426180c5f22560b20f53.png

它的prev应该是指向尾结点的,那现在还没有有效结点,只有哨兵位自己,那它自己就是尾,那就让prev指向它自己。

那next呢?

如果指向空的话,是不是好像没有循环起来啊,到空就断了。而且现在只有一个哨兵位的头结点,它自己就是尾,尾的next应该指向头,而现在头也是它自己,所以我们初始化的时候让next也指向自己。

我们初始化的时候让头结点的两个指针域都指向自己,首先这样更符合循环,其次这样做在后面的操作中会带来很大的优势,我们到后面就能体会到。

所以,初始化的函数应该是这样的:

//初始化
DLNode* DLInit()
{
    DLNode* guard = CreateNewnode(-1);
    //创建哨兵位的头结点(不存储有效数据)
    guard->next = guard;
    guard->prev = guard;
    return guard;
}

另外这里再给大家提一点:


我们看到初始化函数我们搞了一个返回值,为什么这样做呢?

因为我们初始化之后,有一个哨兵位的头结点在这里,我们需要有一个头指针来指向这个头结点,以便我们来访问链表。

所以我们初始化的函数需要有一个返回值,返回哨兵位结点的地址,让我们自己的指针指向它,这样就能访问这个链表。

当然,除了返回值的方法,也可以用二级指针(传头指针的地址), 这里就不实现了。

但是传一级肯定是不行的,因为形参的改变并不会影响实参。

4. 销毁

和单链表一样,每个结点的空间是我们使用malloc动态开辟的,所以是需要我们手动去释放的。

很简单,还是对链表进行遍历,一一释放每个结点。

但是,对于带头双向循环链表,遍历结束的条件是什么呢?

这一点就和单链表不一样了,我们遍历单链表的时候,定义一个cur,走到空结束,因为单链表尾结点的指针域存的是NULL。

但是,对于循环链表来说,每个结点的指针域都没有空,那怎么判断遍历结束呢?

我们要知道循环链表是怎么循环起来的,是尾结点的next指针存了头结点的地址,头结点的prev指针存了尾结点的地址。

所以:

我们定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束,然后再把头结点释放一下:free(phead);,就销毁完了。

f2e17747f2b04fdeaadd782cc0cba7c1.png

正常情况下,phead不可能为空(即使是空表,phead指向头结点,也不为空),所以我们进行一个断言:

assert(phead);

//销毁
void DLDestory(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        DLNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
    phead = NULL;
}

对空表的时候也适用,空的时候只有哨兵位的头结点,phead指向头结点,cur = phead->next,而我们初始化的时候头结点的next就指向自己。这样cur==phead,直接就不会进while循环,只销毁一下头结点,就完事了。

如果我们初始化的时候没有让头结点的next指向自己,这样cur = phead->next之后cur 就是NULL了,就进去while循环了,反而会出现问题了。

另外:

这里我们释放完哨兵头之后虽然phead = NULL;,把头指针置空了,但其实并不会影响外面真正的头指针,还是形参与实参的关系。

所以其实函数内部这句phead = NULL;加不加都无所谓,因为函数调用结束这个指针变量也就销毁了,当然加上是一个好习惯。

但是需要我们在函数外部手动将真正的头指针置一下空,不置的话就是一个空指针了。

5. 头插

带头双向循环链表的头插也非常简单,给大家看图比较直观一下:

4381b10e5c584d45b61d249bb13f8208.png

void DLPushFront(DLNode* phead, DLDataType x)
{
    assert(phead);
    //DLNode* newnode = CreateNewnode(x);

    //如果不保存第一个结点的地址,改变指针指向需要注意顺序
    /*newnode->next = phead->next;
    phead->next->prev = newnode;

    phead->next = newnode;
    newnode->prev = phead;*/

    //顺序无关
    DLNode* first = phead->next;//保存第一个结点的地址
    //phead newnode first
    phead->next = newnode;
    newnode->prev = phead;
    newnode->next = first;
    first->prev = newnode;
}

6. 头删

d4e0b553ffbd49e9895e31532f6d62a2.png

头删的时候要注意进行一个判断:


如果链表为空了,就不能再删了,那怎么判断带头双向循环链表为空呢?

如果哨兵位的头结点的prev和next指针域都指向自己,是不是就是空表啊。

所以,我们可以加一个断言:

assert(phead->next != phead);

然后,改变对应的指针指向,释放结点就行了。

//头删
void DLPopFront(DLNode* phead)
{
    assert(phead);
    assert(phead->next != phead);

    DLNode* first = phead->next;
    DLNode* second = first->next;
    //phead first second
    phead->next = second;
    second->prev = phead;
    free(first);
    first = NULL;
}

7. 尾插

我们先来回忆一下,我们上一篇文章实现的单链表,它的尾插尾删其实是很麻烦的:


单链表尾插还要进行一个判断,因为我们实现的是没有头结点的,如果是对空表尾插,直接将要插入的结点赋值给头指针即可。

但对于不是空表的尾插,还要要先遍历找尾,然后让尾结点的指针域存新结点的地址,使其成为新的尾。


但是带头双向循环链表的尾插需要这么麻烦吗?


不需要的,带头双向循环链表的尾插尾删实现起来就爽多了。

首先它是带头的,空表头插我们也不需要像单链表那样单独处理,其次,单链表尾插我们还需要遍历找尾,但是对于循环链表来说,找尾简单吗?

是不是so easy啊。头结点的prev指向的不就是尾嘛,找尾一句代码就搞定了:

DLNode* tail = phead->prev;

那接下来插入就很简单了,还是对指针的改变:

9e98fa39675640a884574b00c10c9bcc.png

上代码:

//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
    assert(phead);
    DLNode* newnode = CreateNewnode(x);

    DLNode* tail = phead->prev;
    tail->next = newnode;
    newnode->prev = tail;

    newnode->next = phead;
    phead->prev = newnode;
}

8. 尾删

尾删呢也很简单:

880824370c11422b94f8d8a1560c8e46.png

//尾删
void DLPopBack(DLNode* phead)
{
    assert(phead);
    assert(phead->next != phead);//如果为空,就不能再删了
    DLNode* tail = phead->prev;
    DLNode* newtail = tail->prev;

    phead->prev = newtail;
    newtail->next = phead;
    free(tail);
    tail = NULL;
}

我们单链表尾删的时候对于只剩一个结点的情况还需要单独判断,但是对于带头双向循环链表只有一个有效结点时头删也不需要单独判断,我们直接删就行了,删完就变成初始化的状态了。

eff3a6ad298e4929bdbc9f769d533240.png

9. 打印

打印呢也很好搞:


对链表进行遍历,打印每个结点中的数据就行了。

而遍历结束的条件,我们在实现销毁的时候是不是就讨论过了呀,定义一个指针(cur ),从哨兵位后面的第一个有效结点开始向后走(cur =cur->next),直到cur == phead时循环结束就行了。

63ee68974ae442759ed767f1ed177628.png

//打印
void DLPrint(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        printf("%d ", cur->data);
        cur = cur->next;
    }
    printf("\n");
}

10. 查找

查找是不是还是对链表进行遍历啊。

假设我们要查找的元素是X,那就遍历链表,将每个结点的值与X进行对比,相同的时候就是找到了,我们可以返回该结点的地址,如果找不到,我们就返回一个NULL。

d239417824ca40c789e6d1dacfc318ce.png

//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

11. 在pos之前插入数据

我们直接看图:

25e6ccc56caa4b87b980ce0e2b14a2ae.png

那实现起来也很简单,创建一个新结点,它的数据域赋值为我们要插入的数据,然后链接起来就行了.

那有没有什么需要注意的呢?

pos是不是得是个有效的位置啊,所以我们要加一个断言:

assert(pos);

我们这个函数是和查找函数结合使用的,find函数给我们返回一个地址,我们把它传给当前函数,在该位置前面插入.

如果pos为空,说明在链表中都找不到,那还往哪插呢?

//在pos之前插入(头插尾插可以复用)
void DLInsert(DLNode* pos, DLDataType x)
{
    assert(pos);
    DLNode* pos_prev = pos->prev;
    DLNode* newnode = CreateNewnode(x);
    //pos_prev newnode pos
    pos_prev->next = newnode;
    newnode->prev = pos_prev;
    newnode->next = pos;
    pos->prev = newnode;
}

这个函数实现好之后,我们会发现:

我们前面实现的头插尾插是不是可以复用这个函数啊,因为DLInsert函数是在pos位置之前插入,这个pos可以是链表中任意一个有效位置啊,那当然可以在头尾进行插入了.

那头插尾插就可以这样简化了.

头插:

DLInsert(phead->next, x);

尾插:

DLInsert(phead, x);

尾插为什么传的是phead呢?

phead是指向头结点的指针,而在循环链表中,头结点的前面位置不就是尾嘛.

12. 删除pos位置

直接来看图:

e3b7e44523c746b89cb1ff87a79ca240.png

这里的pos位置有没有什么限制啊?

它不能是哨兵位的头结点,既然是带头的链表,那头结点我们肯定不能删.

不过如果我们是把find的返回值传给pos , pos也不会是头结点,因为我们遍历都是从头结点后面开始的.

//删除pos位置(头插头删可以复用)
void DLErase(DLNode* pos)
{
    assert(pos);
    DLNode* pos_prev = pos->prev;
    DLNode* pos_next = pos->next;
    //pos->prev pos pos->next
    pos_prev->next = pos_next;
    pos_next->prev = pos_prev;
    free(pos);
    pos = NULL;
}

那这个函数写好,头删尾删是不是也可以复用:

头删:

DLErase(phead->next);

尾删:

DLErase(phead->prev);

所以我们以后再写的时候可以先写这两个函数,然后头部尾部的插入删除就可以直接复用了.

13. 判空

判空的话呢,也很简单:

如果头结点的prev或next指针域指向的是自己,是不是就代表此时是空的状态啊。

6ebc14cc110849b2bd7edcdc4a1f442f.png

//判空
bool DLEmpty(DLNode* phead)
{
    assert(phead);
    return phead->next == phead;
}

当然,我们这里函数的返回值用的bool类型,需要包含一个头文件:

#include <stdbool.h>

14. 计算大小

计算大小,那就遍历一下链表,计算一下元素个数就行了.

//计算大小
int DLSize(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    int size = 0;
    while (cur != phead)
    {
        size++;
        cur = cur->next;
    }
    return size;
}

源码展示

1. DoubleList.h

(头文件的包含、结构定义和函数声明)

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int DLDataType;
typedef struct DoubleListNode
{
    struct DoubleListNode* prev;
    DLDataType data;
    struct DoubleListNode* next;
}DLNode;

//创建结点
DLNode* CreateNewnode(DLDataType x);
//初始化
DLNode* DLInit();
//销毁
void DLDestory(DLNode* phead);
//打印
void DLPrint(DLNode* phead);
//头插
void DLPushFront(DLNode* phead, DLDataType x);
//头删
void DLPopFront(DLNode* phead);
//尾插
void DLPushBack(DLNode* phead, DLDataType x);
//尾删
void DLPopBack(DLNode* phead);
//查找
DLNode* DLFind(DLNode* phead, DLDataType x);
//在pos之前插入
void DLInsert(DLNode* pos, DLDataType x);
//删除pos位置
void DLErase(DLNode* pos);
//判空
bool DLEmpty(DLNode* phead);
//计算大小
int DLSize(DLNode* phead);

2. DoubleList.c

(函数具体实现)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"

//创建结点
DLNode* CreateNewnode(DLDataType x)
{
    DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
    if (newnode == NULL)
    {
        perror("malloc fail\n");
        exit(-1);
    }
    newnode->data = x;
    newnode->next = NULL;
    newnode->prev = NULL;
    return newnode;
}
//初始化
DLNode* DLInit()
{
    DLNode* guard = CreateNewnode(-1);//创建哨兵位的头结点(不存储有效数据)
    guard->next = guard;
    guard->prev = guard;
    return guard;
}
//销毁
void DLDestory(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        DLNode* next = cur->next;
        free(cur);
        cur = next;
    }
    free(phead);
    phead = NULL;
}
//打印
void DLPrint(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        printf("%d ", cur->data);
        cur = cur->next;
    }
    printf("\n");
}
//头插
void DLPushFront(DLNode* phead, DLDataType x)
{
    assert(phead);
    //DLNode* newnode = CreateNewnode(x);

    //如果不保存第一个结点的地址,改变指针指向需要注意顺序
    /*newnode->next = phead->next;
    phead->next->prev = newnode;

    phead->next = newnode;
    newnode->prev = phead;*/

    顺序无关
    //DLNode* first = phead->next;//保存第一个结点的地址
    phead newnode first
    //phead->next = newnode;
    //newnode->prev = phead;
    //newnode->next = first;
    //first->prev = newnode;

    DLInsert(phead->next, x);//复用DLInsert
}
//头删
void DLPopFront(DLNode* phead)
{
    assert(phead);
    assert(phead->next != phead);

    //DLNode* first = phead->next;
    //DLNode* second = first->next;
    phead first second
    //phead->next = second;
    //second->prev = phead;
    //free(first);
    //first = NULL;

    DLErase(phead->next);//复用DLErase
}
//尾插
void DLPushBack(DLNode* phead, DLDataType x)
{
    assert(phead);
    //DLNode* newnode = CreateNewnode(x);

    /*DLNode* tail = phead->prev;
    tail->next = newnode;
    newnode->prev = tail;

    newnode->next = phead;
    phead->prev = newnode;*/

    DLInsert(phead, x);//复用DLInsert
}
//尾删
void DLPopBack(DLNode* phead)
{
    assert(phead);
    assert(phead->next != phead);//如果为空,就不能再删了
    /*DLNode* tail = phead->prev;
    DLNode* newtail = tail->prev;

    phead->prev = newtail;
    newtail->next = phead;
    free(tail);
    tail = NULL;*/

    DLErase(phead->prev);//复用DLErase
}
//查找
DLNode* DLFind(DLNode* phead, DLDataType x)
{
    assert(phead);
    DLNode* cur = phead->next;
    while (cur != phead)
    {
        if (cur->data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}
//在pos之前插入(头插尾插就可以复用了)
void DLInsert(DLNode* pos, DLDataType x)
{
    assert(pos);
    DLNode* pos_prev = pos->prev;
    DLNode* newnode = CreateNewnode(x);
    //pos_prev newnode pos
    pos_prev->next = newnode;
    newnode->prev = pos_prev;
    newnode->next = pos;
    pos->prev = newnode;
}
//删除pos位置(头插头删就可以复用了)
void DLErase(DLNode* pos)
{
    assert(pos);
    DLNode* pos_prev = pos->prev;
    DLNode* pos_next = pos->next;
    //pos->prev pos pos->next
    pos_prev->next = pos_next;
    pos_next->prev = pos_prev;
    free(pos);
    pos = NULL;
}
//判空
bool DLEmpty(DLNode* phead)
{
    assert(phead);
    return phead->next == phead;
}
//计算大小
int DLSize(DLNode* phead)
{
    assert(phead);
    DLNode* cur = phead->next;
    int size = 0;
    while (cur != phead)
    {
        size++;
        cur = cur->next;
    }
    return size;
}

3. Test.c

(对函数功能的测试)

#define _CRT_SECURE_NO_WARNINGS
#include "DoubleList.h"
#include <stdio.h>

void test1()
{
    DLNode* phead = DLInit();

    DLPushFront(phead, 1);
    DLPushFront(phead, 2);
    DLPushFront(phead, 3);
    DLPushFront(phead, 4);
    DLPushFront(phead, 5);
    DLPrint(phead);
    DLPopFront(phead);
    DLPrint(phead);
    DLPopFront(phead);
    DLPrint(phead);
    DLPopFront(phead);
    DLPrint(phead);
    DLPopFront(phead);
    DLPrint(phead);
    DLPopFront(phead);
    DLPrint(phead);

    DLDestory(phead);
    phead = NULL;
}
void test2()
{
    DLNode* phead = DLInit();

    DLPushBack(phead, 1);
    DLPushBack(phead, 2);
    DLPushBack(phead, 3);
    DLPushBack(phead, 4);
    DLPushBack(phead, 5);
    DLPrint(phead);

    DLPopBack(phead);
    DLPrint(phead);
    DLPopBack(phead);
    DLPrint(phead);
    DLPopBack(phead);
    DLPrint(phead);
    DLPopBack(phead);
    DLPrint(phead);
    DLPopBack(phead);
    DLPrint(phead);

    DLDestory(phead);
    phead = NULL;
}

void test3()
{
    DLNode* phead = DLInit();

    DLPushBack(phead, 1);
    DLPushBack(phead, 2);
    DLPushBack(phead, 3);
    DLPushBack(phead, 4);
    DLPushBack(phead, 5);
    DLPrint(phead);

    DLNode* pos = DLFind(phead, 3);
    /*if (pos)
    {
        pos->data = 10;
    }*/
    if (pos)
    {
        DLInsert(pos, 99);
    }
    DLPrint(phead);
    /*if (pos)
    {
        printf("找到了\n");
    }
    else
    {
        printf("找不到\n");
    }*/

    DLDestory(phead);
    phead = NULL;
}
void test4()
{
    DLNode* phead = DLInit();

    DLPushBack(phead, 1);
    DLPushBack(phead, 2);
    DLPushBack(phead, 3);
    DLPushBack(phead, 4);
    DLPushBack(phead, 5);
    DLPrint(phead);

    DLNode* pos = DLFind(phead, 3);
    if (pos)
    {
        DLErase(pos);
    }
    DLPrint(phead);

    DLDestory(phead);
    phead = NULL;
}
void test5()
{
    DLNode* phead = DLInit();

    DLPushBack(phead, 1);
    DLPushBack(phead, 2);
    DLPushBack(phead, 3);
    DLPushBack(phead, 4);
    DLPushBack(phead, 5);
    DLPrint(phead);
    printf("元素个数:%d\n", DLSize(phead));

    DLPopBack(phead);
    DLPopBack(phead);
    DLPopBack(phead);
    DLPopBack(phead);
    DLPrint(phead);
    if (DLEmpty(phead))
        printf("空表\n");
    DLPopBack(phead);
    DLPrint(phead);
    if (DLEmpty(phead))
        printf("空表\n");

    DLDestory(phead);
    phead = NULL;
}
int main()
{
    test5();
    return 0;
}

好了,对带头双向循环链表的讲解就到这里,欢迎大家指正!!!

da0d28ea57214b269b7cd75d68be82e5.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。