【数据结构与算法】数据结构初阶:详解顺序表和链表(五)——双向链表


🔥个人主页艾莉丝努力练剑

❄专栏传送门:《C语言》《数据结构与算法》C语言刷题12天IO强训LeetCode代码强化刷题

🍉学习方向:C/C++方向

⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平



**前言:**本篇文章,我们继续来看顺序表和链表相关的知识点,在初阶的数据结构与算法阶段,我们把知识点分成三部分,复杂度作为第一部分,顺序表和链表、栈和队列、二叉树为第二部分,排序为第二部分,我们之前已经介绍完了第一部分:算法复杂度,本文我们继续学习第二部分中的顺序表和链表部分内容啦。


**再次提醒:**为什么我们要学那么多的数据结构?这是因为没有一种数据结构能够去应对所有场景。我们在不同的场景需要选择不同的数据结构,所以数据结构没有谁好谁坏之分,而评估数据结构的好坏要针对场景,如果在一种场景下我们需要频繁地对头部进行插入删除操作,那么这个时候我们用链表;但是如果对尾部进行插入删除操作比较频繁,那我们用顺序表比较好。

因此,不同的场景我们选择不同的数据结构。


目录

正文

四、双向链表

(一)链表的分类

1、带头或者不带头

2、单向或者双向

3、循环或者不循环

(二)带头双向循环链表节点的概念与结构

1、概念

2、结构

3、双向链表为空

(三)实现双向链表

1、初始化

2、打印

3、遍历

4、判断链表是否为空

5、增删查改功能的实现

(1)尾插

(2)头插

(3)在指定位置之前插入数据

(4)在指定位置之后插入数据

(1)尾删

(2)头删

(3)删除pos位置节点

(1)查找

(1)修改

6、销毁链表

7、完整代码

8、优化版代码(考虑接口的一致性)

(1)初始化优化版

(2)销毁优化版

9、优化后的完整代码

五、顺序表和链表的对比分析

结尾


正文

四、双向链表

(一)链表的分类

链表并不是一个具体的结构,链表的结构非常多样,下面情况组合起来就有8(2*2*2)种链表结构:

1、带头或者不带头

带头链表中的头节点,不存储任何有效的数据,只用来占位子,"哨兵位" 。

在前面介绍单链表的时候,有的地方博主也会表述成"头节点",这个称呼是为了方便uu们理解,实际上把单链表中第一个节点称为"头节点"是错误的表述。

2、单向或者双向
3、循环或者不循环

虽然有这么多的链表结构,但是我们实际中最常用的还是两种结构:单链表和双向链表。

(二)带头双向循环链表节点的概念与结构

1、概念

**注意:**这里的"带头"跟前面我们说的"头结点"是两个概念,实际前面的在单链表阶段称呼并不严谨,但是为了uu们更好的理解就直接称为单链表的头结点。

2、结构

带头链表里的头节点,实际为**"哨兵位"**,哨兵位节点不存储任何有效元素,只是在这里"放哨的"。

双向链表中由一个一个的节点组成,这里的节点有三个组成部分------

cpp 复制代码
struct ListNode
{
	int data;
	struct ListNode* next;//指向后一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
}
3、双向链表为空

双向链表为空------指向自己

(三)实现双向链表

1、初始化

(1)List.h

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>

//定义双向链表结构
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;//指向后一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
}LTNode;

//要改变头节点plist------要用二级指针
//初始化
void LTInit(LTNode** pphead);

(2)List.c:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

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

(3)test.c:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);
}

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

调试一下,打开监视,进入函数(F11)------

像这样,初始化就完成了------

**补充:**博主并不建议把LTNode* plist改成LTNode plist(不用**),很多教材是这样写的,看起来很困难,还存在一个误导的作用。本身大家对指针的理解就很困难了,如果我们这样一会儿加一个一级指针一会儿定义成LTNode,那大家后续去使用是很混乱的,所以不建议这样使用。

而且关键是后面我们基本用到的都是一级指针,只需要一个*就好了,所以建议大家如果教材中有上面这种写法,不要用,我们就搞一个二级指针就好了。

2、打印

List.c:

cpp 复制代码
//打印
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

test.c:

cpp 复制代码
	LTPrint(plist);
3、遍历
4、判断链表是否为空

List.c:

cpp 复制代码
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
5、增删查改功能的实现
(1)尾插

哨兵位在双向链表始终不会发生改变------

