C实现双向链表和相关函数!巨详细!

双向链表的实现和相关函数

一.链表的分类

根据单向/双向、带头/不带头、循环/不循环,可以将链表分为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------

相关推荐
_深海凉_2 小时前
LeetCode热题100-移除元素
数据结构·算法·leetcode
Makoto_Kimur2 小时前
Java Scanner 的 ACM 常用输入模板
java·数据结构·算法
m0_716765232 小时前
数据结构三要素、时间复杂度计算详解
开发语言·数据结构·c++·经验分享·笔记·算法·visual studio
网安INF3 小时前
数据结构第二章复习:线性表
java·开发语言·数据结构
北顾笙9803 小时前
day21-数据结构力扣
数据结构
csuzhucong3 小时前
puzzle(0334)双面数局
数据结构·算法
菠萝地亚狂想曲3 小时前
FreeRTOS heap4
c语言·stm32·嵌入式开发
计算机安禾3 小时前
【数据结构与算法】第40篇:图论(四):最短路径——Dijkstra算法与Floyd算法
c语言·数据结构·算法·排序算法·哈希算法·图论·visual studio
啦啦啦!4 小时前
c++AI大模型接入SDK项目
开发语言·数据结构·c++·人工智能·算法