数据结构:双向链表(1~2)

目录

前言

一、双向链表概念与结构

双向链表概念

带头双向循环链表

双向链表结构

二、实现双向链表

1.双向链表的初始化

代码逐行解析

​编辑

2.双向链表的尾插

创建节点

3.双向链表的头插

4.双向链表的尾删

双向链表的判空

5.双向链表的头删

6.双向链表的销毁

借助现有实现测试:

7.双向链表查找

8.双向链表在指定位置插入

双向链表在指定位置之后插入

双向链表在指定位置之前插入

9.双向链表指定位置删除

10.总代码展示:(加入了测试代码)

三、顺序表与链表的分析

一、相同点

二、不同点(核心差异)

三、关键结论

四、链表算法题

一、移除链表元素

二、反转链表

总结

前言

本篇文章将讲解双向链表概念与结构,实现双向链表,顺序表与链表的分析,链表算法题等知识的相关内容,为本章节知识的内容。

一、双向链表概念与结构

双向链表概念

双向链表是一种链式存储的数据结构,每个节点包含两个指针:一个指向前驱节点(prior),一个指向后继节点(next),同时包含数据域(data)存储数据。这种结构允许双向遍历(从头到尾或从尾到头),并支持更灵活的插入、删除操作,但相比单链表会增加一定的空间开销(额外的指针域)。

图示:(不带头双向循环链表)

双向链表不仅能找到当前节点的下一个节点还可以找到当前节点的上一个节点,使用起来是很方便的。

因为刚刚讲解过的单链表为单向不带头不循环链表,目前还没有讲过带头形式的,所以本双链表为

带头双向循环链表。

带头双向循环链表

带头双向循环链表 是一种兼具"头节点""双向指针""循环结构"三大特性的链表,是应用最广泛的双向链表类型。其结构稳定、边界处理简单,支持高效的插入、删除和双向遍历操作。

对于上面头结点讲解:

  • 带头链中的头节点,是不存储任何有效数据,只用来站岗放哨,我们可称之为"哨兵位"
  • 在单链表的学习中,我们有时候也会把第一个节点表述为头节点,其实这个称呼是不严谨的:
  • 按照定义来说,严谨的定义:头节点 是链表中第一个节点 ,但不存储有效数据 (部分场景可存储链表长度等元信息),其核心价值是简化边界操作(如插入/删除首节点时无需特殊判断)。

双向链表结构

根据前文知识讲解,以及前面单向不带头不循环链表 的知识,我们来实现下双向链表结构:

cpp 复制代码
typedef int type;
typedef struct ListNode
{
	type data;
	//前驱指针,指向前一个指针
	struct ListNode* prev;
	//后继指针,指向后一个指针
	struct ListNode* next;
}ListNode;

二、实现双向链表

1.双向链表的初始化

我们在双向链表中头节点(可叫哨兵位)是需要初始化一下的,数据域可以存任意的数据,前驱指针和后继指针都指向自己即可。

函数形式:

void LTInit(ListNode** h);

实现:

cpp 复制代码
void LTInit(ListNode** h)
{     
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	*h = ph;
	(*h)->data = -1;
	(*h)->next = *h;
	(*h)->prev = *h;
}

细讲:

代码逐行解析
代码行 功能说明
ListNode* ph = (ListNode*)malloc(sizeof(ListNode)); 为头节点分配内存空间(哨兵位节点)。
if (ph == NULL) { perror("malloc fail!"); exit(1); } 检查内存分配是否成功,失败则报错并终止程序。
*h = ph; 将传入的二级指针 h 指向新创建的头节点(即外部头指针指向哨兵位节点)。
(*h)->data = -1; 给哨兵位节点的 data 赋值 -1(通常无实际意义,仅作标记)。
(*h)->next = *h; 头节点的 next 指针指向自身(形成循环)。
(*h)->prev = *h; 头节点的 prev 指针指向自身(双向循环)。

初始图示:

cpp 复制代码
void test()
{
	ListNode* h;
	LTInit(&h);

}

int main()
{
	test();
}

2.双向链表的尾插

双向链表尾插 是指在链表的 尾部(最后一个有效节点之后) 插入新节点。对于 带头节点的双向循环链表 ,尾插可直接通过头节点的 prev 指针定位尾节点,无需遍历链表,时间复杂度为 O(1)

void LTPushBack(ListNode* h, type x)

不过,该函数为实现尾插入,需要插入一个新节点,但传入的参数为type,需要先将type类型转化为ListNode*类型,所以,应有下面函数:

创建节点

ListNode* LTcreat(type x)