plist本来指向0x100,有了值之后plist指向0x800。

List.c:

cpp 复制代码
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

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

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
}

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

尾插的时间复杂度:O(1) 。

打开监视,观察一下尾插------

(2)头插

List.c:

cpp 复制代码
//头插
void LTPushFront(LTNode* phead, LTDataType x) 
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

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

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	//LTPushBack(plist, 1);
	//LTPushBack(plist, 2);
	//LTPushBack(plist, 3);
	//LTPushBack(plist, 4);
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
}

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

头插的时间复杂度:O(1) 。

打开监视看一下------

(3)在指定位置之前插入数据

pos不可能指向phead,因为phead不存储任何有效数据(LTFind找不到)。

List.c:

cpp 复制代码
//在pos位置之前插入节点
void LTInsertBefore(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos->prev newnode pos
	newnode->prev = pos->prev;
	newnode->next = pos;

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

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,1);

	LTInsertBefore(pos, 100);
	LTPrint(plist);
}

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

在指定位置之前插入 数据时间复杂度:O(N)。

测试一下------

(4)在指定位置之后插入数据

List.c:

cpp 复制代码
//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x)//任意位置的插入
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	LTInsert(pos, 100);
	LTPrint(plist);
}

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

在指定位置之后插入 数据时间复杂度:O(1)。

这里如果给个这个位置------

cpp 复制代码
LTNode* pos = LTFind(plist,4);

就相当于尾插,头插行不行呢?头插是在头节点之后插入,不行,这里只能给1。

(1)尾删

List.c:

cpp 复制代码
//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
	//其实置不置为空都没关系,这里执行到这里程序已经结束了,不会再用到del了
	//这样写无非是养成一个好习惯
}

这里其实置不置为空都没关系,这里执行到这里程序已经结束了,不会再用到del了,我们这样写无非是养成一个好习惯而已。

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//LTPushFront(plist, 1);
	//LTPushFront(plist, 2);
	//LTPushFront(plist, 3);
	//LTPushFront(plist, 4);

	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
	LTPopBack(plist);
	LTPrint(plist);
}

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

尾删 的时间复杂度:O(1) 。

这里再删一次就会断言报错了(我们的期望)。

(2)头删

List.c:

cpp 复制代码
//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
	LTPopFront(plist);
	LTPrint(plist);
}

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

头删 的时间复杂度:O(1) 。

再删一次就会断言报错。

(3)删除pos位置节点

这里没有传phead。

List.c:

cpp 复制代码
//删除pos位置的节点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	LTErase(pos);
	LTPrint(plist);
}

int main()
{
	test01();
	return 0;
}
(1)查找

如果找到了,把这个节点的指针返回,既然是查找,无非就是遍历;

如果没找到,返回一个NULL就好了。

List.c:

cpp 复制代码
//在双向链表里面查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,4);
	if (pos)
	{
		printf("找到了!\n");
	}
	else
	{
		printf("未找到!\n");
	}
}

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

测试一下------

我们来测试一下查找一个链表中没有的值------

(1)修改

修改就很简单了,我们不做过多介绍。

List.c:

cpp 复制代码
//修改
void LTChange(LTNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}

test.c:

cpp 复制代码
#include"List.h"

void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	//修改
	LTChange(pos, 100);
	LTPrint(plist);}

int main()
{
	test01();
	return 0;
}
6、销毁链表

哨兵位phead也要销毁,传二级指针**pphead。

List.c:

cpp 复制代码
//销毁
void LTDestory(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while(pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(*pphead);
	*pphead = NULL;
}

test.c:

cpp 复制代码
void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	LTErase(pos);
	LTPrint(plist);

	//销毁
	LTDestory(&plist);
}

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

打开监视观察一下------

7、完整代码

(1)List.h

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义双向链表结构
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;//指向后一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
}LTNode;

//要改变头节点plist------要用二级指针
//初始化
void LTInit(LTNode** pphead);
//销毁
void LTDestory(LTNode** pphead);

//在双向链表中,增删查改都不会改变哨兵位节点
//所以我们要用一级指针,因为我们不改变phead------plist指针
//尾插
void LTPushBack(LTNode* phead, LTDataType x);

//头插
void LTPushFront(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//打印
void LTPrint(LTNode* phead);

//头删
void LTPopFront(LTNode* phead);

//在双向链表里面查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos位置之前插入节点
void LTInsertBefore(LTNode* pos, LTDataType x);

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);//任意位置的插入

