双向链表的实现和相关函数。
一.链表的分类
根据单向/双向、带头/不带头、循环/不循环,可以将链表分为8类。(2*2*2)
单双向:指针是只有后继,还是既有后继又有前驱;
带头不带头:是否有专门的哨兵位;(由此也能说明,我们在单链表中说的头结点并不是真正意义上的头节点,只有哨兵位才能被称为头结点)
循环不循环:尾结点的next指针指向NULL还是哨兵位phead。
常见的有2类:单向不带头不循环链表(简称单链表)、双向带头循环链表(简称双向链表)。

------单链表

------双向链表
二.双向链表的实现
和单链表相同,双向链表也是封装在结构体中的。
//List.h
//双向链表的实现
typedef int SLDatatype;
typedef struct ListNode
{
LTDatatype data;
//数据
struct ListNode* prev;
//前驱指针
struct ListNode* next;
//后继指针
}LTNode;
要实现循环,前驱和后继指针都不能少。
如果我将代码这样写------
//List.h
//双向链表的实现
typedef int SLDatatype;
typedef struct ListNode
{
LTDatatype data;
ListNode* prev;
ListNode* next;
}LTNode;
可以吗?
可以将重定义和结构体的定义分开来看。
//List.h
//双向链表的实现
typedef int SLDatatype;
struct ListNode
{
LTDatatype data;
ListNode* prev;
ListNode* next;
};
//重定义
typedef struct ListNode LTNode;
LTNode出现在结构体的定义之后,所以在定义前驱和后继指针时,只能使用struct ListNode,而不能用LTNode。
三.相关函数
(1)头插 LTPushfront
在顺序表中,是在检查函数SLCheckCapacity中动态申请内存,在单链表中,我们是在创建节点函数SLTBuyNode函数中动态申请内存,先确定一个点,在双向链表中,我们也是在创建节点函数LTBuyNode函数中为节点动态申请内存的。
为了实现头插,我们先来实现创建节点函数LTBuyNode。
(2)创建节点 LTBuyNode
//List.c
#include"List.h"
//创建节点函数的定义
LTNode* LTBuyNode(LTDatatype x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if(NULL == newnode)
exit(1) ;
//申请成功
newnode->data = x;
return newnode;
}
在单链表的节点创建函数中,我们不仅将数据赋值给了data,我们还将newnode->next=NULL,那双链表中的prev、next指针,需要做处理吗?
我们先来模拟一下创建节点的调用。
在第一次的调用时,是创建头结点phead,但仅仅只是创建头节点,双向链表是空的,但空的链表(带头双向循环链表)即使只有一个头结点也要满足循环的特征,所以phead的前驱和后继指针都应该指向它自己。
代码修改:
//List.c
#include"List.h"
//创建节点函数的定义
LTNode* LTBuyNode(LTDatatype x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if(NULL == newnode)
exit(1) ;
//申请成功
newnode->data = x;
newnode->prev = newnode->next = newnode;
return newnode;
}
再回到头插的实现:
(1)空链表

(2)非空双向链表

