目录标题

  • 一.单向链表的不足
  • 二.带头双向链表的准备
  • 三.带头双向链表的初始化
  • 四.带头双向链表的尾插
  • 五.带头双向链表的打印
  • 六.带头双向链表头插
  • 七.判断链表是否为空
  • 八.带头双向链表尾删
  • 九.带头双向链表头删
  • 十.带头双向链表的长度
  • 十一.带头双向链表的查找
  • 十二.带头双向链表任意位置的插入
  • 十三.带头双向链表任意位置的删除
  • 十四.带头双向链表的销毁

一.单向链表的不足

我们上一篇文章中就讲解了一下单链表的简单的实现,但是在实现的过程中大家似乎也能感觉到一个问题就是我们单向链表好像也没有比顺序表方便了多少,我们单链表在在实现尾插尾删的时候也得挨个的找到尾部,我们在中间插入或者删除的时候也得挨个的找到你传过来的那个位置,而且单链表的一些函数在实现的过程中有时候还要分情况来讨论是传一级指针还是传二级指针,而且我们单链表不停的使用malloc来开辟空间的话,还会导致我们整体的效率下降,所以整体来看的话我们的单链表好像也没有占到什么便宜吧,所以我们这里就得对单链表进行一下简单让其变成带头双向循环链表。

二.带头双向链表的准备

我们首先来看看这个链表的图是怎么来表示的:

我们可以看到这个链表中的相邻的元素之间是用两个指针来连接的,这就是我们这里双向的体现,然后在我们这个链表的头部有一个头节点,这个节点就是起到一个指引的作用,因为我们之前的没带头的单向链表在实现的过程中会不停的改变我们外部的头指针,所以我们这里就加一个头节点,该节点指向我们链表的第一个元素,这样我们在头插的时候就不用改变我们外部的头指针,只要我们指针指向这个头节点我们就可以通过这个头节点来找到其他的元素,我们这里的头节点也有两个节点,一个节点指向后面的第一个元素,但是我们头节点前面是没有元素的,但是这个指针又不想简单的空着,所以我们这里就让这个节点指向我们的最后一个元素,同样的道理我们的最后一个元素也有两个节点,有一个指针指向倒数第二个元素,但是我们最后一个元素的后面已经没有元素了,所以我们就让这个指针指向头节点,这样的话,我们就可以很快的通过头节点找到链表吧的尾部,通过最后一个元素找到链表的头节点,那么这里就可以体现我们这里的循环的功能,那么我们这里就介绍了一下带头双向链表的一些特点,那么我们这里显然每个元素里面有很多的变量,那么我们这里就可以创建一个结构体来储存这些变脸,那么我们这里的代码就如下:

typedef int LTDataType;
typedef struct ListNode
{struct ListNode* prev;struct ListNode* next;LTDataType x;
}LTNode;

三.带头双向链表的初始化

首先为我们在main函数里面创建一个指针变量plist,将其值初始化为空,然后我们就执行链表的初始化,那么我们这里的初始化肯定是会改变这个变量的值的所以我们这里的函数需要的参数是一个二级指针,但是我们这个链表中的其他函数是只用一级指针的,所以我们这里就对其做出一点修改,将其改成只需要一级指针,那么我们这里是怎么来做的呢?我们只用在函数的内部返回一下我们新开辟出来的地址就可以了,然后在函数外部来进行一下接收,这样我们就可以做到即传递的是一级指针但是也可以改变外部的指针变量的值的特点,那么接下来我们再来看看这个函数的内部是如何来实现的,首先我们想一下我们这里的初始化要干嘛,首先我们的这个链表要有一个头节点,所以我们这里的初始化干的事情就是先创建出来一个头节点,然后我们这个头节点中有两个指针,但是此时我们的链表中是没有其他的元素的,所以我们这里就只能将这两个指针指向自己本省,最后我们再将节点的地址作为函数的返回值,那么我们这里的函数的具体的实现就如下:

LTNode* ListInit(LTNode* phead)
{LTNode* guard = (LTNode*)malloc(sizeof(LTNode));if (guard == NULL){perror("malloc fail");exit(-1);}guard->next = guard;guard->prev = guard;return guard;
}

四.带头双向链表的尾插

