C语言实现双向链表

前言

在讲双向链表之前,我会先总结一下前面的知识点,如需直接看双向链表的,可以直接跳转到双向链表的实现去阅读~~

链表的分类

在上一篇的8道算法题,我提到了用哨兵位可以很好地进行插入,这个哨兵位就是头结点!还有在解决约瑟夫问题时,我提到了使用循环链表的概念,循环链表就是头尾相连,形成一个环。

链表的分类有这几种情况:带不带头(头结点==哨兵位),单向还是双向(就是一个节点只能找到下一个节点的就是单向,如果既能找到上一个节点又能找到下一个节点就是双向),循环还是不循环(头尾相连的就是循环,头尾不相连的就是不循环)

所以链表一共有八大类,那我们回顾一下什么是单链表,单链表实际上就是不带头单向不循环的链表,这里我要讲的双向链表实际上是带头双向循环的链表,只要我们会这两个链表的实现,其他的链表实现也是很简单的~~

链表的优势

在C语言进阶的第一篇文章中,我带大家实现了动态顺序表,但是动态顺序表还是有存在空间浪费的出现,举个例子,我一共有101个数据需要保存,但是顺序表在第一百零一的时候会进行2倍或3倍扩容,假设扩2倍,那就是变成200容量,这时候就有99个空间浪费了,链表就可以实现一个数据一个数据的申请节点,不会有空间的浪费,但是单链表有一个缺陷,尾插尾删等操作时需要遍历所有节点导致效率不高,这时候我们就可以使用双向链表来大幅减少这种遍历的出现,也就是我会在下面提到的链表结构。

但是链表也有一个缺点,在数据量少的情况下,链表其实浪费的空间可能更大,因为链表结构是需要带一个或者两个指针的~~

任何事物都有两面性,我们需要根据实际情况来选择合适的数据结构来解决问题才是最perfect!!!

双向链表的实现

双向链表的含义

双向链表是带头结点(哨兵位),每一个结点都能找到前一个和后一个节点,并且头尾是相连的。形成带头双向循环的链表,双向链表就是它的简称。

那我们来定义双向链表的结构体:

typedef int ListDataType;

typedef struct ListNode
{
	ListDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

初始化和创建新节点

注意双向链表是一个带头的循环的链表,带头意味着我们在初始化要创建一个头结点,那头结点的两个指针怎么处理?由于这是双向链表,需要头尾相连,因此我们将头节点的两个指针都指向自己!既然如此,我们就写一个函数来创建新节点,让每个新节点的指针开始都指向自己~~

//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail"); exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

注意了,由于创建新节点的函数我们需要传入一个值,那我们的头结点就随便传入一个值,但是我们自己要知道头结点知识一个站岗的,不会存放有效值。

会不会有人会问,为什么不专门写一个函数来创建头结点,你可以自己尝试,我觉得没有必要,多写一个和创建新节点的代码感觉很浪费也没有很大必要~~

//初始化
void ListInit(ListNode** pphead)
{
	*pphead = CreatNewnode(-1);
}

头指针的传参问题

我们要知道初始化链表的时候就创建好头结点了,头指针就是指向这个头结点,所以我们不需要改变头结点,它是一个放哨的,与链表是共存亡的,所以我们一般情况下传一级指针就可以了~~

简单来说:头指针是指向头结点的,只要头结点还存在,头指针就没有必要发生改变~~

尾插

尾插操作,我们需要改变三个节点,分别是newnode,head,还有原来的尾节点head->prev。

//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);

	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

这里建议大家先将newnode 的next和prev两个指针先连接好,毕竟改变newnode的指向不会影响到另外两个节点,这时候我们就要考虑剩下两个节点怎么连接,为了避免找不到d3这个节点,所以我先改变d3这个节点,再改变head的节点~~

头插

头插,我们需要改变三个节点,newnode,head,d1;我们还是先改变newnode(不会对另外两个产生影响),再改变d1(防止改变head的时候找不到d1),最后改变head。

//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);
	
	newnode->prev = phead;
	newnode->next = phead->next;

	phead->next->prev = newnode;
	phead->next = newnode;
}

打印

注意头结点存放的值是无效的,所以从头结点的下一个结点开始打印,由于链表是循环的,所以当回到头结点的时候就要停止打印(循环停止的条件)

//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

尾删

为了更好的进行删除操作,我使用一个变量保存要删除的节点,便于要删除的节点的前一个节点与头结点进行连接~~

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

头删

注意这里的头删是删除第一个有效的节点,不是我们的哨兵位!!!

还是一样,使用一个变量保存要删除的节点

//头删
void ListPopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

查找

从第一个有效节点开始遍历链表进行查找即可~~

//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
	ListNode* pcur = phead->next;

	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

写这一个查找函数是为了方便我们后续指定位置的相关操作~~
还有要注意如果找不到就会放回NULL,所以后面的指定位置操作是记得判断pos是否有效!!!

指定位置删除

需要改变三个节点pos,pos->prev,pos->next这三个节点~~

