数据结构(笔记)——双向链表

1.双向链表定义

顾名思义,在双向链表的结点中有两个指针域,其一指向直接后继,另一指向直接前驱。

2.特点

项目 内容
定义 一种链式存储结构,每个结点包含 数据域前驱指针域(prio)后继指针域(next)
指针数量 每个结点有两个指针:一个指向前驱结点,一个指向后继结点
存储方式 逻辑上连续,物理存储不要求连续
访问方向 可以沿着 next 向后遍历,也可以沿着 prev 向前遍历
头结点特点 常设一个头结点,prio = NULL,方便统一操作
尾结点特点 最后一个结点的 next = NULL

3.双向链表的优缺点

优点 缺点
支持双向遍历,查找更灵活 每个结点需要额外存储一个前驱指针,空间开销大
删除、插入时,不必只依赖单方向指针 插入、删除时要维护两个指针,操作稍复杂
可以在 O(1) 时间找到前驱结点 实现比单链表复杂

4.实现代码

申请节点

cpp 复制代码
Node* _buynode(ElemType x)
{
	Node* s = (Node*)malloc(sizeof(Node));
	assert(s != NULL);
	s->data = x;
	s->next = s->prio = NULL;
	return s;
}

目的:申请并初始化一个新节点(数据域赋值,前后指针置空)。

实现步骤:

malloc(sizeof(Node)) 分配内存给一个 Node。

assert(s != NULL) 检查分配是否成功(失败时程序中止)。

s->data = x:把数据放入节点。

s->next = s->prio = NULL:初始化前驱和后继指针为空(后续插入时再设置指针)。

返回新节点指针。

时间复杂度:O(1)。

初始化

cpp 复制代码
void InitDList(List* list)
{
	Node* s = (Node*)malloc(sizeof(Node));
	assert(s != NULL);
	list->first = list->last = s;
	list->last->next = NULL;
	list->first->prio = NULL;
	list->size = 0;
}

目的:初始化链表(创建头结点、设定空表状态)。

实现步骤:

分配一个头结点 s。

把 list->first 和 list->last 都指向头结点(表示空表)。

list->last->next = NULL:头结点的 next 置 NULL(空表无有效节点)。

list->first->prio = NULL:头结点 prio 为 NULL。

list->size = 0:长度置 0。

时间复杂度:O(1)。

尾插

cpp 复制代码
void push_back(List* list, ElemType x)
{
	Node* s = _buynode(x);
	s->prio = list->last;
	list->last->next = s;
	list->last = s;
	list->size++;
}

目的:尾插一个新节点,把 x 放到链表末尾。

实现步骤:

创建新节点 s(next/prio 初始为 NULL)。

s->prio = list->last;:把新节点的前驱指向当前尾节点(头结点或某个有效节点)。

list->last->next = s;:旧尾节点的 next 指向新节点(若为空表则 list->last 为头结点)。

list->last = s;:把 last 更新为新节点。

list->size++:长度加一。

时间复杂度:O(1)。

头插

cpp 复制代码
void push_front(List* list, ElemType x)
{
	Node* s = _buynode(x);

	if (list->first == list->last)
	{
		// empty list
		list->last = s;
	}
	else
	{
		s->next = list->first->next;
		s->next->prio = s;
	}

	s->prio = list->first;
	list->first->next = s;

	list->size++;
}

目的:头插,把 x 插入到第一个有效节点之前(即头结点之后)。

实现步骤:

新建节点 s。

判断是否为空表(list->first == list->last):

空表:把 list->last = s;(新节点成为尾节点)。注意头结点的 next 还未设置。

非空:把 s->next = list->first->next;,并把原第一个节点的 prio 更新为 s。

s->prio = list->first;:新节点的前驱指向头结点。

list->first->next = s;:头结点的 next 指向新节点(完成插入)。

list->size++。

时间复杂度:O(1)。

显示数

cpp 复制代码
void show_list(List* list)
{
	Node* p = list->first->next;
	while (p != NULL)
	{
		printf("%d-->", p->data);
		p = p->next;
	}
	printf("Nul.\n");
}

目的:从头到尾遍历并打印所有有效节点的数据。

实现步骤:

从 list->first->next(第一个有效节点或 NULL)开始。

循环 while (p != NULL):打印 p->data,然后 p = p->next。

遍历结束打印结束标识。

时间复杂度:O(n)。

尾删

cpp 复制代码
void pop_back(List* list)
{
	if (list->size == 0)
		return;

	Node* p = list->first;
	while (p->next != list->last)
		p = p->next;

	free(list->last);
	list->last = p;
	list->last->next = NULL;
	list->size--;
}