因为我们这里的链表是带头的,所以我们这里不管有没有元素,我们这里都不需要改变外部头指针的值,所以我们这个函数需要的是一个一级指针,同样的道理通过之前的初始化我们知道这里的链表不管有没有其他的元素,那一定是有一个头节点的,而且我们的外部的头指针还指向着这个头节点,所以我们这里传过来的一级指针是一定不能为空的,那么我们就得对其进行一下断言,防止传过来的值为空,接下来我们就实现尾插,首先我们得创建一个节点,因为用到这个功能的地方还很多,所以我们这里就将其合并成为一个函数那么代码如下:

LTNode* BuyListNode(LTDataType num)
{LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead == NULL){perror("malloc fail");exit(-1);}phead->next = phead->prev = NULL;phead->x = num;return phead;
}

然后再通过头节点的prv指针来找到最后一个元素的地址,再将最后一个元素中的next的值改成newnode的地址,再将newnode中的prev指向之前的最后一个值,这样我们的newnode就成为了当前的最后一个元素,因为我们这里是循环的所以我们还要将头节点的prev指向newnode和newnode的next指向头节点,这样我们的尾插就完成了,那么我们的代码就如下:

void ListPushBack(LTNode* phead, LTDataType x)
{LTNode* cur = BuyListNode(x);phead->prev->next = cur;cur->prev = phead->prev;phead->prev = cur;cur->next = phead;
}

但是我们这样写的话可读性就优点低,我们可以再创建一个指针变量来专门的指向我们的最后一个元素,那么这样的话我们的代码的可读性就会大大的提高:

void ListPushBack(LTNode* phead, LTDataType x)
{LTNode* newnode = BuyListNode(x);LTNode* at_last = phead->prev;at_last->next = newnode;newnode->prev = at_last;phead->prev = newnode;newnode->next=phead;
}

那么这样的话我们的可读性就提高了许多,那么我们这里再来测试一下我们这里写的代码:

void ListPrint(LTNode* phead)
{LTNode* cur = phead->next;while (cur != phead){printf("%d->", cur->x);cur = cur->next;}printf("null");
}

我们来看看这个代码运行的结果为:

那么我们这里运行的结果跟我们预测的是一模一样的所以我们这里的函数实现是正确的。那么我们接着往下看。

五.带头双向链表的打印

我们将数据插入进去了,但是看不到那是不是就特别的尴尬啊,所以我们这里就来实现一下这个函数,首先我们得知道一件事就是我们这里的链表与之前的不带头的单向链表还是有那么点区别的就是,我们这里的尾部事没有空指针的,我们之前在不带头的单链表中实现这个函数的原理事链表的末尾指向的是一个空指针,所以我们以此作为结束的根据,但是我们这里就不能这么做,因为我们这里是循环的,那我们该怎么做呢?我们这里将打印完之后我吗的指针就又回到了起点,嗯?起点,既然我们这里没有空指针来作为结束的标志,那我们这里能不能把回到开头来作为结束的标志呢?答案是可以的,因为我们的头节点里面并没有数据,所以我们一开始就创建一个指针变量cur将其值赋值为第一个元素的地址,而不是头节点,然后我们就创建一个循环,在该循环里面打印数据,并且修改cur的值,让其指向下一个元素,当cur的值等于头节点的话我们的循环就结束了,这样的话我们的打印也就跟着结束了,那么我们的代码就如下:

void ListPrint(LTNode* phead)
{LTNode* cur = phead->next;while (cur != phead){printf("%d->", cur->x);cur = cur->next;}printf("null");
}

六.带头双向链表头插

既然我们这里有了尾插,那么我们这里也就会有头插,那我们头插就不用去管循环之类的事情,因为我们这里的头插插的是头节点的后面,并不会影响循环,所以我们这里还是想创建一个节点newnode,然后再创建一个指针变量scend将其值赋值为第二个元素的地址,那么我们这里的插入的逻辑就是将newnode的next指向scend,再将scend的prev指向newnode,那么这样我们的newnode就和当前第一个元素连接了起来,那么我们剩下要做的就是将newnode和头节点连接起来,那么这里的逻辑就是一样的,就不多说了直接看代码:

void ListPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = BuyListNode(x);LTNode* scend = phead->next;newnode->next = scend;scend->prev = newnode;phead->next = newnode;newnode->prev = phead;
}

那么我们这里再来测试一下这个函数的实现是否是正确的:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushBack(plist, 10);ListPushBack(plist, 20);ListPushBack(plist, 30);ListPushBack(plist, 40);ListPushFront(plist, 50);ListPushFront(plist, 60);ListPushFront(plist, 70);ListPrint(plist);return 0;
}