//删除pos位置的节点
void LTErase(LTNode* pos);

//修改
void LTChange(LTNode* pos, LTDataType x);

(2)List.c:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//第一版初始化
void LTInit(LTNode** pphead)
{
	//*pphead = (LTNode*)malloc(sizeof(LTNode));
	//if (*pphead == NULL)
	//{
	//	perror("malloc fail!");
	//	exit(1);
	//}
	//(*pphead)->data = -1;
	//(*pphead)->next = (*pphead)->prev = *pphead;
	*pphead = LTBuyNode(-1);//申请一个哨兵位节点
}

//销毁
void LTDestory(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while(pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头节点
	free(*pphead);
	*pphead = NULL;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

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

//头插
void LTPushFront(LTNode* phead, LTDataType x) 
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

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

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

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
	//其实置不置为空都没关系,这里执行到这里程序已经结束了,不会再用到del了
	//这样写无非是养成一个好习惯
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

//在双向链表里面查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

//在pos位置之前插入节点
void LTInsertBefore(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos->prev newnode pos
	newnode->prev = pos->prev;
	newnode->next = pos;

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

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x)//任意位置的插入
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

//删除pos位置的节点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

//修改
void LTChange(LTNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}

(3)test.c:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//LTPushFront(plist, 1);
	//LTPushFront(plist, 2);
	//LTPushFront(plist, 3);
	//LTPushFront(plist, 4);
	// 
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	LTNode* pos = LTFind(plist,2);
	//if (pos)
	//{
	//	printf("找到了!\n");
	//}
	//else
	//{
	//	printf("未找到!\n");
	//}
	// 
	//LTInsertBefore(pos, 100);
	//LTPrint(plist);
	//LTInsert(pos, 100);
	//LTPrint(plist);
	//LTErase(pos);
	//LTPrint(plist);
	//修改
	LTChange(pos, 100);
	LTPrint(plist);

	//销毁
	LTDestory(&plist);
}

int main()
{
	test01();
	return 0;
}
8、优化版代码(考虑接口的一致性)

我们的程序是要给使用用户使用的。

考虑到接口的一致性,即一级指针就全部一级指针,二级指针就全部二级指针,一会儿一级指针一会儿又二级指针,会增加使用用户的记忆成本。

(1)初始化优化版

List.c:

cpp 复制代码
//优化版初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

test.c:

cpp 复制代码
void test01()
{
	//LTNode* plist = NULL;
	//LTInit(&plist);
	LTNode* plist = LTInit();//创建一个指针来接收初始化的返回值

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	//修改
	LTChange(pos, 100);
	LTPrint(plist);

	//销毁
	LTDestory(&plist);
}

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

现在初始化优化完了,满足了接口的一致性。

(2)销毁优化版

List.c:

cpp 复制代码
//优化版销毁
void LTDestroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(phead);
	phead = NULL;
}

空间全部释放完plist会变野指针,最后实参会变成一个随机数,我们要把实参手动置为空。

test.c:

cpp 复制代码
void test01()
{
	//LTNode* plist = NULL;
	//LTInit(&plist);
	LTNode* plist = LTInit();//创建一个指针来接收初始化的返回值

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);

	LTNode* pos = LTFind(plist,2);

	//修改
	LTChange(pos, 100);
	LTPrint(plist);


	//优化版销毁
	LTDestroy(plist);
	plist = NULL;
}

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

优化的部分就是原先用了二级指针的初始化销毁

9、优化后的完整代码
(1)List.h
cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义双向链表结构
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;//指向后一个节点的指针
	struct ListNode* prev;//指向前一个节点的指针
}LTNode;

//优化版初始化
LTNode* LTInit();

//优化版销毁------为了保持接口一致性
void LTDestroy(LTNode* phead);

//在双向链表中,增删查改都不会改变哨兵位节点
//所以我们要用一级指针,因为我们不改变phead------plist指针
//尾插
void LTPushBack(LTNode* phead, LTDataType x);

//头插
void LTPushFront(LTNode* phead, LTDataType x);

//尾删
void LTPopBack(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//打印
void LTPrint(LTNode* phead);

//头删
void LTPopFront(LTNode* phead);

//在双向链表里面查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos位置之前插入节点
void LTInsertBefore(LTNode* pos, LTDataType x);

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x);//任意位置的插入

//删除pos位置的节点
void LTErase(LTNode* pos);