cpp 复制代码
ListNode* LTcreat(type x)
{
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	ph->data = x;
	ph->next = ph;
	ph->prev = ph;
	return ph;
}
void LTPushBack(ListNode* h, type x)
{     
	ListNode* p = LTcreat(x);
	p->next = h;
	p->prev = h->prev;
	h->prev->next = p;
	h->prev = p;
}

讲解:

LTcreat 函数:创建新节点(含哨兵位初始化)

  • 功能 :创建一个新节点,初始化 datax,并让 nextprev 指针自指(形成循环)。
  • 用途
    1. 可用于 初始化链表的哨兵位头节点 (此时 x 通常为无意义值,如 -1);
    2. 也可用于 创建普通有效节点 (此时 x 为实际数据)。

LTPushBack 函数:尾插操作

  • 核心逻辑 :通过 LTcreat(x) 创建新节点 p,并插入到链表尾部(头节点 hprev 位置)。
  • 步骤拆解
    1. ListNode* p = LTcreat(x); → 创建新节点 pp->nextp->prev 初始指向自身)。
    2. p->next = h; → 新节点 pnext 指向头节点 h(保持循环)。
    3. p->prev = h->prev; → 新节点 pprev 指向原尾节点(h->prev 是原尾节点)。
    4. h->prev->next = p; → 原尾节点的 next 指向新节点 p
    5. h->prev = p; → 头节点 hprev 指向新节点 p(更新尾节点为 p)。
cpp 复制代码
 newNode->prev = h->prev;    // 新节点 prev 指向原尾节点
    newNode->next = h;          // 新节点 next 指向头节点
    h->prev->next = newNode;    // 原尾节点 next 指向新节点
    h->prev = newNode;          // 头节点 prev 更新为新节点(新尾节点)

图示:

3.双向链表的头插

头插 是双向链表中最常用的操作之一,指将新节点插入到 头节点之后、第一个有效节点之前 的位置。适用于需频繁在头部添加数据的场景。

根据上面所说,我们可以知晓一点:插入到 头节点之后、第一个有效节点之前 的位置,所以实现上要考虑考虑:

void LTPushFront(ListNode* h, type x)

cpp 复制代码
ListNode* LTcreat(type x)
{
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	ph->data = x;
	ph->next = ph;
	ph->prev = ph;
	return ph;
}   
void LTPushFront(ListNode* h, type x)
{
	ListNode* p = LTcreat(x);
	p->next = h->next;
	p->prev = h;
	h->next->prev = p;
	h->next = p;
}

细讲一下:

cpp 复制代码
ListNode* LTcreat(type x) {
    // 1. 动态内存分配(关键:检查 malloc 失败场景)
    ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
    if (ph == NULL) { // 工程化必备:处理内存分配失败
        perror("malloc fail!"); // 打印错误原因(如内存不足)
        exit(1); // 终止程序(或返回 NULL,视场景而定)
    }
    // 2. 初始化节点数据与指针
    ph->data = x;       // 存储有效数据
    ph->next = ph;      // 初始 next 指向自身(循环结构基础)
    ph->prev = ph;      // 初始 prev 指向自身(循环结构基础)
    return ph;          // 返回新节点地址
}

带头节点的双向循环链表 头部插入新节点(头插)。
前提 :链表已初始化(头节点 h 存在,且 h->nexth->prev 初始指向自身,即空链表状态)。

完整头插流程(以空链表插入第一个节点为例)

假设头节点 h 已通过 LTcreat(-1) 创建(-1 为哨兵位无效数据),插入第一个有效节点 x=10

  1. 创建新节点ListNode* p = LTcreat(10);

    • LTcreat 分配内存并初始化 p->data=10p->next=pp->prev=p
  2. 插入新节点

    cpp 复制代码
    p->next = h->next;   // 步骤1:p->next = h(因空链表 h->next = h)
    p->prev = h;         // 步骤2:p->prev = h(头节点为 p 的前驱)
    h->next->prev = p;   // 步骤3:h->prev = p(原 h->next 是 h,故 h->prev 指向 p)
    h->next = p;         // 步骤4:h->next = p(头节点 next 指向 p,p 成为第一个有效节点)
  3. 结果 :链表变为 h <-> p(双向循环,p 为唯一有效节点)。

图示:

4.双向链表的尾删

双向链表的尾删(删除链表最后一个有效节点)是链表操作的高频场景,其核心是 安全释放尾节点内存并修复前驱节点的指针关系。实现尾删之前,我们需要先实现一个判空的函数,如果链表为空则不能继续删除了:

尾删函数形式:

void LTPopBack(ListNode* h)

双向链表的判空

bool LTEmpty(ListNode* phead)

cpp 复制代码
bool LTEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

讲解:

cpp 复制代码
bool LTEmpty(ListNode* phead) {
    assert(phead);  // 确保传入的头节点指针非空(避免对 NULL 解引用)
    return phead->next == phead;  // 直接通过指针关系判断
}
  • 空链表 :头节点的 next 指针指向自身(phead->next == phead),此时链表无任何有效节点。
  • 非空链表 :头节点的 next 指向第一个有效节点(phead->next != phead)。

接下来回归正题:

cpp 复制代码
bool LTEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopBack(ListNode* h)
{
	if (LTEmpty(h))
	{
		return;
	}
	ListNode* p = h->prev;
	h->prev = p->prev;
	p->prev->next = h;
	free(p);
}

讲解:

步骤1:判空------避免对空链表操作(核心安全检查!)

cpp 复制代码
if (LTEmpty(h)) { return; }  // 调用 LTEmpty 判断链表是否为空
  • LTEmpty 逻辑return phead->next == phead(头节点的 next 指向自身,说明无有效节点)。
  • 作用 :若链表为空(如 h <-> h),直接返回,避免后续 p->prev 访问空指针导致 程序崩溃

步骤2:定位尾节点------无需遍历,O(1) 效率!

cpp 复制代码
ListNode* p = h->prev;  // p 指向尾节点(C)
  • 双向循环链表特性 :头节点的 prev 指针 直接指向尾节点 (无需从 h->next 开始遍历),时间复杂度 O(1)(单向链表需 O(n),这是双向链表的核心优势)。

步骤3:修复指针------确保链表循环关系不中断

cpp 复制代码
h->prev = p->prev;      // 步骤3.1:头节点的 prev 指向尾节点的前驱(B)
p->prev->next = h;      // 步骤3.2:尾节点前驱(B)的 next 指向头节点(h)
  • 修复后链表结构h <-> A <-> B <-> h(尾节点 C 已从逻辑上"脱离"链表)。
  • 关键对比 :若仅修改 h->prev = p->prev 而不修改 p->prev->next,会导致 B 的 next 仍指向 C,链表出现"断裂"(B <-> C <-> h <-> A <-> B),形成错误的循环子链。

步骤4:释放内存------避免内存泄漏(必须!)

cpp 复制代码
free(p);  // 释放尾节点 C 的内存空间

尾删图示:

5.双向链表的头删

双向链表的头删指删除链表的第一个有效节点(即头节点后的第一个节点)。带头节点的链表可避免对空链表的特殊处理,实现更简洁。

根据前文所讲,我们可知其实现图示:

函数形式:

void LTPopFront(ListNode* h);

cpp 复制代码
void LTPopFront(ListNode* h)
{
	if (LTEmpty(h) )
	{
		printf("链表为空,无法头删\n");
		return;
	}
	ListNode* p = h->next;
	h->next = p->next;
	p->next->prev = h;
	free(p);
}

讲解:

核心代码逐行解析

1. 判空处理(避免操作无效链表)

cpp 复制代码
if (h == NULL || h->next == h) {
    printf("链表为空,无法头删\n");
    return;
}
  • h == NULL:检查头节点指针是否为空(链表未初始化)。
  • h->next == h :检查链表是否为空(头节点的next指向自身,说明无有效节点)。
  • 处理逻辑:若满足任一条件,打印错误信息并退出函数,避免后续非法操作。

2. 记录待删除节点

cpp 复制代码
ListNode* p = h->next;  // p 指向第一个有效节点(待删除节点)
  • 头节点hnext指针指向链表的第一个有效节点,用p临时保存该节点地址,便于后续释放内存。

3. 更新链表指针关系(断链与重连)

cpp 复制代码
h->next = p->next;       // 步骤1:头节点的next指向p的下一个节点(跳过p)
p->next->prev = h;       // 步骤2:p的下一个节点的prev指向头节点(反向指针同步)
  • 步骤1 :头节点h不再指向p,而是直接指向p的下一个节点(p->next),完成"前向断链"。
  • 步骤2p的下一个节点(p->next)的prev指针从指向p改为指向头节点h,完成"反向断链"。
  • 效果 :通过双向指针的更新,p节点从链表中完全脱离。

4. 释放内存(避免内存泄漏)

cpp 复制代码
free(p);  // 释放p节点的内存空间
  • 手动释放p指向的节点内存(C语言需显式管理内存,否则会导致内存泄漏)。

6.双向链表的销毁

双向链表的销毁需遍历所有节点并逐个释放内存,避免内存泄漏,顺序为:先销毁除了头结点(哨兵位)之外的所有节点,在最后释放头结点空间。

函数形式:

void LTDestory(ListNode* h);