我们来看看这个代码的运行结果:

那么这里我们的函数实现就是正确的。

七.判断链表是否为空

我们来想一下我们链表为空的时候有什么样的特点,是不是就只有一个头节点啊,那么我们这里的头节点中prev指针是不是就是指向自己的啊,那么这个就可以作为我们判断链表是否为空的一个一句,如果为空的话我们就返回0,如果不为空的话我们就返回其他的值,那么我们这里就可以直接返回一个表达式:phead->next!=phead当我们相等的时候我们这个表达式的返回值就是0,那么我们的函数实现就是这样:

bool Listempty(LTNode* phead)
{return phead->next != phead;
}

八.带头双向链表尾删

实现我们这个函数首先做的第一步就是得判断一下我们的链表是否为空,当为空的时候我们就不能再进行删除了,所以这里就得用到我们上面实现的判断链表是否为空的这个函数,好判断完之后我们就来实现尾插,这里尾插就得先找到倒数第二个和倒数第一个元素的地址,将其记录下来,将倒数第一个元素进行释放,然后再把倒数第二个元素和我们的头节点来构成一个循环,那么这里的逻辑就很简单我们直接来看看代码:

void ListPopBack(LTNode* phead)
{assert(phead);assert(Listempty);LTNode* tail = phead->prev;//倒数第一LTNode* prev = tail->prev;//倒数第二free(tail);tail = NULL;prev->next = phead;phead->prev = prev;
}

然后我们再来测试一下这个代码:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushBack(plist, 10);ListPushBack(plist, 20);ListPushBack(plist, 30);ListPushBack(plist, 40);ListPushFront(plist, 50);ListPushFront(plist, 60);ListPushFront(plist, 70);ListPopBack(plist);ListPopBack(plist);ListPrint(plist);return 0;
}

我们来看看这个代码的运行结果:

我们可以看到当有元素的时候我们的删除函数是正确的,然后我们再测试一下当我们的元素个数为0的时候再来执行删除会不会报错:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPopBack(plist);ListPopBack(plist);ListPrint(plist);return 0;
}

那当我们运行起来之后我们就可以看到我们这里报出来了错误:


那么说明我们这里确实是有预警的作用。

九.带头双向链表头删

我们这里的头删和尾删是一样的,得先判断一下我们的链表是否为空,然后再将这里的第一个元素进行删除,因为第一个元素是和第二个元素连在一起的,所以我们这里就创建一个指针记录一下第二个元素的位置,然后我们就先将第一个元素释放掉,然后再将头节点和第二个元素连接起来,那么我们这里的代码就是这样:

void ListPopFront(LTNode* phead)
{assert(phead);assert(Listempty(phead));LTNode* scend = phead->next->next;free(phead->next);phead->next = scend;scend->prev = phead;
}

我们这里可以来测试一下这个函数的实现是否是正确的:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPushFront(plist, 60);ListPushFront(plist, 50);ListPushFront(plist, 40);ListPushFront(plist, 30);ListPopFront(plist);ListPopFront(plist);ListPrint(plist);return 0;
}

我们来看一下运行的结果:

那么我们这里的代码实现就是真确的,但是这里有些小伙伴们可能就会优点疑惑啊,我们这里要是只有一个元素,那你这还是真确的吗?你确定这里不需要分开来讨论吗?那么这里大家可以想象一下,当我们只有一个元素的时候,我们这里的scend会指向的是谁?phead->next->next,因为我们这里的循环是连续的,所以我们这里的scend指的就是我们的phead对吧,如果是phead的话我们下面的操作是不是就是将phead的两个指针都指向自己啊,所以我们这里只有一个元素的话,也不会出现问题。

十.带头双向链表的长度

在有些书籍上面喜欢把头节点中的存放数据的地方用来存放我们链表的长度,那这么做是否是对的呢?我只能说有些情况是对的,我们的链表的长度永远都是整型,但是我们链表中的元素存放的数据一定是整型吗?不一定吧,他可能是其他的类型,比如说指针类型,浮点型。结构体类型等等,那如果是这些类型的话,你再用头节点来顺便求长度的话是不是就会出错啊,所以我们这里就不建议大家采用这样的方法,那么我们这里在求链表的长度的时候就最好采用循环遍历的方式,跟我们之前实现链表的打印是差不多的,那么我们的代码就如下:

