数据结构之双向链表

链表的分类

单向或双向

单向的只有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;
}

顺序表和链表分析

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

相关推荐
乌萨奇也要立志学C++3 小时前
【洛谷】二叉树专题全解析:概念、存储、遍历与经典真题实战
数据结构·c++·算法
MOONICK5 小时前
数据结构——红黑树
数据结构
(●—●)橘子……5 小时前
记力扣2271.毯子覆盖的最多白色砖块数 练习理解
数据结构·笔记·python·学习·算法·leetcode
做运维的阿瑞5 小时前
Python 面向对象编程深度指南
开发语言·数据结构·后端·python
new coder7 小时前
[算法练习]第三天:定长滑动窗口
数据结构·算法
晨非辰7 小时前
《从数组到动态顺序表:数据结构与算法如何优化内存管理?》
c语言·数据结构·经验分享·笔记·其他·算法
筱砚.8 小时前
【数据结构——十字链表】
网络·数据结构·链表
坚持编程的菜鸟9 小时前
LeetCode每日一题——重复的子字符串
数据结构·算法·leetcode
williamdsy13 小时前
【MoonBit初探】:从一个“陷阱”到深入理解数据结构*
数据结构·map·moonbit