数据结构之双向链表

链表的分类

单向或双向

单向的只有next,只能从头遍历到尾;

双向的既有next 也有prve,对于双向的我们既可以从头遍历,也可以从尾遍历到头

带头或不带头

我们前面学的单链表是不带头的 ,前面提到的头节点只是为了让我们好理解,才这样做的;
真正的带头链表也叫头节点,又称哨兵位,是用来占位子的,
因此,在带头链表中,除了头节点,其它节点都存储有效的数据。

循环和不循环

尾节点的next指针不为空的是循环链表,为空的则为不循环链表

总共22 2种(8种)。
单链表的全称:单向不带头不循环链表
双向链表全称:双向带头循环链表

链表的种类虽然很多,我们学习以上这两种即可,其余的我们可以通过举一反三自己就能实现。

双向链表的理解

双向链表的结构虽然比单向链表的结构复的多,但是接口的实现要比单向链表简单

双向链表中的哨兵位的prev节点和尾节点的next节点是相互指的,因为双向链表是循环链表
双向链表的结构是由:数据 + 指向后一个节点的指针 + 指向前一个节点的指针 组成的

双向链表的各种功能的实现:

双向链表的定义

核心代码:

c 复制代码
//双向链表的定义
typedef  int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

////使用这种方式也可以重命名:
//typedef struct ListNode LTNode;

双向链表向内存申请新节点

c 复制代码
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//记得判断是否申请失败
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);//等价于return 1;
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;//双向链表为循环链表,自己指向自己
	
	return newnode;
}

双向链表的初始化

c 复制代码
//双向链表的初始化
//链表中只有一个头节点的情况下,才叫双向链表为空;若连头节点都没有,那它是单链表
void LTInit(LTNode** pphead)//头节点发生改变,传的时二级指针

{
	//创建一个新节点作为头节点, -1代表头节点的无效值
	 *pphead = LTBuyNode(-1);
}

尾插

尾插思路图

核心代码:

c 复制代码
//第一个参数传一级还是二级,看的是pphead指向的节点是否会发生变化
// 如果发生变化,那pphead的改变需要影响到实参,需要传二级
// 如果不发生变化,那pphead不会影响实参,传一级
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);//穿的变量是有效的,哨兵位不能为空,所以这里不能为空
	//1.创建新节点
	LTNode* newnode = LTBuyNode(x);
	//2.找到受到影响的节点,与新节点进行尾插
	//phead  phead->prev newnode  先改变newnode的指向,再去改变其他的
	newnode->next = phead;
	newnode->prev = phead->prev;
	////方法1:
	//phead->prev->next = newnode;
	//phead->prev = newnode;

	//方法2:用中间变量,实现
	LTNode* Tail = phead->prev;
	Tail->next = newnode;
	phead->prev = newnode;

}

头插

头插思路图:

核心代码:

c 复制代码
void LTPushFrant(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//和尾插步骤差不多,不懂得,可以去看看尾插的注释
	//phead  phead->next  newnode
	newnode->next = phead->next;
	newnode->prev = phead;
	////方法1:
	//phead->next->prev = newnode;
	//phead->next = newnode;

	//方法2:
	LTNode* tmp = phead->next;
	phead->next = newnode;
	tmp->prev = newnode;
}

判断双向链表是否为空

核心代码:

c 复制代码
bool LTEmpty(LTNode* phead)//用来判断链表是否为空的
{
	assert(phead);//双向链表的头节点不能为空
	return phead->next == NULL;//如果下一个节点为空,则说明链表为空,条件成立,返回true,反之返回false
}

尾删

尾删思路图

核心代码:

c 复制代码
void LTPopBack(LTNode* phead)
{
	//0.通过断言判断,传的参数是否为空,双向链表的头节点不能为空;其次双向链表不能为空,若为空头删谁去呀
	assert(phead);
	assert(!LTEmpty(phead));//若LTEmpty返回的是true,通过 ! 转成false,断言报错,反之正常运行
	//1.找到删除的节点 和删除该节点后受到影响的节点并用临时变量保存,先将受到影响的节点的指向修改,最后删除要删除的节点
	LTNode* del = phead->prev;
	LTNode* prev = del->prev;

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

	free(del);
	del = NULL;
}