int ListSize(LTNode* phead)
{LTNode* cur = phead->next;int size = 0;while (cur != phead){size++;cur = cur->next;}return size;
}

我们来测试一下这个函数的正确确性:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPushFront(plist, 60);ListPushFront(plist, 50);ListPushFront(plist, 40);ListPushFront(plist, 30);ListPopFront(plist);ListPopFront(plist);int size = ListSize(plist);ListPrint(plist);printf(" %d", size);return 0;
}

我们来看看运行的结果:

这里的元素个数确实是3那么我们这里的函数实现就是正确的。

十一.带头双向链表的查找

我们这里查找的思路也和之前打印的思路是一样的,通过循环遍历来一个一个的查找元素,如果找到了我们就返回这个元素的地址,如果没有找到就返回一个空指针,那么我们这里的代码就如下:

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

那么我们这里就可以来测试一下:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPushFront(plist, 60);ListPushFront(plist, 50);ListPushFront(plist, 40);ListPushFront(plist, 30);LTNode*cur=ListFind(plist, 60);printf("%d", cur->x);return 0;
}

那么我们这里运行起来就可以看到这里的代码运行正确:

十二.带头双向链表任意位置的插入

我们这里默认的插入是前插,我们先创建一个节点newnode,再找到这个位置的前一个元素的地址,然后我们创建一个指针变量prev来记录这个地址,接着我们将这个数据插入到这个链表里面,那么这里我们就让prev的next指向newnode,再让newnode的prev指向prev,这样我们就让前面的元素和后面的元素连接了起来,然后我们再让newnode和pos位置的元素连接起来,那么这里操作也与上面的类似,那么我们这里就不多说了我们直接看代码:

void ListInsert( LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyListNode(x);LTNode* prev = pos->prev;prev->next = newnode;newnode->prev = prev;newnode->next = pos;pos->prev = newnode;
}

那么这里大家要注意的一点就是当我们传过来的地址是头部的话,那我们这里插入的是哪里呢?这里大家仔细地推导一下就可以发现这里插入的地方就是在我们链表的尾部,因为头节点的前面是链表的尾部,那么这里大家在使用的时候得注意一下。我们来测试一下看看是否是对的:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPushFront(plist, 60);ListPushFront(plist, 50);ListPushFront(plist, 40);ListPushFront(plist, 30);LTNode*cur=ListFind(plist, 60);ListInsert(cur, 10 * cur->x);ListPrint(plist);return 0;

我们运行一下:

十三.带头双向链表任意位置的删除

那么我们这里的任意位置的删除的思路也十分的类似就是先找到指定位置前的元素的地址,再找到该位置之后的元素的地址,然后再释放掉该位置的元素,再将前一个元素和后一个元素连接起来,那么这就非常的简单了哈,大家看看代码就应该能够明白:

void ListPop( LTNode* pos)
{assert(pos);LTNode* prev = pos->prev;LTNode* next = pos->next;free(pos);prev->next = next;next->prev = next;
}

我们来测试一下:

#include"List.h"
int main()
{LTNode* plist = NULL;plist = ListInit(plist);ListPushFront(plist, 70);ListPushFront(plist, 60);ListPushFront(plist, 50);ListPushFront(plist, 40);ListPushFront(plist, 30);LTNode*cur=ListFind(plist, 60);ListPop(cur);ListPrint(plist);return 0;
}

那么我们的运行结果就如下:

十四.带头双向链表的销毁

我们对链表的操作结束之后我们就得将其进行销毁,那么我们这里的销毁就是边遍历边销毁,这里的思路也和我们打印的时候的思路是一样的,但是我们在遍历完了还得将我们的头节点进行释放,那么我们的代码如下:

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

那么我们这个函数是不会改变外部指针的,所以这里大家使用这个函数的时候要自己来改变这个指针的值,将其置为空。

点击此处获取代码