//修改
void LTChange(LTNode* pos, LTDataType x);
(2)List.c:
cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//优化版初始化
LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

//优化版销毁
void LTDestroy(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(phead);
	phead = NULL;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

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

//头插
void LTPushFront(LTNode* phead, LTDataType x) 
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

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

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

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
	//其实置不置为空都没关系,这里执行到这里程序已经结束了,不会再用到del了
	//这样写无非是养成一个好习惯
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

//在双向链表里面查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

//在pos位置之前插入节点
void LTInsertBefore(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos->prev newnode pos
	newnode->prev = pos->prev;
	newnode->next = pos;

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

//在pos位置之后插入节点
void LTInsert(LTNode* pos, LTDataType x)//任意位置的插入
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

//删除pos位置的节点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

//修改
void LTChange(LTNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}
(3)test.c:
cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include"List.h"

void test01()
{
	LTNode* plist = LTInit();//创建一个指针来接收初始化的返回值

	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);
	//LTPushFront(plist, 1);
	//LTPushFront(plist, 2);
	//LTPushFront(plist, 3);
	//LTPushFront(plist, 4);
	// 
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//LTPopBack(plist);
	//LTPrint(plist);
	//
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	//LTPopFront(plist);
	//LTPrint(plist);
	LTNode* pos = LTFind(plist,2);
	//if (pos)
	//{
	//	printf("找到了!\n");
	//}
	//else
	//{
	//	printf("未找到!\n");
	//}
	// 
	//LTInsertBefore(pos, 100);
	//LTPrint(plist);
	//LTInsert(pos, 100);
	//LTPrint(plist);
	//LTErase(pos);
	//LTPrint(plist);
	//修改
	LTChange(pos, 100);
	LTPrint(plist);

	//优化版销毁
	LTDestroy(plist);
	plist = NULL;
}

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

五、顺序表和链表的对比分析

详见下图------


结尾

那么到这里,我们【初阶数据结构】中的【顺序表与链表】这一part就全部介绍完了,内容不少,大家要及时回顾、消化。

顺序表、链表的数据结构和下面我们将要介绍的栈和队列 密不可分,马虎不得。

往期回顾:

【数据结构与算法】数据结构初阶:详解顺序表和链表(四)------单链表(下)

【数据结构与算法】数据结构初阶:详解顺序表和链表(三)------单链表(上)

本期内容需要回顾的C语言知识如下面的截图中所示(指针博主写了6篇,列出来有水字数嫌疑了,就只放指针第六篇的网址,博主在指针(六)把指针部分的前五篇的网址都放在【往期回顾】了,点击【传送门】就可以看了)。

大家如果对前面部分的知识点印象不深,可以去上一篇文章的结尾部分看看,博主把需要回顾的知识点相关的博客的链接都放在上一篇文章了,上一篇文章的链接博主放在下面了:

【数据结构与算法】数据结构初阶:详解顺序表和链表(三)------单链表(上)

**结语:**本篇文章到这里就结束了,对数据结构的双向链表知识感兴趣的友友们可以在评论区留言,博主创作时可能存在笔误,或者知识点不严谨的地方,大家多担待,如果大家在阅读的时候发现了行文有什么错误欢迎在评论区斧正,再次感谢友友们的关注和支持!

相关推荐
amazinging1 分钟前
北京-4年功能测试2年空窗-报培训班学测开-第五十四天
python·学习·面试
baidu_1627115927 分钟前
linux 的list_for_each_entry
数据结构·list
java1234_小锋2 小时前
【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 微博类别信息爬取
开发语言·python·flask
shdbdndj2 小时前
C语言:预处理
c语言·预处理
程序员JerrySUN3 小时前
深度理解 KVM:Linux 内核系统学习的重要角度
java·linux·学习
BTU_YC3 小时前
Neo4j Python 驱动库完整教程(带输入输出示例)
开发语言·python·neo4j
曾几何时`3 小时前
分别使用Cypher与python构建neo4j图谱
开发语言·python·机器学习
Eli_Optics3 小时前
《通信原理》学习笔记——第三章
笔记·学习
请站在我身后3 小时前
无声视频自动配音效,开源模型thinksound 和mmaudio复现
人工智能·深度学习·算法·计算机视觉·aigc
屁股割了还要学4 小时前
【C语言进阶】题目练习(2)
c语言·开发语言·c++·学习·算法·青少年编程