目的:删除尾节点(最后一个有效节点)。

实现步骤:

如果空表直接返回。

从头结点 p = list->first 开始,遍历找到尾节点的前驱(找到 p 使 p->next == list->last)。

free(list->last):释放尾节点内存。

list->last = p:把前驱设为新的尾节点。

list->last->next = NULL:新的尾节点 next 置 NULL。

list->size--。

时间复杂度:O(n)(实现中用了线性查找前驱)。

头删

cpp 复制代码
void pop_front(List* list)
{
	if (list->size == 0)
		return;
	Node* p = list->first->next;

	if (list->first->next == list->last)
	{
		list->last = list->first;
		list->last->next = NULL;
	}
	else
	{
		p->next->prio = list->first;
		list->first->next = p->next;
	}
	free(p);
	list->size--;
}

目的:删除第一个有效节点(头结点之后的节点)。

实现步骤:

空表返回。

p = list->first->next:指向要删除的节点。

如果只有一个有效节点(first->next == last):

把 list->last = list->first;(变为空表)。

list->last->next = NULL。

否则(有多个节点):

把第二个节点的 prio 指向头结点(p->next->prio = list->first)。

把头结点的 next 指向第二个节点(list->first->next = p->next)。

free(p) 并 size--。

时间复杂度:O(1)。

插入值

cpp 复制代码
void insert_val(List* list, ElemType x)
{
	Node* p = list->first;
	while (p->next != NULL && p->next->data < x)
		p = p->next;

	if (p->next == NULL)
	{
		push_back(list, x);
	}
	else
	{
		Node* s = _buynode(x);
		s->next = p->next;
		s->next->prio = s;
		s->prio = p;
		p->next = s;
		list->size++;
	}
}

目的:按升序把 x 插入到合适位置(保持链表有序)。

实现步骤:

从头结点 p = first 开始,找到第一个 p 使得 p->next->data >= x 或者到达尾(p->next == NULL)。

如果到达尾(p->next == NULL),调用 push_back(尾插)。

否则在 p 与 p->next 之间插入新节点 s:

s->next = p->next;

s->next->prio = s;(设置后继的前驱指针)

s->prio = p;

p->next = s;

size++。

时间复杂度:O(n)(需要顺序查找插入位置)。

查找

cpp 复制代码
Node* find(List* list, ElemType key)
{
	Node* p = list->first->next;
	while (p != NULL && p->data != key)
		p = p->next;
	return p;
}

目的:查找第一个值为 key 的节点并返回其指针,找不到返回 NULL。

实现步骤:

从 list->first->next 开始线性查找,直到 p==NULL 或 p->data==key。

返回 p(可能为 NULL 或找到的节点)。

时间复杂度:O(n)。

长度

cpp 复制代码
int length(List* list)
{
	return list->size;
}

删除值

cpp 复制代码
void delete_val(List* list, ElemType key)
{
	if (list->size == 0)
		return;
	Node* p = find(list, key);
	if (p == NULL)
	{
		printf("此数据不存在。\n");
		return;
	}

	if (p == list->last)
	{
		list->last = p->prio;
		list->last->next = NULL;
	}
	else
	{
		p->next->prio = p->prio;
		p->prio->next = p->next;
	}

	free(p);
	list->size--;
}

目的:删除值等于 key 的节点(第一个匹配的节点),并释放内存。

实现步骤:

空表返回。

p = find(...) 查找目标节点;若 NULL 则提示并返回。

若 p 是尾节点:把 list->last 更新为 p->prio 并把新的 last->next = NULL。

否则(中间节点):把 p->next->prio 指向 p->prio;把 p->prio->next 指向 p->next(断开 p)。

free(p) 并 size--。

时间复杂度:O(n)(包含 find 的时间)。

排序

cpp 复制代码
void sort(List* list)
{
	if (list->size == 0 || list->size == 1)
		return;

	Node* s = list->first->next;
	Node* q = s->next;

	list->last = s;
	list->last->next = NULL;

	while (q != NULL)
	{
		s = q;
		q = q->next;

		Node* p = list->first;
		while (p->next != NULL && p->next->data < s->data)
			p = p->next;

		if (p->next == NULL)
		{
			s->next = NULL;
			s->prio = list->last;
			list->last->next = s;
			list->last = s;
		}
		else
		{
			s->next = p->next;
			s->next->prio = s;
			s->prio = p;
			p->next = s;
		}
	}
}