c语言实现数据结构中的带头双向循环链表相关推荐

  1. 【数据结构初阶】链表(下)——带头双向循环链表的实现

    目录 带头双向循环链表的实现 1.带头双向循环链表的节点类型 2.创建带头双向循环链表的节点 3.向带头双向循环链表中插入数据 <3.1>从链表尾部插入数据 <3.2>从链表头 ...

  2. 【数据结构】带头双向循环链表的增删查改(C语言实现)

    文章目录 前言 一.什么是带头双向循环链表 二.带头双向循环链表的实现 1.结构的定义 2.链表的初始化 3.开辟新节点 4.在头部插入数据 5.在尾部插入数据 6.查找数据 7.在pos位置之前插入 ...

  3. c语言实现数据结构中的链式表

    以下是我用c语言实现数据结构中的链式表 #pragma once; #ifndef _STDLIB_H #include <stdlib.h> #endif #ifndef _ELEMTY ...

  4. 数据结构篇 --- 带头双向循环链表增删查改

    简述:有了前篇单链表的增删查改的基础 也是出于带头双向循环链表本身的优势  双链表的增删就变得格外简单.让我们一起来体验一下. 目录 带头双向循环链表图解: 带头双向循环链表基本结构: 带头双向循环链 ...

  5. 校园导游图C语言数据结构,用C语言和数据结构中的无向图存储结构编一个校园导游图完全的程序代码.docx...

    用C语言和数据结构中的无向图存储结构编一个校园导游图完全的程序代码.docx 下载提示(请认真阅读)1.请仔细阅读文档,确保文档完整性,对于不预览.不比对内容而直接下载带来的问题本站不予受理. 2.下 ...

  6. C语言实现链表【二】带头双向循环链表

    带头双向循环链表 结构描述: 带头双向循环链表:结构最复杂,一般用在单独存储数据.实际中使用的链表数据结构,都是带头双向循环链表.另外这个结构虽然结构复杂,但是使用代码实现以店会发现结构会带来很多优势 ...

  7. 带头+双向+循环链表(C语言)

    文章目录 什么是带头+双向+循环链表 节点的创建 链表的初始化 打印函数 查找函数 尾插函数 尾删函数 头插函数 头删函数 在pos位置前插入 任意位置删除 销毁函数 什么是带头+双向+循环链表 带头 ...

  8. 一个完整的c语言程序结构图,用C语言和数据结构中的无向图存储结构编一个校园导游图完全的程序代码.docx...

    用C语言和数据结构中的无向图存储结构编一个校园导游图完全的程序代码 #define Infinity 1000 #define MaxVertexNum 35 #define MAX 40 #incl ...

  9. 【数据结构】链表:带头双向循环链表的增删查改

    本篇要分享的内容是带头双向链表,以下为本片目录 目录 一.链表的所有结构 二.带头双向链表 2.1尾部插入 2.2哨兵位的初始化 2.3头部插入 2.4 打印链表 2.5尾部删除 2.6头部删除 2. ...

最新文章

  1. Python可视化——3D绘图解决方案pyecharts、matplotlib、openpyxl
  2. 从运维域看 Serverless 真的就是万能银弹吗?
  3. nginx添加对web status及status的每一项含义
  4. dy之xgorgon0404参数
  5. sqrt()函数的注意事项
  6. 人脸检测三个算法比较
  7. SQL----常用函数
  8. HDU 5976 2016ICPC大连 F: Detachment(找规律)
  9. openwrt运行linux软件,使用OpenWrt开发嵌入式Linux(二):先让系统跑起来(使用initramfs)...
  10. 在sql语句中该如何处理null值
  11. 大学计算机作业互评评语简短,学生作业互评表的填写方法
  12. Autocad中批量调整增强属性块中的元素的位置
  13. linux系统scsi硬盘,Linux系统中SCSI硬盘的热拔插
  14. antd中table组件中如何进行换行操作(react中)
  15. 新浪微博php实习生电面
  16. 2022 QS世界大学排名发布!MIT霸榜,清北冲上全球前20
  17. java代码获取银行实时汇率
  18. 惠普暗影精灵6-开机显示非惠普原装电池(win11)
  19. 老旧小区改造,智慧社区解决方案应用其中
  20. 分享Windows版pgadmin(v4.17)

热门文章

  1. PMP考试主要知识点
  2. 用深度学习预测世界杯胜率,有多大把握?
  3. 用python画六芒星_Python绘制六角星、多角星、小太阳、小风车
  4. 图灵奖级别的设计:计算机中的浮点数
  5. owncloud私有云搭建
  6. 希捷存储服务器型号,希捷SATA硬盘 打造高效存储服务器平台
  7. 自然语言处理在化工的应用
  8. Python完成毫秒级抢单,助你秒杀淘宝大单
  9. 漏洞威胁分析报告(上册)- 不同视角下的漏洞威胁
  10. 如何实现网页背景图片自适应全屏?