板鸭带你手撕C语言双向链表

1.双向链表的结构

我们都知道双向链表有四种,分别是:双向带头循环链表,双向带头不循环链表,双向不带头循环链表,双向不带头不循环链表。

这里我们就讲双向带头循环链表。

为什么要讲这一种链表呢?

因为双向带头循环链表在实际生活中的使用非常广泛,而且我们只需要了解这一种双向链表,其他三种双向链表我们也会很容易掌握。

下图就是我们带头双向链表的逻辑结构。

注意:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严 谨,但是为了同学们更好的理解就直接称为单链表的头节点。 带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的” 。“哨兵位”存在的意义:遍历循环链表避免死循环。

最后面有全部代码的实现

2.带头双向循环链表的实现(头文件)

为了代码的清晰可见以及遵循高内聚低耦合的原理,这里我们采用一个头文件(List.h),两个源文件(List.c[接口的实现]   test.c[测试用例])的方法来实现。

头文件的代码如下(phead指的是我们的哨兵位)

我来讲讲头文件中的各个作用:

1.我们把库函数所需要的头文件包含在List.h中,这样我们在其它两个源文件只需要引用这个头文件就可以了。

2.我们将int型重命名为LTDataType,这样我们以后如果要改为char类型或其它类型只需要改一次就可以了。

3.下面一块就是带头双向循环链表的结构写法,我们也可以对它进行一个重命名来方便我们后面的书写。

3.再下面的就是各种接口(函数)的声明。 

3.带头双向循环链表的实现(源文件)

我们先创建一个哨兵位:

这里我们在test.c中用plist来当这个哨兵位的指针,对应的我们就要在List.c中来写初始化节点的接口

我们在堆上用malloc开辟一块空间,当然还要判断一下这块空间是否开辟成功了,然后我们再对他进行初始化,记住,哨兵位不是我们的头节点,打印的时候我们是不会把哨兵位的信息打印出来的,所以这里我们就把设置的有表示度一点,值给它设为0,然后把它的前指针和后指针都指向它自己。

3.2尾插

我们接下来对数据进行尾插操作,尾插的接口如下图所示:

这个接口是专门用来创建节点的,因为我们的不同类型的插入都需要创建一个新节点嘛,所以我们为了避免代码的重复性,我们重新封装一个函数来实现它。

这里我们先采用七匹狼法(assert)来暴力排除哨兵位为NULL的情况,因为我们是要在哨兵位后面进行尾插的,总不能哨兵位为空指针吧,还有一点,注意我们这里传的是一级指针,咦~,为什么我们不用二级指针呢?在单链表中我们为了改变不带头单向循环链表中的数据都是传递的二级指针呀?那么我们这里为什么只传一个一级指针呢?答案很简单,我们这里是已经创建好了一个哨兵位,并且哨兵位里的数据我们可以把它认为是无效的,我们只需要将它的prev和next指向进行调整就好了。

好,我们回到这里,接下来就是传统操作,先开辟一个节点(创建节点的接口代码等会发),用node来接收这个节点,注意既然是尾插,那么我们是不是该把node的prev指向哨兵位的prev(原先的尾节点),然后我们把node的next指向哨兵位,这样我们就处理好了node这个新节点,接下来我们再处理哨兵位的prev和原先最后一个节点的next,我们这里先改变原先最后一个节点的next,将原先最后一个节点的next指向node,再把我们的哨兵位的prev指向新的尾节点。

我们来看看效果(打印的节点在后面)

由控制台终端的信息我们可以看到我们的尾插成功了。

所以我们会发现,这种链表的实现起来非常简单,没有单链表的那么多顾虑,这也是我为什么说它非常香的原因,其实我们只需要了解尾插的原理,后面的接口实现起来都会非常轻松,因为原理都是大差不差的。

3.3头插

同样的操作,我们先用“七匹狼”来判断哨兵位是否为空,然后用node来创建一个新的节点,之后我们同样先处理node这个新节点,注意,不同的插法对象也是不同的头插我们关注的节点就是哨兵位和原先的头节点(我们的头节点指的是哨兵位后面的那个节点),我们将node的next指向哨兵位的next,再把node的prev指向哨兵位,再把原先头节点的prev指向新的头节点,再把哨兵位的next指向新的头节点。

3.4尾删

插入操作我们都解决了,删除操作那不是更简单嘛

注意:删除我们删除的是有效节点,也就是哨兵位后面的节点,那我们总不能一个有效节点都没有吧,所以我们可以断言一下头节点是否为空然后再进行操作,既然是删除那么我们就不需要创建新的节点了,但是我们要创建一个指针变量来存储需要删除的节点,这是为了方便我们之后对它进行空间释放。所以我们的cur就指向最后一个节点,然后进行操作,我们先把哨兵位的prev指向旧的尾节点的前一个节点,然后再把新的尾节点的next指向我们的哨兵位,这样我们的旧的尾节点就分离成功了,之后我们再把这个节点释放掉,再把cur置为NULL(不然就是野指针了)。

3.5头删