目的:对链表按 data 升序排序(实现为链式插入排序)。

实现步骤(算法思想):

特殊情况:0 或 1 个节点直接返回。

以链表的第一个节点 s 作为已排序部分(初始只有一个节点),q = s->next 指向未处理部分的第一个节点。

设置 list->last = s; list->last->next = NULL;------先把链表当作以 first 头结点和 s 作为已排序子链。

注意:这里并没有把 list->first->next 修改(它本来就是 s)。

进入主循环:对未排序节点 q 逐个取出为 s,并在已排序子链中找到合适位置 p(从头结点开始),然后插入 s:

若 p->next == NULL(插到末尾):把 s->next = NULL, s->prio = list->last; list->last->next = s; list->last = s;

否则插到 p 与 p->next 之间,同时更新 s->next, s->prio, p->next->prio, p->next。

循环直到 q == NULL(所有节点插完)。

时间复杂度:O(n²)(典型插入排序,最坏时每次都需遍历已排序序列)。

逆序

cpp 复制代码
void resver(List* list)
{
	if (list->size == 0 || list->size == 1)
		return;

	Node* p = list->first->next;
	Node* q = p->next;

	list->last = p;
	list->last->next = NULL;

	while (q != NULL)
	{
		p = q;
		q = q->next;

		p->next = list->first->next;
		p->next->prio = p;
		p->prio = list->first;
		list->first->next = p;
	}
}

目的:反转链表中有效节点(把原来顺序反转,头结点之后变为原尾)。

实现步骤(头插法反转):

空表或单节点直接返回。

p = first->next(原第一个),q = p->next(原第二)。

把 list->last = p; list->last->next = NULL;(最初把第一个节点设为新尾)。

然后对余下每个节点 q:

把 p = q; q = q->next;

把 p 插到头结点之后:p->next = first->next; p->next->prio = p; p->prio = first; first->next = p;

循环直到所有节点移动到头部顺序反转完成。

时间复杂度:O(n)。

清除

cpp 复制代码
void clear(List* list)
{
	if (list->size == 0)
		return;

	Node* p = list->first->next;
	while (p != NULL)
	{
		if (p == list->last)
		{
			list->last = list->first;
			list->last->next = NULL;
		}
		else
		{
			p->next->prio = list->first;
			list->first->next = p->next;
		}

		free(p);
		p = list->first->next;
	}
	list->size = 0;
}

目的:清空所有有效节点(保留头结点),释放内存并恢复为空表状态。

实现步骤:

若空表直接返回。

p = first->next 指向当前要删除的第一个有效节点。

在循环中:

如果 p 是尾节点:把 last = first 并 last->next = NULL(即变空)。

否则把 p->next->prio = first 并 first->next = p->next(把头结点指向下一个要删除节点)。

free(p),然后更新 p = first->next(下一次循环删除新头之后的节点)。

循环直到 p == NULL(没有有效节点)。

list->size = 0。

时间复杂度:O(n)。

销毁

cpp 复制代码
void destroy(List* list)
{
	clear(list);
	free(list->first);
	list->first = list->last = NULL;
}

目的:彻底销毁链表,释放头结点以及所有有效节点,最终把 first/last 置 NULL。

实现步骤:

调用 clear(list) 释放所有有效节点并把 size 置 0。

free(list->first):释放头结点内存。

list->first = list->last = NULL:避免野指针。

时间复杂度:O(n)(clear 的成本)。

相关推荐
程序猿乐锅2 小时前
【Tilas|第三篇】多表SQL语句
数据库·经验分享·笔记·学习·mysql
AOwhisky3 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes
爱编码的小八嘎3 小时前
C语言完美演绎9-7
c语言
澈2073 小时前
深耕进阶 Day1:C 与 C++ 核心差异 + C++ 入门基石
c语言·开发语言·c++
love530love3 小时前
Windows Podman Machine 虚拟硬盘迁移完整指南:从 C 盘到非系统盘
c语言·人工智能·windows·podman
嘻嘻哈哈樱桃3 小时前
牛客经典101题题解集--动态规划
java·数据结构·python·算法·职场和发展·动态规划
Felven3 小时前
C. Need More Arrays
c语言·开发语言
love530love3 小时前
Podman Machine 虚拟硬盘迁移实战二:用 Junction 把 vhdx 从 C 盘搬到其他盘
c语言·开发语言·人工智能·windows·wsl·podman·podman machine
脱氧核糖核酸__4 小时前
LeetCode热题100——234.回文链表(两种解法)
c++·算法·leetcode·链表