cpp 复制代码
void LTDestory(ListNode* h)
{
	if (LTEmpty(h))
	{
		free(h);
		return;
	}
	ListNode* p = h->next;
	while (p != h)
	{
		ListNode* pr = p;
		p = p->next;
		free(pr);
	}
	free(h);
	h = NULL;
}

讲解:

按道理来说是要传入二级指针的,但是前面其它接口都用的一级,这里和初始化部分统一比较好点,我们可以在测试文件中最后销毁完手动将头节点置为空。

借助现有实现测试:

本代码通过三个文件来实现的:

首先:

1.h

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int type;
typedef struct ListNode
{
	type data;
	//前驱指针,指向前一个指针
	struct ListNode* prev;
	//后继指针,指向后一个指针
	struct ListNode* next;
}ListNode;
void LTInit(ListNode** h);
void LTPushBack(ListNode* h, type x);
ListNode* LTcreat(type x);
void LTPushFront(ListNode* h, type x);
void LTPopBack(ListNode* h);
void LTPopFront(ListNode* h);
void LTDestory(ListNode* h);
void print(ListNode* h);

1.cpp

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

void LTInit(ListNode** h)
{     
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	*h = ph;
	(*h)->data = -1;
	(*h)->next = *h;
	(*h)->prev = *h;
}
ListNode* LTcreat(type x)
{
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	ph->data = x;
	ph->next = ph;
	ph->prev = ph;
	return ph;
}
void LTPushBack(ListNode* h, type x)
{     
	ListNode* p = LTcreat(x);
	p->next = h;
	p->prev = h->prev;
	h->prev->next = p;
	h->prev = p;
}
void LTPushFront(ListNode* h, type x)
{
	ListNode* p = LTcreat(x);
	p->next = h->next;
	p->prev = h;
	h->next->prev = p;
	h->next = p;
}
bool LTEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopBack(ListNode* h)
{
	if (LTEmpty(h))
	{
		return;
	}
	ListNode* p = h->prev;
	h->prev = p->prev;
	p->prev->next = h;
	free(p);
}
void LTPopFront(ListNode* h)
{
	if (LTEmpty(h) )
	{
		printf("链表为空,无法头删\n");
		return;
	}
	ListNode* p = h->next;
	h->next = p->next;
	p->next->prev = h;
	free(p);
}
void LTDestory(ListNode* h)
{
	if (LTEmpty(h))
	{
		free(h);
		return;
	}
	ListNode* p = h->next;
	while (p != h)
	{
		ListNode* pr = p;
		p = p->next;
		free(pr);
	}
	free(h);
	h = NULL;
}
void print(ListNode* h)
{
	if (LTEmpty(h))
	{
		return;
}
	ListNode* p = h->next;
	while (p != h)
	{
		printf("%d ", p->data);
		p = p->next;
 }
	printf("\n");
}

main.cpp

cpp 复制代码
#include"1.h"
void test()
{
	ListNode* h;
	LTInit(&h);
	LTPushBack(h, 10);    //10 
	LTPushBack(h, 15);    //10 15 
	LTPushBack(h, 111);   //10 15 111
	print(h);
	LTPushFront(h, 2);     //2 10 15 111
	LTPushFront(h, 12);    //12 2 10 15 111
	print(h);
	LTPopBack(h);         // 12 2 10 15
	print(h);
	LTPopFront(h);        //2 10 15
	print(h);

	LTDestory(h);
}

int main()
{
	test();
}

结果:

7.双向链表查找

双向链表的查找操作与单链表类似,但可利用创建一个暂时的指针实现遍历。

函数形式:

ListNode* LTFind(ListNode* h, type x);

实现:

cpp 复制代码
ListNode* LTFind(ListNode* h, type x)
{
	if (LTEmpty(h))
	{
		return NULL;
    }
	ListNode* p = h->next;
	while (p != h)
	{
		if (p->data == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}

细讲:

cpp 复制代码
 if (LTEmpty(h)) {
        return NULL;
    }

    // 3. 遍历查找目标节点(从第一个数据节点开始)
    ListNode* p = h->next;
    while (p != h) {  // 双向循环链表:遍历至回到头节点结束
        if (p->data == x) {
            return p;  // 找到目标,返回节点指针
        }
        p = p->next;
    }

    // 4. 遍历完未找到
    return NULL;

通过遍历来实现,如果在遍历过程中找到了我们需要查找的数据就返回当前位置的节点,没有就返回空。

8.双向链表在指定位置插入

双向链表在指定位置插入分为双向链表在指定位置之前插入与双向链表在指定位置之后插入

双向链表在指定位置之后插入

函数形式:

void LTInsert(ListNode* pos, type x)

实现:

cpp 复制代码
void LTInsert(ListNode* pos, type x)
{
	assert(pos);
	ListNode* p = pos->next;
	ListNode* newnode = LTcreat(x);
	pos->next = newnode;
	newnode->prev = pos;
	newnode->next = p;
	p->prev = newnode;
}

细讲:该函数用于在双向链表的 指定节点 pos 之后插入新节点,核心逻辑是通过调整指针关系实现无缝插入。

1.参数与前置检查

cpp 复制代码
void LTInsert(ListNode* pos, type x) {  // type需替换为实际数据类型(如int)
    assert(pos);  // 断言:确保pos不为空指针,避免操作无效节点
  • 作用pos 是插入位置的基准节点(新节点将插入其后),x 是新节点的数据。
  • 风险控制assert(pos) 防止用户传入 nullptr 导致后续操作崩溃。

2. 保存后继节点

cpp 复制代码
    ListNode* p = pos->next;  // 临时保存pos的原后继节点
  • 必要性 :插入新节点后,posnext 指针会指向新节点,若不提前保存原后继 pos->next,将导致原链表后半部分丢失。

核心:调整指针关系(4步插入法)

cpp 复制代码
    pos->next = newnode;       // 步骤1:pos的后继指向新节点
    newnode->prev = pos;       // 步骤2:新节点的前驱指向pos
    newnode->next = p;         // 步骤3:新节点的后继指向原后继p
    p->prev = newnode;         // 步骤4:原后继p的前驱指向新节点

在指定位置之后的插入操作其实也没有很难,还是先断言,后续就是先申请一个新节点,跟头插尾插相似的方式。

双向链表在指定位置之前插入

函数形式:

void LTInsertfront(ListNode* pos, type x);

实现:

cpp 复制代码
void LTInsertfront(ListNode* h, ListNode* pos, type x)
{
	if (LTEmpty(h))
	{
		return ;
	}
	ListNode* p = h;
	while (p->next != h)
	{
		if (p->next == pos)
		{
			break;
		}
		p = p->next;
	}
	if (p->next == h)
	{
		return;
	}
	ListNode* newnode = LTcreat(x);
	ListNode* pr = p->next;
	newnode->next = pr;
	newnode->prev = p;
	p->next = newnode;
	pr->prev = newnode;
}

细讲:核心功能是 在指定节点 pos 的前驱位置插入新节点 (即"在 pos 前面插入")。

cpp 复制代码
void LTInsertfront(ListNode* h, ListNode* pos, type x)
 {  // type替换为实际数据类型(如int)

    if (LTEmpty(h)) {
        return;
    }

    // 2. 查找pos的前驱节点p
    ListNode* p = h;
    while (p->next != h) {  // 遍历所有有效节点(不包含头节点h)
        if (p->next == pos) {
            break;  // 找到pos,p是pos的前驱
        }
        p = p->next;
    }

    // 3. 校验pos是否存在(未找到则退出)
    if (p->next == h) {
        return;
    }

    // 4. 创建并插入新节点
    ListNode* newnode = LTcreat(x);
    ListNode* pos_node = p->next;  // pos_node = pos,明确变量含义
    newnode->prev = p;
    newnode->next = pos_node;
    p->next = newnode;
    pos_node->prev = newnode;
}

9.双向链表指定位置删除

删除节点需修改 前驱节点的 next后继节点的 prev ,并释放被删节点内存。关键是 处理边界情况 (如 pos 是头/尾节点)。

函数形式:

void LTErase(ListNode* pos)

实现:

cpp 复制代码
void LTErase(ListNode* pos)
{
	assert(pos);
	ListNode* p = pos->prev;
	p->next = pos->next;
	pos->next->prev = p;
	free(pos);
	pos = NULL;
}

细讲:

  • 步骤

    1. 通过 pos->prev 获取前驱节点 p
    2. 调整指针:p->next = pos->next(切断 ppos 的连接,指向 pos 的后继);
    3. 调整后继节点的前驱指针:pos->next->prev = p(切断后继与 pos 的连接,指向 p);
    4. 释放 pos 节点内存,并置空局部指针 pos
  • 前置断言 assert(pos)

  • 作用 :确保 pos 不为空指针,避免后续 pos->prev 等操作引发崩溃。

10.总代码展示:(加入了测试代码)

1.h

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int type;
typedef struct ListNode
{
	type data;
	//前驱指针,指向前一个指针
	struct ListNode* prev;
	//后继指针,指向后一个指针
	struct ListNode* next;
}ListNode;
void LTInit(ListNode** h);
void LTPushBack(ListNode* h, type x);
ListNode* LTcreat(type x);
void LTPushFront(ListNode* h, type x);
void LTPopBack(ListNode* h);
void LTPopFront(ListNode* h);
void LTDestory(ListNode* h);
void print(ListNode* h);
ListNode* LTFind(ListNode* h, type x);
void LTInsert(ListNode* pos, type x);
void LTInsertfront(ListNode* h,ListNode* pos, type x);
void LTErase(ListNode* pos);

1.cpp

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

void LTInit(ListNode** h)
{     
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	*h = ph;
	(*h)->data = -1;
	(*h)->next = *h;
	(*h)->prev = *h;
}
ListNode* LTcreat(type x)
{
	ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
	if (ph == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	ph->data = x;
	ph->next = ph;
	ph->prev = ph;
	return ph;
}
void LTPushBack(ListNode* h, type x)
{     
	ListNode* p = LTcreat(x);
	p->next = h;
	p->prev = h->prev;
	h->prev->next = p;
	h->prev = p;
}
void LTPushFront(ListNode* h, type x)
{
	ListNode* p = LTcreat(x);
	p->next = h->next;
	p->prev = h;
	h->next->prev = p;
	h->next = p;
}
bool LTEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}
void LTPopBack(ListNode* h)
{
	if (LTEmpty(h))
	{
		return;
	}
	ListNode* p = h->prev;
	h->prev = p->prev;
	p->prev->next = h;
	free(p);
}
void LTPopFront(ListNode* h)
{
	if (LTEmpty(h) )
	{
		printf("链表为空,无法头删\n");
		return;
	}
	ListNode* p = h->next;
	h->next = p->next;
	p->next->prev = h;
	free(p);
}
void LTDestory(ListNode* h)
{
	if (LTEmpty(h))
	{
		free(h);
		return;
	}
	ListNode* p = h->next;
	while (p != h)
	{
		ListNode* pr = p;
		p = p->next;
		free(pr);
	}
	free(h);
	h = NULL;
}
void print(ListNode* h)
{
	if (LTEmpty(h))
	{
		return;
}
	ListNode* p = h->next;
	while (p != h)
	{
		printf("%d ", p->data);
		p = p->next;
 }
	printf("\n");
}
ListNode* LTFind(ListNode* h, type x)
{
	if (LTEmpty(h))
	{
		return NULL;
    }
	ListNode* p = h->next;
	while (p != h)
	{
		if (p->data == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}
void LTInsert(ListNode* pos, type x)
{
	assert(pos);
	ListNode* p = pos->next;
	ListNode* newnode = LTcreat(x);
	pos->next = newnode;
	newnode->prev = pos;
	newnode->next = p;
	p->prev = newnode;
}
void LTInsertfront(ListNode* h, ListNode* pos, type x)
{
	if (LTEmpty(h))
	{
		return ;
	}
	ListNode* p = h;
	while (p->next != h)
	{
		if (p->next == pos)
		{
			break;
		}
		p = p->next;
	}
	if (p->next == h)
	{
		return;
	}
	ListNode* newnode = LTcreat(x);
	ListNode* pr = p->next;
	newnode->next = pr;
	newnode->prev = p;
	p->next = newnode;
	pr->prev = newnode;
}
void LTErase(ListNode* pos)
{
	assert(pos);
	ListNode* p = pos->prev;
	p->next = pos->next;
	pos->next->prev = p;
	free(pos);
	pos = NULL;
}

main.cpp

cpp 复制代码
#include"1.h"
void test()
{
	ListNode* h;
	LTInit(&h);
	LTPushBack(h, 10);    //10 
	LTPushBack(h, 15);    //10 15 
	LTPushBack(h, 111);   //10 15 111
	print(h);
	LTPushFront(h, 2);     //2 10 15 111
	LTPushFront(h, 12);    //12 2 10 15 111
	print(h);
	LTPopBack(h);         // 12 2 10 15
	print(h);
	LTPopFront(h);        //2 10 15
	print(h);
	ListNode* p = LTFind(h,10);
	LTInsert(p, 100);	//2 10 100 15
	print(h);

	LTInsert(p, 200);	//2 10 200 100 15
	print(h);
	LTErase(p);
	print(h);            //2 200 100 15
	LTDestory(h);
}

int main()
{
	test();
}

三、顺序表与链表的分析

本图列举了顺序表与链表之间的相同点与不同点:


一、相同点
  • 逻辑结构一致:均为线性表,数据元素之间呈一对一的顺序关系。
  • 核心操作相同:都支持插入、删除、查找、遍历等基本线性表操作。
  • 存储数据类型:均可存储相同类型的数据元素(如整数、结构体等)。
二、不同点(核心差异)
对比维度 顺序表 链表
存储结构 连续内存空间(数组实现) 非连续内存空间(节点通过指针/引用连接)
内存分配方式 静态分配(固定大小)或动态分配 动态分配(节点按需申请释放)
访问效率 随机访问(通过下标 O(1) 顺序访问(需从头遍历 O(n)
插入/删除效率 中间/头部插入删除需移动元素(O(n) 仅需修改指针(O(1),已知前驱节点时)
空间利用率 可能存在内存浪费(预分配过大)或溢出 无内存浪费(按需分配),但额外存储指针
实现复杂度 简单(依赖数组) 复杂(需管理指针/引用,避免内存泄漏)
三、关键结论
  • 顺序表 :适合 频繁随机访问、数据量固定 的场景(如存储学生信息表)。
  • 链表 :适合 频繁插入删除、数据量动态变化 的场景(如实现队列、栈)。

四、链表算法题

一、移除链表元素

移除链表元素

203. 移除链表元素 - 力扣(LeetCode)

由题意可知,本题要求移除值为val的节点,并要求返回新的头结点:

  • 1.链表的解法可以通过遍历整个链表,用一个节点存储前一个节点,在发现值为val时候改变next区域的指向解答:
  • 2.我们也可以选择创建一个新链表,存储符合要求的节点,虽然没有释放原链表空间,但做OJ题不释放也没什么问题的,该方法较为简单,本次解题选择此方法:

解题代码:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode Node; 
struct ListNode* removeElements(struct ListNode* head, int val) 
{ Node *h=NULL,*pr=NULL;
Node * p=head;
while(p)
{
    if(p->val!=val)
    {  if(h==NULL)
    {
        h=p;
        pr=p;
    }
else 
{
    pr->next=p;
    pr=p;
}
    }
    p=p->next;
}
if(pr)
    pr->next=NULL;
   return h; 
}

解题思路:

cpp 复制代码
// 定义链表节点结构体(题目已给出)
struct ListNode {
    int val;
    struct ListNode *next;
};

// 简化结构体名称为 Node(方便后续使用)
typedef struct ListNode Node; 

// 主函数:移除值为 val 的节点
struct ListNode* removeElements(struct ListNode* head, int val) { 
    Node *h = NULL;  // 新链表的头节点(待返回)
    Node *pr = NULL; // 新链表的尾节点指针(用于拼接有效节点)
    Node *p = head;  // 遍历原链表的指针

    // 遍历原链表
    while (p) {
        // 当前节点值不等于 val,需保留到新链表
        if (p->val != val) {  
            if (h == NULL) {  // 新链表为空(首次遇到有效节点)
                h = p;       // 新链表头节点指向当前节点
                pr = p;      // 新链表尾节点也指向当前节点
            } else {          // 新链表非空(拼接后续有效节点)
                pr->next = p; // 尾节点 next 指向当前节点(拼接)
                pr = p;       // 尾节点指针后移到当前节点
            }
        }
        p = p->next; // 遍历下一个节点(无论当前节点是否保留)
    }

    // 遍历结束后,新链表尾节点 next 置空(避免野指针)
    if (pr) pr->next = NULL;

    return h; // 返回新链表头节点
}

1.变量初始化

  • h:新链表的头节点,初始为 NULL(表示新链表为空)。
  • pr:新链表的尾节点指针,用于拼接有效节点(值不等于 val 的节点)。
  • p:遍历指针,从原链表头节点 head 开始,依次访问每个节点。

2. 遍历原链表(while (p)

循环条件 p 等价于 p != NULL,即遍历至原链表末尾时停止。

  • 情况1:当前节点 p 的值不等于 val(需保留)

    • 首次保留节点(h == NULL
      新链表为空,因此 h(头节点)和 pr(尾节点)都指向当前节点 p
    • 非首次保留节点(h != NULL
      通过 pr->next = p 将当前节点 p 拼接到新链表尾部,然后 pr = p 更新尾节点指针。
  • 情况2:当前节点 p 的值等于 val(需删除)

    不执行任何操作(不拼接至新链表),直接通过 p = p->next 跳过当前节点。

3. 处理新链表尾节点(if (pr) pr->next = NULL

  • 遍历结束后,pr 指向新链表的最后一个有效节点。
  • 若新链表非空(pr != NULL),需将其 next 置为 NULL,避免原链表中后续节点(已删除节点)的指针残留,导致野指针风险。

4. 返回新链表头节点

h 指向新链表的第一个有效节点,若原链表所有节点都被删除(如 head = [2,2,2], val=2),则 h 保持 NULL,返回空链表。

二、反转链表

反转链表

206. 反转链表 - 力扣(LeetCode)

解题思路:

  • 反转题中给出的链表,如果简单来想,我们可以创建一个新链表一个一个节点的复制,但是题中的是单链表,不可找到前驱节点的,如果用上面思路,那会相当麻烦了。

  • 正确思路:通过三个指针来实现:

  • 迭代法(推荐:时间O(n),空间O(1))

    核心思路

    通过 3个指针 遍历链表,逐次反转节点指向:

  • prev:指向 已反转部分 的头节点(初始为 NULL)。

  • curr:指向 当前待反转节点 (初始为 head)。

  • next:临时保存 curr 的下一个节点(避免反转后断链)。

简单来做,我将其命名为s1,s2,s3了。

基本结构是这样的,接下来我将结合代码来讲解:

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode node;
struct ListNode* reverseList(struct ListNode* head)
{  node * s1=NULL;
node *s2=head,*s3=NULL;
if(s2)
{
    s3=s2->next;
}

  while(s2)
  {
    s2->next=s1;
    s1=s2;
    s2=s3;
    if(s3)
    {
        s3=s3->next;
    }
  }  
  return s1;
}

讲解:

cpp 复制代码
typedef struct ListNode node;  // 简化结构体名称为 node
struct ListNode* reverseList(struct ListNode* head) {  
    node *s1 = NULL;           // s1:指向「已反转部分」的头节点(初始为空,因为还未开始反转)
    node *s2 = head;           // s2:指向「当前待反转节点」(初始为原链表的头节点)
    node *s3 = NULL;           // s3:临时保存 s2 的下一个节点(避免反转后链表断链)
    
    if (s2) {  // 若 s2(原头节点)不为空,才初始化 s3(避免空链表时访问 NULL->next)
        s3 = s2->next;  
    }
    // ... 循环反转逻辑 ...
}
cpp 复制代码
while (s2) {  // 循环条件:当前待反转节点 s2 不为 NULL(遍历完所有节点后终止)
    // 步骤1:反转当前节点 s2 的指向(指向已反转部分的头节点 s1)
    s2->next = s1;  
    // 步骤2:s1 后移到 s2(已反转部分的长度+1,s1 成为新的"已反转头节点")
    s1 = s2;        
    // 步骤3:s2 后移到 s3(继续处理下一个待反转节点)
    s2 = s3;        
    // 步骤4:若 s3 不为空,s3 后移到下一个节点(为下次循环做准备)
    if (s3) {       
        s3 = s3->next;  
    }
}  
return s1;  // 循环结束后,s1 指向原链表的尾节点(即新链表的头节点)

图示:

根据上图:我们可知代码 if (s3) { s3 = s3->next; 的原因了:

while (s2) { // 循环条件:当前待反转节点 s2 不为 NULL(遍历完所有节点后终止)

// 步骤1:反转当前节点 s2 的指向(指向已反转部分的头节点 s1)

s2->next = s1;

// 步骤2:s1 后移到 s2(已反转部分的长度+1,s1 成为新的"已反转头节点")

s1 = s2;

// 步骤3:s2 后移到 s3(继续处理下一个待反转节点)

s2 = s3;

// 步骤4:若 s3 不为空,s3 后移到下一个节点(为下次循环做准备)

if (s3) {

s3 = s3->next;

}

}

return s1; // 循环结束后,s1 指向原链表的尾节点(即新链表的头节点)

总结

以上就是今天要讲的内容,本篇文章涉及的知识点为: 讲解双向链表概念与结构,实现双向链表,顺序表与链表的分析,链表算法题等知识的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,接下来的内容我会很快更新。

相关推荐
CodeAmaz4 小时前
通用 List 分批切割并循环查询数据库工具类
java·数据结构·工具类·分页
xlq223224 小时前
15.list(上)
数据结构·c++·list
我不会插花弄玉4 小时前
排序【由浅入深-数据结构】
c语言·数据结构
XH华6 小时前
数据结构第三章:单链表的学习
数据结构
No0d1es6 小时前
电子学会青少年软件编程(C/C++)六级等级考试真题试卷(2025年9月)
c语言·c++·算法·青少年编程·图形化编程·六级
Knox_Lai6 小时前
数据结构与算法学习(0)-常见数据结构和算法
c语言·数据结构·学习·算法
逐步前行6 小时前
C项目--羊了个羊(两关全)--含源码
c语言·开发语言
blammmp7 小时前
算法专题二十:贪心算法
数据结构·算法·贪心算法
小白程序员成长日记7 小时前
2025.11.17 力扣每日一题
数据结构·算法·leetcode
赖small强8 小时前
【Linux C/C++开发】第10周:STL容器 - 理论与实战
linux·c语言·c++·stl容器