头删的操作也是一样的,先来两个断言,再来一个新的指针变量来记录要删除的节点的位置,然后进行删除操作,我们把哨兵位的next指向旧的头节点的下一个节点,再把新的头节点的prev指向哨兵位,然后再释放这个节点,再把这个野指针置为NULL。

3.6查找

注意:查找是需要返回我们找到这个节点的指针的,所以类型为LTNode*型。

我们这里采用的方法是查找数字是否存在来判定这个节点是否存在。

操作:我们先断言一下哨兵位是否为空,其实也可以再断言一下头节点是否为空。创建一个新的指针变量来进行遍历的操作,遍历的结束条件就为当cur指向哨兵位的时候,指向哨兵位就遍历结束了嘛,找对应的data值是否等于x是就返回,不是就继续遍历直到遍历完截至。

3.7指定位置插入

操作:先断言这个节点是否为NULL,然后创建一个新的节点,把新的节点的next指向原先pos的下一个节点,再把新节点的prev指向pos,再把pos节点的原先下一个节点的prev指向新的节点,再把pos的下一个节点指向新的节点。

3.8指定位置删除

我们先断言一下,然后再把pos的前一个节点的next指向pos的后面一个节点,再把pos的后面一个节点的prev指向pos的前一个节点。

3.9打印双链表

这里我们不要忘记断言一下头节点(我忘记了),然后创建一个新的指针变量cur来进行遍历整个链表,然后再打印出来就好了。

3.10链表的销毁

我们进行断言操作,然后创建一个新的指针变量cur来进行遍历整个链表,我们还要再创建一个指针变量next,因为中途我们要释放节点嘛,最后注意,在这里我是故意把哨兵位置为NULL的,但实际上我们很容易就能想到我们传递的是一级指针,不可能改变plist,所以我们要在test.c文件中自己把哨兵位置为NULL。

4.0所有的代码

List.h

#pragma once
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <stdbool.h>
//定义双向链表节点的结构
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

void LTPrint(LTNode* phead);

//链表的初始化以及销毁
//void LTInit(LTNode** pphead);
LTNode* LTInit();

//插入删除操作
void LTPushBack(LTNode* phead, LTDataType x);//链表的尾插
void LTPushFront(LTNode* phead, LTDataType x);//链表的头插
void LTPopBack(LTNode* phead);//链表的尾删
void LTPopFront(LTNode* phead);//链表的头删

void LTInsert(LTNode* pos, LTDataType x);//链表的指定插入
void LTErase(LTNode* pos);//链表的指定删除
LTNode* LTFind(LTNode* phead, LTDataType x);//链表的查找
void LTDestroy(LTNode* phead);//链表的销毁

List.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "List.h"

//void LTInit(LTNode** pphead)
//{
//	*pphead = (LTNode*)malloc(sizeof(LTNode));
//	if (*pphead == NULL)
//	{
//		perror("malloc");
//	}
//	(*pphead)->data = -1;
//	(*pphead)->next = (*pphead)->prev = *pphead;
//}

LTNode* LTInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		perror("malloc");
		return;
	}
	phead->data = -1;
	phead->next = phead->prev = phead;
	return phead;
}

LTNode* ListBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	node->data = x;
	node->next = node->prev = node;
	return node;
}

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* node = ListBuyNode(x);
	node->prev = phead->prev;
	node->next = phead;
	phead->prev->next = node;
	phead->prev = node;
}

void LTPrint(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}


void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* node = ListBuyNode(x);
	node->next = phead->next;
	node->prev = phead;
	phead->next->prev = node;
	phead->next = node;
}


void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* cur = phead->prev;
	phead->prev = phead->prev->prev;
	phead->prev->next = phead;
	free(cur);
	cur = NULL;
}
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* del = phead->next;
	phead->next = phead->next->next;
	phead->next->prev = phead;
	free(del);
	del = NULL;
}


void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* node = ListBuyNode(x);
	node->next = pos->next;
	node->prev = pos;
	pos->next->prev = node;
	pos->next = node;
}
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}



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

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void ListTest()
{
	/*LTNode* plist = NULL;
	LTInit(&plist);*/
	LTNode* plist = LTInit();
	//LTPushBack(plist, 1);
	//LTPushBack(plist, 2);
	//LTPushBack(plist, 3);
	//LTPushBack(plist, 4);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	LTPushFront(plist, 5);
	LTPushFront(plist, 6);
	LTPushFront(plist, 7);
	LTPushFront(plist, 8);
	LTPrint(plist);
	LTNode* pos = LTFind(plist,6);
	LTNode* pos1 = LTFind(plist,8);
	LTInsert(pos, 10);
	LTPrint(plist);
	LTErase(pos1);
	LTPrint(plist);
	//if (pos != NULL)
	//{
	//	printf("找到了\n");
	//}
	//else
	//{
	//	printf("没找到\n");
	//}
	//if (pos1 != NULL)
	//{
	//	printf("找到了\n");
	//}
	//else
	//{
	//	printf("没找到\n");
	//}
	//LTPrint(plist);

	LTDestroy(plist);
	plist = NULL;
}

int main()
{
	ListTest();
	return 0;
}

希望能帮助到大家!!!

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值