在开始写头插函数前,我想先问一个问题:我们应该在头插函数中调用LTBuyNode函数创建头结点吗?
如果头插函数是在没有创建头结点时被调用的,那我们需要在函数内部调用LTBuyNode函数创建头结点phead,但如果在双向链表不为空的情况下调用头插函数,还需要再调用LTBuyNode函数创建头结点吗?显然是不用的。
那我们要写分支语句判断啥时候该创建phead、啥时候不创建phead?
理论上是行的,但还存在一个问题。
如果我在头插函数内部为双向链表创建了头节点,那头结点的指针是需要一直被用的,也代表需要返回接收(LTPushFront函数的返回值不再是void,而是LTNode*)的,这么一想是不是挺麻烦?还不如我们单独再写一个初始化函数,专门来处理头结点的创建问题呢。
(3)初始化函数 LTInint
初始化函数的目的是创建头结点。
有两种实现方式:
①返回值为空+二级指针传址调用
//List.c
#include"List"
//初始化函数定义
void LTInit(LTNode** pphead )
{
//初始化函数的本质还是创建节点,调用LTBuyNode
*pphead = LTBuyNode(-1);
//头结点中是不需要放数据的,但想调用LTBuyNode函数需要传参
//传个值就行
}
//Test.c
#inclide"List"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
}
int main()
{
Test01();
return 0;
}
为啥要传二级指针不用多说了吧?
要对plist里存的指针产生真正的改动,就必须要传&plist(plist的指针,在plist一级指针的层面。只有传&plist二级指针,才是传址调用)。
②返回值为一级指针
//List.c
#include"List"
//初始化函数定义
LTNode* LTInit()
{
//在初始化函数内部创建头结点后返回
LTNode* phead = LTBuyNode(-1);
return phead;
}
//Test.c
#inclide"List"
void Test01()
{
LTNode* plist = LTInit();
//后面就能使用plist头结点指针了
}
int main()
{
Test01();
return 0;
}
接收初始化函数的返回值,让plist内存的指针确实是头结点指针也可以。
我们调试来看看:

头结点创建成功。
实现头插函数前的准备工作做好了,我们接下来来实现头插函数。
提一个疑问:头插函数的参数肯定需要头结点指针,那我们是传一级指针还是二级指针呢?
肯定有人会说肯定传二级指针呀!前面的单链表头插都用了二级指针,双向链表难道不用吗?
理论上可以用二级指针,但一级指针就已经够用了。
再次回到传二级指针的初衷,在单链表的头插函数中,是因为当时所谓的头指针(现在我们知道并不是头指针,而是第一个节点指针)会在函数内部发生改动,我们才采用二级指针传址调用的参数形式,可双向链表的头指针会被改动吗?
要知道,头插可不是在头结点前插入新节点,而是在头结点和第一个节点间插入新节点。
那既然头指针的值不需要改动,那传二级指针的必要性就不大了,传一级指针就已经完全够用。
我先讨论看起来更复杂的非空双向链表实现头插的情况。

代码实现:
//List.c
//头插的定义
void LTPushFront(LTNode* phead,LTDatatype x)
{
assert(phead);
//对指针有访问,则不能为空
//需要有能够插入的节点-创建节点的函数
LTNode* newnode = LTBuyNode(x);
//指针指向的变化
//新节点
newnode->prev = phead;
newnode->next = phead->next;
//老节点
phead->next->prev = newnode;
phead->next = newnode;
}
细节答疑:
①能够将老节点的两条语句直接进行颠倒吗?
phead->next = newnode;
phead->next->prev = newnode;
//还对吗?
既然我提出来,那显然说明不对。
不过原因在哪?
直接(关键在于不做语句的改动,直接生硬地将两条语句的位置进行颠倒)颠倒位置后,代码的实现就有误了,说明前一条语句对后一条语句产生了影响。
当我们将phead的next指针指向newnode后,再想找到d1节点的指针还能用phead->next来表示吗?不能,因为phead->next已经不指向d1而是newnode了。
此时,要找d1节点,就要写成phead->next->next,或者newnode->next。
phead->next = newnode;
phead->next->next->prev = newnode;
//或者 newnode->next->prev = newnode
联想到单链表也存在交换位置导致出现问题的情况,我们能发现,在一大群指针的指向发生交换时,改动顺序一般是从右向左。
②新老节点指针指向改动的语句能直接颠倒吗?
我们直接来写写看。
//先改变老节点
phead->next->next = newnode;
phead->next = newnode;
//再改变新节点
newnode->next = ?
我们将能指向d1节点的phead->next提前改成了newnode,当我们想让newnode的next指向d1节点指针时,怎么表示,总不能写newnode->next=newnode->next?
所以答案是不能颠倒顺序。
为了避免脑子糊涂,我们可以借助图像来理解改动顺序的问题:

如果我们先改变newnode的prev、next指针的指向,在图像中就是伸出两条分别指向head和d1的箭头,仅仅只是多出两条指向节点的箭头③④,不会对原链表中的节点指针产生任何影响。
这时,我们从右向左改动老节点的指针指向,让d1节点的prev指针指向newnode后,②箭头就被叉除了,但并没有影响①箭头,再将head的next指针指向newnode,才会使①被叉除。
我们将两次的直接颠倒放在一起分析:
如果我们先改变老节点的指针指向,在图像上就是叉除了①②箭头,①②箭头一旦被叉除,head和d1的一条线就断了(双向变成了单向),传递了头指针,所以head->next=newnode可以实现,newnode的next指针要指向d1,d1就无法用phead->next/phead->next->next/newnode->next来表示了。
而如果我们从左向右改动新节点的指针指向,当head->next指向newnode后,在图像上就是多了条③箭头,且叉除了①箭头(对老节点产生了影响),那再想通过phead->next表示d1的prev指针,就不行了(①箭头已经被叉除了),但可以用phead->next->next/newnode->next来表示d1的prev指针。
或许你会疑惑:前面不是说指针的指向改动一般是从右向左的,可新节点的指针指向为啥先是prev再是next?
其实两者直接颠倒也并没有关系,因为新节点的改动不会对老节点的指针指向产生影响,先写谁后写谁,完全没关系。
讨论完非空的双向链表后,再来看看空的双向链表。

唯一可能存在变化的部分就是指针的指向。
//新节点的改动
newnode->prev = phead;
newnode->next = phead;
//老节点的改动
phead->next = newnode;
phead->prev = newnode;
看似和下面的非空双向链表有出入,但其实是一样的道理。
//新节点
newnode->prev = phead;
newnode->next = phead->next;
//老节点
phead->next = newnode;
phead->next->prev = newnode;

最终的头插代码:
//List.c
#include"List.h"
//头插的定义
void LTPushFront(LTNode* phead,LTDatatype x)
{
assert(phead);
//需要有能够插入的节点-创建节点的函数
LTNode* newnode = LTBuyNode(x);
//指针指向的变化
//新节点
newnode->prev = phead;
newnode->next = phead->next;
//老节点
phead->next->prev = newnode;
phead->next = newnode;
}
打印在屏幕中检查实现是否正确更方便,接下来我们来实现打印函数。
(4)打印函数 LTPrint
在单链表的打印函数中,我们是通过next指针遍历打印那一套,双向链表也是这个逻辑。
//List.c
#include"List.h"
//打印函数的定义
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur!= phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}

(5)尾插 LTPushBack

//List.c
#include"List.h"
//尾插定义
void LTPushBack(LTNode* phead,LTDatatype x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
//新节点
newnode->next = phead;
newnode->prev = phead->prev;
//老节点
phead->prev->next = newnode;
phead->prev = newnode;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}
在实现单链表的尾插函数时,有一个比较麻烦的操作------找尾ptail,但在双向链表中不用,由于循环的特征,尾结点的指针可以通过phead->prev来表示,代码量因此减少。(不用找尾)
代入非空和空双向链表,都是能满足的。

(6)在pos之后插入 LTIsert
比较简单,就直接贴代码了。
//List.c
#include"List"
//在pos之后插入
void LTIsert(LTNode* pos, LTDatatype x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTIsert(plist,99);
LTPrint(plist);
LTIsert(plist->next, 66);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}

(7)头删 LTPopFront

改变指针的指向后,free指定节点就行。
注意:头删是删除第一个存了我们需要数据的节点,不是把哨兵位头结点删了!
//List.c
#include"List.h"
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
//排除空链表情况
LTNode* del = phead->next;
phead->next->next->prev = phead;
phead->next = phead->next->next;
free(del);
//很容易写成free(phead->next);
//但是phead->next的值已经改了
//需要创建临时变量提前保存
del = NULL;
}
既然都创建了del=phead->next,那代码可以精简成:
//List.c
#include"List.h"
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
//排除空链表情况
LTNode* del = phead->next;
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTIsert(plist,99);
LTPrint(plist);
LTIsert(plist->next, 66);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}