//指定位置删除
void ListPopPos(ListNode* pos)
{
	assert(pos);
	
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

这里要注意了pos 如果就是头指针的话,是不可以进行删除操作的,由于我没有传头指针,所以没有判断这个条件~~

删除指定位置之前的数据

这里要注意,指定位置之前需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->prev != phead);

	ListNode* del = pos->prev;
	del->prev->next = pos;
	pos->prev = del->prev;
	free(del);
	del = NULL;
}

删除指定位置之后的数据

这里要注意,指定位置之后需要有有效的节点才能进行删除!!!所以这里你可以判断一下,传入头指针是为了更好地进行判断处理~~

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->next != phead);

	ListNode* del = pos->next;
	del->next->prev = pos;
	pos->next = del->next;
	free(del);
	del = NULL;
}

在指定位置之前插入数据

//在指定位置之前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->next = pos;
	newnode->prev = pos->prev;

	pos->prev->next = newnode;
	pos->prev = newnode;
}

在指定位置之后插入数据

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->prev = pos;
	newnode->next = pos->next;

	pos->next->prev = newnode;
	pos->next = newnode;
}

销毁链表

销毁链表我们需要传入头结点的二级指针了,因为头结点也需要进行销毁,头指针要置为NULL
但是这里我却使用了一级指针,是为了保持接口的一致性~~ 因为上面的函数除了初始化都是传一级指针,也是为了方便别人来使用我们的接口函数,减少使用者的记忆负担~~

//销毁链表
void ListDestroy(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

如果你还想让接口函数更加完美,我们可以改变一下初始化函数的:

//初始化
ListNode* ListInit()
{
	ListNode* phead = CreatNewnode(-1);
	return phead;
}

小结

在实现双向链表的时候,我们可以通过画图理解的方式进行理解和书写相应的代码~~

封装函数

List.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int ListDataType;

typedef struct ListNode
{
	ListDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//初始化
void ListInit(ListNode** pphead);

//打印
void ListPrint(ListNode* phead);

//插入
void ListPushBack(ListNode* phead, ListDataType x);
void ListPushFront(ListNode* phead, ListDataType x);

//删除
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);

//查找
ListNode* ListFind(ListNode* phead, ListDataType x);

//指定位置删除
void ListPopPos(ListNode* pos);

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos);

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos);

//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x);

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x);

//销毁链表
void ListDestroy(ListNode* phead);

List.c

#include "List.h"

//创建新节点
ListNode* CreatNewnode(ListDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("malloc fail"); exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//初始化
void ListInit(ListNode** pphead)
{
	*pphead = CreatNewnode(-1);
}

//尾插
void ListPushBack(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);

	newnode->prev = phead->prev;
	newnode->next = phead;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

//打印
void ListPrint(ListNode* phead)
{
	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//头插
void ListPushFront(ListNode* phead, ListDataType x)
{
	ListNode* newnode = CreatNewnode(x);
	
	newnode->prev = phead;
	newnode->next = phead->next;

	phead->next->prev = newnode;
	phead->next = newnode;
}

//尾删
void ListPopBack(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

//头删
void ListPopFront(ListNode* phead)
{
	assert(phead && phead->next != phead);

	ListNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

//查找
ListNode* ListFind(ListNode* phead, ListDataType x)
{
	ListNode* pcur = phead->next;

	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}

//指定位置删除
void ListPopPos(ListNode* pos)
{
	assert(pos);
	
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

//删除指定位置前的数据
void ListPopPosFront(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->prev != phead);

	ListNode* del = pos->prev;
	del->prev->next = pos;
	pos->prev = del->prev;
	free(del);
	del = NULL;
}

//删除指定位置之后的数据
void ListPopPosAfter(ListNode* phead, ListNode* pos)
{
	assert(phead);
	assert(pos && pos->next != phead);

	ListNode* del = pos->next;
	del->next->prev = pos;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//在指定位置前插入数据
void ListPushPosFront(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->next = pos;
	newnode->prev = pos->prev;

	pos->prev->next = newnode;
	pos->prev = newnode;
}

//在指定位置之后插入数据
void ListPushPosAfter(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = CreatNewnode(x);
	newnode->prev = pos;
	newnode->next = pos->next;

	pos->next->prev = newnode;
	pos->next = newnode;
}

//销毁链表
void ListDestroy(ListNode* phead)
{
	assert(phead);

	ListNode* pcur = phead->next;
	while (pcur != phead)
	{
		ListNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

双向链表完结撒花~~

相关推荐
A懿轩A28 分钟前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
半盏茶香33 分钟前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
️南城丶北离2 小时前
[数据结构]图——C++描述
数据结构··最小生成树·最短路径·aov网络·aoe网络
✿ ༺ ོIT技术༻2 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
字节高级特工2 小时前
【C++】深入剖析默认成员函数3:拷贝构造函数
c语言·c++
计算机学长大白3 小时前
C中设计不允许继承的类的实现方法是什么?
c语言·开发语言
XH华8 小时前
初识C语言之二维数组(下)
c语言·算法
菜鸡中的奋斗鸡→挣扎鸡9 小时前
滑动窗口 + 算法复习
数据结构·算法
axxy200010 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
Uu_05kkq11 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法