数据结构之双向链表(源码)
线性表
双向链表是线性表链式存储结构的一种,若对链式存储结构进行分类可以分为八种。
- 带头、不带头:指的是该连链表有无头节点,头节点不存放任何内容,它不一定是链表必备的元素,而一个链表拥有一个头节点就可以对后续的插入、删除等操作进行统一而不需要判空等情况。
- 单向、双向:双向链表与单向链表的区别在于,它多了一个指针用来存放上一个节点的指针,这样就可以通过一个节点任意的向前、向后遍历。
- 循环、不循环:链表的循环结构是将不循环链表的尾节点从指向空指针改为指向头指针。完成链表的自循环。
本篇所述:带头双向循环链表,简称双链表,双链表和单链表(不带头单向不循环链表)是八种链表中常用的两个链表,
双向链表
双链表结构
双链表是一个带头双向循环链表,更据它的特性不能想出,在一个双链表的一个节点里它的指针域用来存放前一个节点的地址和存放下一个节点的地址
typedef int ListNodeDataType;
typedef struct ListNode
{
ListNodeDataType data;
struct ListNode* prev;
struct ListNode* next;
}ListNode;
prev是前驱指针存放前一个节点的地址,next是后继指针存放下一个节点的指针,而双链表是循环链表,尾节点没有下一个节点,它是指向头节点,头节点的prec指针指向尾节点。
功能实现
//初始化
void ListNodeInit2(ListNode** pphead);
ListNode* LTInit();
//打印
void ListPrint(ListNode* phead);
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x);
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x);
//尾删
void ListNodePopBack(ListNode* phead);
//头删
void ListNodePopFront(ListNode* phead);
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x);
//指定位置删除
void ListNodeErase(ListNode* pos);
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x);
//销毁
void ListDestory(ListNode* phead);
void ListDestory2(ListNode** pphead);
创建节点、初始化、打印
创建节点
//创建节点
ListNode* ListBuyNode(ListNodeDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc :");
exit(1);
}
newnode->data = x;
newnode->next = newnode;
newnode->prev = newnode;
}
我们说双链表是由一个一个的节点组成,而节点里存放的有数据,前驱指针用来存放上一个节点的地址和后继指针
用来存放下一个节点的地址,在创建节点时,使用malloc函数开辟空间即可,realloc适合对一块连续存储的空间开辟新的更大的空间,而节点用不是realloc函数,它们在物理空间上不一定是连续的。
使用malloc函数开辟完空间后别忘了判断是否开辟成功,这是为了养成一个良好的习惯,如今的计算机想要开辟空间是否,一般都是内存被用完了 。
最后,将x值放入节点里,由于它只是一个节点,没有前驱节点,和后继节点,而双向链表是循环链表所以让这两个指针指向自己,实现自循环,而不会去指向空指针。
初始化
//初始化
ListNode* LTInit()
{
ListNode* phead = ListBuyNode(-1);
return phead;
}
// 初始化
void ListNodeInit2(ListNode** pphead)
{
*pphead = ListBuyNode(-1);
}
初始化头节点,使双链表带头,为了后续插入、删除等操作同意而设置的,数据域一般无意义,在链表里不一定有头节点。
初始化双链表有两种写法:
创建头节点让将其地址返回
- 不需要传递参数,调用创建节点函数后将开辟的空间返回即可
- 头节点不需要存储有效信息,所以在调用ListBuyNode函数是任意传递了一个整形变量。
将头节点声明后传递它的地址给ListNodeInit2初始化函数,在函数内完成空间的开辟。
- 需要传递参数,且是二级指针,指向双链表的变量已经是一级指针,想要改变一级指针的内容必须取出其地址,使用二级指针接收。
- 传递的是二级指针在函数内对形参的开辟,影响到了实参,不需要将地址返回。
再重述一遍,指向双链表的变量是一级指针,也就是头指针它是链表不可缺少的元素,链表里有头节点他就指向头节点,没有头节点它就指向第一个节点。想要对头指针修改,得传递它的地址,才能使函数内对形参的改变影响到实参。而一级指针的地址需要二级指针接收。
打印
//打印
void ListPrint(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
提前将打印功能的实现,是为了方便后续对双链表,插入数据、删除数据进行更方便的观察,而不是每一次调用插入、删除等函数都使用vs的调试功能。
首先、在打印单链表时,同样使用了循环,创建了一个临时指针来指向第一个节点,打印单链表数据的接收条件是pcur指向空时停止。而双链表是一个循环链表它没有一个节点的指针是指向空的。
所以这里以pcur指向头节点时为循环结束条件 pcur != phead
, 它并不存放有效数据,不用打印它。
在打印时需要从第一个节点开始打印所以pcur指针的起始位置是 ListNode* pcur = phead->next;
,通过将pcur的下一个节点的地址赋给它,完成自循环 pcur = pcur->next;
。
查找
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
查找,数据有着比较重要的地位,涉及到了指定位置(之前)插入,和指定位置删除等。
双链表的查找很好理解,想要查找一个元素,只需要将链表里每一个数据一一比较大小,若两者刚好相等,这就是需要查找的数据,然后将其地址返回即可,若跳出循环后并没有找到对应的数据,返回空指针。
使用assert断言,防止传递的头指针为空,并在函数内对空指针解引用从而引发一系列报错。
循环遍历每一个节点时,并不会查找头节点,将临时指针的起始位置设为第一个节点 ListNode* pcur = phead->next;
,在循环内使用if语句进行判断,形参x和pcur内的data大小是否相等,相等将其地址返回,若不等继续循环判断,出了循环后没有找到,返回空指针即可。
插入
插入函数,传递的都是一级指针,为啥不传递二级指针捏,执行对节点的插入并不会影响到头指针的指向,传递一级指针即可。
插入分为:
- 头插、尾插
- 指定位置之前插入、指定位置之后插入
在实现节点的插入时实现需要考虑的是,我插入如这个节点会影响到那些节点,该如何选择插入的顺序将影响最小化。
尾插
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* newnode = ListBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
第一步:使用assert断言判断,头指针是否为空,为空运行程序就会报错。
第二步:如图,想要在尾节点插入一个新的节点,那就需要创建新的节点调用 ListBuyNode函数。
第三步:首先尾插一个节点会影响到的节点有:phead
(头节点)、phead->prev
。
以及待插入的newnode
新节点,会影响到phead的prev指针、phead->next的next指针,newnode的next、prev指针,
其中影响最小的是新节点newnode的两个指针,先改变它们的指向,让其prev指向最后一个节点,next指向头指针
然后再改变phead->prev指向的节点,也就是尾节点,先改变尾节点是由于提前将尾节点的prev指针改为newnode,那就无法通过头指针找到尾节点了。phead->prev->next = newnode;
,这里让尾节点的next指针的指向从头节点改为newnode。
所以先改变头指针之外的节点,然后再改变头指针。
改变头指针里prev的指向,让其指向新的节点,这样就完成了所有节点的改变,成功插入了新的节点。
头插
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* newnode = ListBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
头插与尾插及其类似,只是改变了插入如新节点的位置。
第一步:使用assert断言判断,头指针是否为空,为空运行程序就会报错。
第二步:第二步:如图,想要在尾节点插入一个新的节点,那就需要创建新的节点调用 ListBuyNode函数。
通过代码观察可以发现,再头插和尾插里的第一步和第二步一模一样,当然指定位置之后插入的逻辑也是一样的。
最主要的区别还是它们影响的节点不同。对新节点进行头插,主要影响到头节点phead
和头节点的下一个节点(第一个节点)phead->next
。
有了尾插的经验,插入头节点时。首先改变新节点的next、prev指针的指向让prev指向头节点 newnode->prev = phead;
,和next指针指向第一个节点 newnode->next = phead->next;
,完成了头节点的改变,接着就改变,第一个节点的prev指针,让它指向newnode,最后改变头节点的next指针让其指向newnode。
指定位置之后插入
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x)
{
assert(pos);
//pos newnode pos->next
ListNode* newnode = ListBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
指定位置之后插入,一开始的断言的创建新节点的操作理解是一样的,最主要的区别是,指定位置插入影响的节点不同,而指定位置之后插入的实现,我只能说与头插基本上没有区别,在代码上只是将phead指针改为了pos指针。
首先,在pos之后插入新的节点newnode,它会影响到pos
节点,pso->next
两个节点,其中需要改变pos的next指针的指向,以及pos->next->prev指针的指向,需要它们指向新的节点newnode,而newnode的next指针需要指向pos的下一个节点,pos->next,和newnode的prev指针需要指向pos节点。这样就完成了对新节点的插入。其逻辑与头插无异。
删除
判空
bool ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead;
}
在对删除双链表时有一种特殊情况,就是只有一个头节点,这时双链表被看作是空链表,这与前文实现的单链表不同,单链表是需要将所有节点删除才为空,因为它没有头节点,这才将所有节点删除。
对于这种特殊情况做出了判断,当头节点的next指针指向自己的时候说明双链表里只有一个头节点,此时双链表为空,返回true,双链表不为空的话返回false。
再删除函数里都需要调用这种方法,以免错误的将头节点删除。
使用assert断言,进行判断 assert(!ListEmpty(phead));
,若双链表为空返回true,!true
,而为假,运行代码时直接报警告。
尾删
//尾删
void ListNodePopBack(ListNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//phead phead->prev phead->prev->prev
ListNode* del = phead->prev;
phead->prev = del->prev;
del->prev->next = phead;
free(del);
del = NULL;
}
尾删,首先考虑的是删除尾节点会影响到那些节点,而这些节点有需要做出那些改变,改变的顺序。
影响的节点:删除尾节点会影响到头节点 phead
,待删除的节点phead->perv
,尾节点的前一个节点phead->prev->prev
.
为了避免出现过多的结构体访问操作符从而降低代码的可读性,以及方边后续的删除操作,这里将 ListNode* del = phead->prev;
删除节点重命名,也就是del节点。这样 phead->prev->prev
,又为 del->prev
。
如图需要对del节点进行删除,首先需要改变 del->prev
节点的next指针,然后再改变头节点的prev指针,这是为了先将头节点的prev指针修改后而找不到del节点,但不过我们以及提前将del节点保存的所以不用担心它的发生。
接下来就是修改指针的指向了,先修改 del前一个节点的指向del->prev
,删除尾节点后他就是新的尾,将它的next指向头节点 del->prev->next = phead;
,然后修改头节点的prec指针让其指向新的尾节点,最后将del节点释放即可,释放完别忘了将其置为空,他此时可是一个野指针~。
头删
//头删
void ListNodePopFront(ListNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//phead phead->next phead->next->next
ListNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
头删的逻辑与尾删类似,首先删除头节点会影响到那些节点:phead
、phead->next
、phead->next->next
,这三个节点,同样为了避免出现过多的结构体访问操作符,以及后续的操作,将 phead->next
,保存下来赋给del指针。
将这两个指针撇开重新指向新的节点
按照图示,需要将 del->next
,的prev指针指向头节点,将头节点的next指针指向新的next节点。
将指针的指向修改完后,将del释放即完成了头删操作,别忘了将del置空。
指定位置删除
//指定位置删除
void ListNodeErase(ListNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
指定位置删除的思考逻辑与头删尾删一样,首先考虑删除指定的节点pos会影响到那些节点:pos->prev pos pos->next
,这三个节点
将 pos->prev
节点的next指针撇开和, pos->next
的prec指针撇开,这样该是容易理解的多。
然后改变其指向,让
pos->prev
节点的next指针指向pos->next
节点。让
pos->next
节点的prev指针指向pos->prev
节点。最后将pos释放,置空即可。
销毁
//销毁
void ListDestory(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
ListNode* nextnode = pcur->next;
free(pcur);
pcur = nextnode;
}
phead = NULL;
pcur = NULL;
}
void ListDestory2(ListNode** pphead)
{
ListNode* pcur = (*pphead)->next;
while(pcur != *pphead)
{
ListNode* nextnode = pcur->next;
free(pcur);
pcur = nextnode;
}
*pphead = NULL;
pcur = NULL;
}
接口:
销毁函数的功能是将双链表所有内容全部清除,包括头节点。所以不许要调用判空函数。而销毁函数的两种方法一是传递二级指针,而是传递一级指针,这里与之前的初始化函数类似,分为了两种情况。而更推荐使用一级指针。
其原因是为了保证双链表接口的一致性,接口:现阶段不考虑吧过多的话,接口就是指函数。
保证接口的一致性是为了当实现的函数功能过多时,一些函数传递一级指针,一些函数传递二级指针,为了防止调用者的混淆,所以这里将双链表的所有功能均采用一级指针来实现。
销毁:
在进行销毁时需要创建一个临时变量用来遍历所有节点进行销毁,这里销毁的逻辑与单链表的销毁的逻辑时一样的,然后再循环内遍历销毁时需要使用一个next指针一便于将pcur释放后重新找到下一个节点释放,循环结束条件为 pcur != *pphead
。
最后将节点释放完后将pcur置为空,此时它为空指针。
缺陷
前文说过,需要对头节点进行更改就需要传递它的地址,也就是二级指针,而这里使用一级指针来实现对双链表的销毁就丢失了这种特性,所以在销毁完双链表后,还需要手动的将头节点置为空。
总结
总的来说,在实现双链表的算法时,在插入和删除上优先考虑的是插入一个节点会影响到那些节点、删除一个节点又会影响到那些节点,以及被影响节点的指针的指向。这里最好画图加以理解。
在插入、删除、查找等功能里均使用assert断言,这样做的目的是提高函数的健壮性、而不是在传递空指针时函数无法解决而产生一系列未知异常的情况。
还强调了在实现函数功能是统一双链表的一致性、这样虽然保证了所有函数传递的都是一级指针,但不可否认的是这样又会丢失一些功能,需要手动去实现,如传递一级指针,将一个双链表销毁后需要手动置空。
源码
List.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int ListNodeDataType;
typedef struct ListNode
{
ListNodeDataType data;
struct ListNode* prev;
struct ListNode* next;
}ListNode;
//初始化
void ListNodeInit2(ListNode** pphead);
ListNode* LTInit();
//打印
void ListPrint(ListNode* phead);
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x);
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x);
//尾删
void ListNodePopBack(ListNode* phead);
//头删
void ListNodePopFront(ListNode* phead);
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x);
//指定位置删除
void ListNodeErase(ListNode* pos);
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x);
//销毁
void ListDestory(ListNode* phead);
void ListDestory2(ListNode** pphead);
List.c
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
ListNode* ListBuyNode(ListNodeDataType x)
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
if (newnode == NULL)
{
perror("malloc :");
exit(1);
}
newnode->data = x;
newnode->next = newnode;
newnode->prev = newnode;
}
ListNode* LTInit()
{
ListNode* phead = ListBuyNode(-1);
return phead;
}
void ListNodeInit2(ListNode** pphead)
{
*pphead = ListBuyNode(9);
}
//打印
void ListPrint(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//尾插
void ListNodePushBack(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* newnode = ListBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
//头插
void ListNodePushFront(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* newnode = ListBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
bool ListEmpty(ListNode* phead)
{
assert(phead);
return phead->next == phead;
}
//尾删
void ListNodePopBack(ListNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//phead phead->prev phead->prev->prev
ListNode* del = phead->prev;
phead->prev = del->prev;
del->prev->next = phead;
free(del);
del = NULL;
}
//头删
void ListNodePopFront(ListNode* phead)
{
assert(phead);
assert(!ListEmpty(phead));
//phead phead->next phead->next->next
ListNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
free(del);
del = NULL;
}
//查找
ListNode* ListNodeFind(ListNode* phead, ListNodeDataType x)
{
assert(phead);
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//指定位置删除
void ListNodeErase(ListNode* pos)
{
assert(pos);
assert(!ListEmpty(pos));
//pos->prev pos pos->next
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
//指定位置插入(之后)
void ListNodeInsert(ListNode* pos, ListNodeDataType x)
{
assert(pos);
//pos newnode pos->next
ListNode* newnode = ListBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
//销毁
void ListDestory(ListNode* phead)
{
ListNode* pcur = phead->next;
while (pcur != phead)
{
ListNode* nextnode = pcur->next;
free(pcur);
pcur = nextnode;
}
phead = NULL;
pcur = NULL;
}
void ListDestory2(ListNode** pphead)
{
ListNode* pcur = (*pphead)->next;
while(pcur != *pphead)
{
ListNode* nextnode = pcur->next;
free(pcur);
pcur = nextnode;
}
*pphead = NULL;
pcur = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "List.h"
void rest()
{
ListNode* head = NULL;
//ListNodeInit2(&head);
head = LTInit();
//尾插
ListNodePushBack(head, 1);
ListNodePushBack(head, 2);
ListNodePushBack(head, 3);
ListNodePushBack(head, 4);
ListPrint(head);
//头插
//ListNodePushFront(head, 1);
//ListNodePushFront(head, 2);
//ListNodePushFront(head, 3);
//ListNodePushFront(head, 4);
//ListPrint(head);
//尾删
//ListNodePopBack(head);
//ListPrint(head);
//ListNodePopBack(head);
//ListPrint(head);
//ListNodePopBack(head);
//ListPrint(head);
//ListNodePopBack(head);
//ListPrint(head);
//头删
//ListNodePopFront(head);
//ListPrint(head);
//ListNodePopFront(head);
//ListPrint(head);
//ListNodePopFront(head);
//ListPrint(head);
//ListNodePopFront(head);
//ListPrint(head);
ListNode* ret = ListNodeFind(head,4);
if (ret != NULL)
{
printf("有了!\n");
}
else
{
printf("没有!\n");
}
//ListNodeErase(ret);
//ListNodeInsert(ret, 6);
ListPrint(head);
//ListDestory2(&head);
ListDestory(head);
head = NULL;
}
int main()
{
rest();
return 0;
}