数据结构之双向链表

链表的分类

单向或双向

单向的只有next,只能从头遍历到尾;
双向的既有next 也有prve,对于双向的我们既可以从头遍历,也可以从尾遍历到头
在这里插入图片描述

带头或不带头

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

在这里插入图片描述

循环和不循环

尾节点的next指针不为空的是循环链表,为空的则为不循环链表
在这里插入图片描述
总共222种(8种)。
单链表的全称:单向不带头不循环链表
双向链表全称:双向带头循环链表

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

双向链表的理解

双向链表的结构虽然比单向链表的结构复的多,但是接口的实现要比单向链表简单
双向链表中的哨兵位的prev节点和尾节点的next节点是相互指的,因为双向链表是循环链表
双向链表的结构是由:数据 + 指向后一个节点的指针 + 指向前一个节点的指针 组成的
双向链表的各种功能的实现:
双向链表的定义

核心代码:

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

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

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

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;
}

双向链表的初始化

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

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

尾插

尾插思路图

核心代码:

//第一个参数传一级还是二级,看的是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;

}

头插

头插思路图:
在这里插入图片描述

核心代码:

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;
}

判断双向链表是否为空

核心代码:

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

尾删

尾删思路图
在这里插入图片描述
核心代码:

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;
}

头删

头删思路图
在这里插入图片描述

核心代码:

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;
}

查找

核心代码:

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;
}

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

再指定位置之后插入

思路图:
在这里插入图片描述

核心代码:

	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;

}

在指定位置之前插入节点

思路图:
在这里插入图片描述

核心代码:

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;
}

删除指定位置节点

思路图:
在这里插入图片描述

核心代码:

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;

}

双向链表销毁

思路图:
在这里插入图片描述
核心代码:

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;

}

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

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

销毁优化:

void LTDestroy2(LTNode* phead)
{
	assert(phead);

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

初始化优化:

LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

顺序表和链表分析

在这里插入图片描述
没有绝对的谁好,只是应用的场景不同,存在即合理!

### 链表中哨兵节点的概念及作用 #### 哨兵节点的定义 哨兵节点是一种特殊的链表节点,通常用于简化链表的操作逻辑。它不存储实际的数据,而是作为一个辅助节点来帮助管理链表结构[^2]。 #### 哨兵节点的作用 1. **简化边界条件** 使用哨兵节点可以避免许多特殊情况下的额外判断。例如,在单链表头部插入新节点时,如果没有哨兵节点,则需要单独处理头指针的变化;而有了哨兵节点后,所有的插入操作都可以按照统一的方式进行。 2. **保持一致性** 哨兵节点使得链表中的每一个有效节点都具有前驱和后继节点(对于双向链表而言),从而减少了对特殊位置(如首尾节点)的特别处理需求。 3. **防止空链表问题** 在某些实现方式下,链表总是包含两个固定的哨兵节点(头哨兵和尾哨兵),这确保了即使链表为空,也至少有两个节点存在,进一步降低了复杂度[^1]。 --- ### 哨兵节点的具体实现 以下是基于 Python 的单向链表双向链表中使用哨兵节点的例子: #### 单向链表中的哨兵节点实现 ```python class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def create_sentinel_linked_list(): sentinel = ListNode() # 创建哨兵节点 current = sentinel values = [1, 2, 3] for value in values: node = ListNode(value) current.next = node current = current.next return sentinel # 返回哨兵节点 # 测试函数 sentinel_node = create_sentinel_linked_list() current = sentinel_node.next while current is not None: print(current.val) current = current.next ``` 上述代码通过创建一个哨兵节点 `ListNode()` 来初始化链表,并将其作为链表的一部分。这样可以在后续操作中无需担心链表为空的情况。 --- #### 双向链表中的哨兵节点实现 ```python class DoublyListNode: def __init__(self, val=0, prev=None, next=None): self.val = val self.prev = prev self.next = next def create_doubly_sentinel_linked_list(): head_sentinel = DoublyListNode() # 头哨兵节点 tail_sentinel = DoublyListNode() # 尾哨兵节点 head_sentinel.next = tail_sentinel tail_sentinel.prev = head_sentinel values = [1, 2, 3] current = head_sentinel for value in values: node = DoublyListNode(value) node.prev = current node.next = current.next current.next.prev = node current.next = node current = current.next return head_sentinel, tail_sentinel # 测试函数 head_sentinel, tail_sentinel = create_doubly_sentinel_linked_list() current = head_sentinel.next while current != tail_sentinel: print(current.val) current = current.next ``` 在双向链表中,除了普通的前后链接关系外,还引入了头哨兵和尾哨兵节点,它们分别位于链表的两端并相互连接,形成闭环结构[^4]。 --- ### 经典应用场景 一些经典的 LeetCode 题目展示了如何利用哨兵节点解决复杂的链表问题: - **反转单链表 (LeetCode 206)**:可以通过设置哨兵节点简化翻转过程中的指针调整[^3]。 - **合并两个有序链表 (LeetCode 21)**:借助哨兵节点构建一个新的结果链表,减少对初始状态的特判。 - **删除链表倒数第 N 个节点 (LeetCode 19)**:双指针法配合哨兵节点能够更清晰地定位目标节点的位置。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值