头删

头删思路图

核心代码:

c 复制代码
void LTPopFrant(LTNode* phead)
{
	//头删这里的思路和前面尾删的思路是大差不差的,可以去看上面的尾删的注释
	assert(phead);
	assert(!LTEmpty(phead));

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

	free(del);
	del = NULL;
}

查找

核心代码:

c 复制代码
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;
}

//有了查找这个功能,我们就可以在任意位置之后\之前插入删除数据了,我们这里先实现在位置之后的,之前的后续会实现

再指定位置之后插入

思路图:

核心代码:

c 复制代码
	void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//pos位置不能传空
	//1.创建新节点
	LTNode* newnode = LTBuyNode(x);
	//2.找到受到影响的节点,与新节点进行尾插
	//pos newnode pos->next  先改变newnode的指向,再去改变其他的

	newnode->next = pos->next;
	newnode->prev = pos;
	//方法1:
	pos->next->prev = newnode;
	pos->next = newnode;

	////方法2:
	//LTNode* tmp = pos->next;
	//pos->next = newnode;
	//tmp->prev = newnode;

}

在指定位置之前插入节点

思路图:

核心代码:

c 复制代码
void LTInBefor(LTNode* pos, LTDataType x)
{
	assert(pos);

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

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

删除指定位置节点

思路图:

核心代码:

c 复制代码
void LTErase(LTNode* pos)
{
	assert(pos);

	//pos->prev pos pos->next --- 先将删除pos后受到影响的两个节点的指向进行修改,再将pos节点进行删除
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;

}

双向链表销毁

思路图:

核心代码:

c 复制代码
void LTDestroy(LTNode** pphead)
//销毁传的是二级指针,因为最后我们要将头节点也要进行销毁
//创建指针遍历pcur指向*pphead的下一个指针,进行依次销毁之前,先对pcur的下一个指针用Next进行保存,
// 方便后续继续往后销毁,再进行销毁操作,因为我们传的是二级指针,所以我们最后要对*pphead进行手动销毁
{
	LTNode* pcur = (*pphead)->next;//这里的-> 权限比*高,这里要加上括号
	while (pcur != *pphead)
	{
		LTNode* Next = pcur->next;
		free(pcur);
		pcur = Next;
	}
	*pphead = NULL;
	pcur = NULL;

}

保持接口的一致性后代码优化

为了保持接口的一致性,将初始化和销毁优化接口都为一级指针(接口:指的是实现功能的方法)

以下是优化后的核心代码:

销毁优化:

c 复制代码
void LTDestroy2(LTNode* phead)
{
	assert(phead);

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

初始化优化:

c 复制代码
LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

顺序表和链表分析

没有绝对的谁好,只是应用的场景不同,存在即合理!

相关推荐
Q741_14713 小时前
C++ 面试高频考点 链表 迭代 递归 力扣 25. K 个一组翻转链表 每日一题 题解
c++·算法·链表·面试·递归·迭代
_fairyland13 小时前
数据结构 力扣 练习
数据结构·考研·算法·leetcode
沪漂的码农14 小时前
C语言队列与链表结合应用完整指南
c语言·链表
点云SLAM15 小时前
算法与数据结构之二叉树(Binary Tree)
数据结构·算法·二叉树·深度优先·广度优先·宽度优先
小龙报15 小时前
《算法通关指南:算法基础篇 --- 一维前缀和 — 1. 【模板】一维前缀和,2.最大子段和》
c语言·数据结构·c++·算法·职场和发展·创业创新·visual studio
.柒宇.17 小时前
力扣hoT100之找到字符串中所有字母异位词(java版)
java·数据结构·算法·leetcode
YoungHong199218 小时前
面试经典150题[063]:删除链表的倒数第 N 个结点(LeetCode 19)
leetcode·链表·面试
王璐WL18 小时前
【数据结构】单链表的经典算法题
数据结构·算法
Zzzzmo_18 小时前
Java数据结构:二叉树
java·数据结构·算法
聆风吟º19 小时前
【数据结构入门手札】数据结构基础:从数据到抽象数据类型
数据结构·数据类型·逻辑结构·数据对象·物理结构·数据项·数据元素