删除函数的关键有两个:
①必须要对链表判空!
单链表是assert(phead),可理解成没有第一个节点,就无法找到单链表,所以认为单链表为空;双向链表是assert(phead->next!=phead),只有哨兵位,没有其他节点就是双向链表为空。
②必要时,及时保存要被删除的节点指针!
(8)尾删 LTPopBack

//List.c
#include"List.h"
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
//写free(phead->prev)
// 相当于free(del->prev),显然错了
del = NULL;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTIsert(plist,99);
LTPrint(plist);
LTIsert(plist->next, 66);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}

(9)删除pos节点 LTErase
//List.c
#include"List.h"
//删除pos
void LTErase(LTNode* pos)
{
assert(pos);
//需要对双向链表判空,但我们不传头指针,没有条件判空
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTIsert(plist,99);
LTPrint(plist);
LTIsert(plist->next, 66);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
LTErase(plist->next);
LTErase(plist->prev);
LTPrint(plist);
}
int main()
{
Test01();
return 0;
}
pos节点删除函数内部不需要用到phead,所以参数只有pos节点,理论上,删除函数都要判空,但是此处没传phead,没有校验条件,故没有判空。

(10)查找节点 LTFind
注意一点循环条件,就够了。
//List.c
//查找定义
LTNode* LTFind(LTNode* phead, LTDatatype x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (x == pcur->data)
return pcur;
pcur = pcur->next;
}
//出循环代表没找到
return NULL;
}
//Test.c
#include"List.h"
void Test01()
{
LTNode* plist = NULL;
LTInit(&plist);
LTPushFront(plist, 1);
LTPrint(plist);
LTPushFront(plist, 2);
LTPrint(plist);
LTPushFront(plist, 3);
LTPrint(plist);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPrint(plist);
LTIsert(plist,99);
LTPrint(plist);
LTIsert(plist->next, 66);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
LTErase(plist->next);
LTErase(plist->prev);
LTPrint(plist);
LTNode* find = LTFind(plist,2);
if (find)
printf("找到了!\n");
else
printf("没找到!\n");
find= LTFind(plist, 200);
if (find)
printf("找到了!\n");
else
printf("没找到!\n");
}
int main()
{
Test01();
return 0;
}

(11)销毁双向链表
使用malloc创建,那就一个个free。
//List.c
//销毁
#include"List.h"
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while(pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
//跳出循环时:pcur=phead
//头指针指向的空间也要被回收释放
free(phead);
pcur = NULL;
phead = NULL;
}
上述代码是对的,但我想问一个问题:在链表的销毁中,我们需要释放头指针指向的空间,并将头指针phead置空,说明头指针的值是会改变的。
既然一级指针的值变了,函数传参怎么能传一级指针的传值调用呢?
不应该传二级指针的传址调用吗?
其实理论上确实要传二级指针。
但此处一级指针能实现动态内存的回收释放,只是与传递二级指针相比,传一级指针不能改变头指针的值,这一点小问题很好处理,只要在测试源文件里,我们对plist指针手动置空 就行了。
不过传二级指针就不用手动置空了,不是更方便吗?
这里使用一级指针传值调用还有一个重要的原因------保持接口一致性 。
与双向链表有关的函数大多(如果统一了LTInit的参数就是全部)函数的参数传递的都是头指针(一级指针),如果我们将销毁函数的参数写成二级指针,一方面不便于记忆,另一方面可能更容易出现传参类型不匹配的错误。
来看测试代码:
//Test.c
#include"List.h"
void Test01()
{
LTDestory(plist);
plist = NULL;
//手动置空
}
int main()
{
Test01();
return 0;
}
------end------
