详细介绍:数据结构:双向链表(总)

   目录

   目录

前言 

一、双向链表概念与结构

双向链表概念

带头双向循环链表

双向链表结构

二、实现双向链表

1.双向链表的初始化

代码逐行解析

​编辑

2.双向链表的尾插

创建节点

3.双向链表的头插

4.双向链表的尾删

双向链表的判空

5.双向链表的头删

6.双向链表的销毁

借助现有实现测试:

7.双向链表查找

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

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

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

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

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

三、顺序表与链表的分析

一、相同点

二、不同点(核心差异)

三、关键结论

四、链表算法题

1、移除链表元素

 2、反转链表

3、链表的中间结点

单链表找中间节点算法解析:

推荐解法:

核心思路:快慢指针同步移动

有奇数个节点:

 有偶数个节点:

复杂度分析

 4、两个升序链表合并为一个新的升序链表

 5、链表分割

1. 初始化指针

2. 遍历原链表并分区

3. 处理边界情况和拼接链表

6.链表回文结构

核心思路

总结

前言 

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

一、双向链表概念与结构

双向链表概念

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

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

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

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

带头双向循环链表。

带头双向循环链表

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

对于上面头结点讲解:

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

双向链表结构

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

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

二、实现双向链表

1.双向链表的初始化

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

函数形式:

void LTInit(ListNode** 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* 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 指针指向自身(双向循环)。

初始图示:

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)

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 函数:创建新节点(含哨兵位初始化)

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

LTPushBack 函数:尾插操作

  • 核心逻辑:通过 LTcreat(x) 创建新节点 p,并插入到链表尾部(头节点 h 的 prev 位置)。
  • 步骤拆解
    1. ListNode* p = LTcreat(x); → 创建新节点 pp->next 和 p->prev 初始指向自身)。
    2. p->next = h; → 新节点 p 的 next 指向头节点 h(保持循环)。
    3. p->prev = h->prev; → 新节点 p 的 prev 指向原尾节点(h->prev 是原尾节点)。
    4. h->prev->next = p; → 原尾节点的 next 指向新节点 p
    5. h->prev = p; → 头节点 h 的 prev 指向新节点 p(更新尾节点为 p)。
  •  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)

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

细讲一下:

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->next 和 h->prev 初始指向自身,即空链表状态)。

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

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

  1. 创建新节点ListNode* p = LTcreat(10);
    • LTcreat 分配内存并初始化 p->data=10p->next=pp->prev=p
  2. 插入新节点
    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)

bool LTEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

讲解:

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

接下来回归正题:

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:判空——避免对空链表操作(核心安全检查!)

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

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

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

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

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:释放内存——避免内存泄漏(必须!)

free(p);  // 释放尾节点 C 的内存空间

尾删图示:

5.双向链表的头删

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

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

函数形式:

void LTPopFront(ListNode* h);

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. 判空处理(避免操作无效链表)

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

2. 记录待删除节点

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

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

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. 释放内存(避免内存泄漏)

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

6.双向链表的销毁

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

函数形式:

void LTDestory(ListNode* h);

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

#include
#include
#include
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

#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

#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);

实现:

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

细讲:

 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)

实现:

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.参数与前置检查

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

2. 保存后继节点

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

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

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

实现:

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 前面插入”)。

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)

实现:

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(切断 p 与 pos 的连接,指向 pos 的后继);
    3. 调整后继节点的前驱指针:pos->next->prev = p(切断后继与 pos 的连接,指向 p);
    4. 释放 pos 节点内存,并置空局部指针 pos
  • 前置断言 assert(pos)

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

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

1.h

#include
#include
#include
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

#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

#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),已知前驱节点时)
空间利用率可能存在内存浪费(预分配过大)或溢出无内存浪费(按需分配),但额外存储指针
实现复杂度简单(依赖数组)复杂(需管理指针/引用,避免内存泄漏)
三、关键结论
  • 顺序表:适合 频繁随机访问、数据量固定 的场景(如存储学生信息表)。
  • 链表:适合 频繁插入删除、数据量动态变化 的场景(如实现队列、栈)。

四、链表算法题

1、移除链表元素

移除链表元素

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

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

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

解题代码:

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

解题思路:

// 定义链表节点结构体(题目已给出)
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,返回空链表。

 2、反转链表

反转链表

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

解题思路:

  • 反转题中给出的链表,如果简单来想,我们可以创建一个新链表一个一个节点的复制,但是题中的是单链表,不可找到前驱节点的,如果用上面思路,那会相当麻烦了。
  • 正确思路:通过三个指针来实现:
  • 迭代法(推荐:时间O(n),空间O(1))

    核心思路

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

  • prev:指向 已反转部分 的头节点(初始为 NULL)。
  • curr:指向 当前待反转节点(初始为 head)。
  • next:临时保存 curr 的下一个节点(避免反转后断链)。

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

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

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

讲解:

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;
    }
    // ... 循环反转逻辑 ...
}
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 指向原链表的尾节点(即新链表的头节点)

3、链表的中间结点

876. 链表的中间结点 - 力扣(LeetCode)

中间节点

思路讲解:

单链表找中间节点算法解析:

  单链表的节点通过指针串联,无法像数组一样通过索引直接访问中间元素,而我们要返回中间节点,简单来说:我们可以创建一个数组,来将每一个节点的值存入到数组中,通过一个临时变量记录节点个数,通过创建的临时变量来访问中间节点的值,但此方法很大的缺点:

  • 空间复杂度与时间复杂度都很大,效率低
  • 没有事先告知我们该链表有多少个节点,如果使用的为静态数组,空间开小了不行,开大了又会有空间浪费。

推荐解法:

  通过 快慢指针法 高效定位单链表的中间节点,时间复杂度为 O(n),空间复杂度为 O(1),是链表操作中的经典技巧。

核心思路:快慢指针同步移动

  • 慢指针(slow):每次移动 1 步
  • 快指针(fast):每次移动 2 步
  • 终止条件:当快指针无法继续移动(fast == NULL 或 fast->next == NULL)时,慢指针恰好指向 中间节点

至于(fast == NULL 或 fast->next == NULL)的情况,我以图来表示:

情况1:

有奇数个节点:

此时为fast->next == NULL

 有偶数个节点:


通过图可知:此时为fast= == NULL

所以我们的解决代码为:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode node;
struct ListNode* middleNode(struct ListNode* head)
{   node *fast=head;
    node *slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
    }
    return slow;
}

解题思路:

通过两个指针的步差关系,一次遍历即可定位中间节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;               // 节点值
 *     struct ListNode *next; // 指向下一节点的指针
 * };
 */
typedef struct ListNode node;  // 将结构体名简化为 node,简化后续代码书写
struct ListNode* middleNode(struct ListNode* head) {
    node *fast = head;  // 快指针:初始指向头节点,每次走 2 步
    node *slow = head;  // 慢指针:初始指向头节点,每次走 1 步
    // 循环条件:快指针未到链表尾部(需同时满足 fast 和 fast->next 非空)
    while (fast && fast->next) {
        fast = fast->next->next;  // 快指针走 2 步
        slow = slow->next;        // 慢指针走 1 步
    }
    return slow;  // 慢指针指向中间节点
}
  • 快指针(fast):每次移动 2 步,相当于“领跑者”;
  • 慢指针(slow):每次移动 1 步,相当于“追随者”;
  • 终止条件:当快指针无法继续移动(fast 或 fast->next 为空)时,慢指针恰好走到链表中间。
  • 必须同时满足两个条件
    • fast != NULL:防止快指针本身为空(如链表只有 1 个节点时,fast->next 会越界);
    • fast->next != NULL:防止快指针下一步越界(如链表有 2 个节点时,fast->next->next 会越界)。
  • 复杂度分析
  • 时间复杂度:O(n),仅需遍历链表一次(快指针最多移动 n/2 步)。
  • 空间复杂度:O(1),仅用两个指针变量(常数空间)。

 4、两个升序链表合并为一个新的升序链表

21. 合并两个有序链表 - 力扣(LeetCode)

升序链表合并为一个新的升序链表

思路讲解:

题目要求,我们将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

本题有多种方式实现:

  • 我们可以创建一个新链表,通过这一个链表来进行操作,在新链表中进行操作。

解决代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode node;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    if(list1==NULL)
    {
        return list2;
    }
    if(list2==NULL)
    {
        return list1;
    }
    node* s1=list1;
    node * s2=list2;
    node * head=NULL;
    node* tail=NULL;
    while(s1&&s2)
    {
        if(s1->valval)
        {
            if(head==NULL)
            {
                head=s1;
                tail=s1;
            }
            else
            {
                tail->next=s1;
                tail=s1;
            }
            s1=s1->next;
        }
        else
        {
            if(head==NULL)
            {
                head=s2;
                tail=s2;
            }
            else
            {
                tail->next=s2;
                tail=s2;
            }
            s2=s2->next;
        }
    }
    if(s1)
    {
        tail->next=s1;
    }
    if(s2)
    {
        tail->next=s2;
    }
    return head;
}

讲解:

函数定义与参数

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
  • 功能:合并两个有序链表 list1 和 list2,返回合并后的新链表头节点。
  • 参数list1 和 list2 为待合并的有序单链表(非递减)。

 边界条件处理

if(list1==NULL) return list2;  // 若list1为空,直接返回list2
if(list2==NULL) return list1;  // 若list2为空,直接返回list1
  • 作用:避免空链表导致的无效遍历,提升效率。

初始化指针

node* s1 = list1;  // 遍历list1的指针
node* s2 = list2;  // 遍历list2的指针
node* head = NULL; // 合并后新链表的头节点
node* tail = NULL; // 合并后新链表的尾节点(用于链接新节点)

 核心合并逻辑(双指针遍历比较)

while(s1 && s2) {  // 当s1和s2均未遍历完时循环
    if(s1->val < s2->val) {  // 若s1当前节点值更小
        if(head == NULL) {   // 首次链接节点(头节点未初始化)
            head = s1;       // 头节点指向s1
            tail = s1;       // 尾节点指向s1
        } else {
            tail->next = s1; // 尾节点的next指向s1(链接新节点)
            tail = s1;       // 尾节点后移到s1
        }
        s1 = s1->next;       // s1指针后移,继续遍历list1
    } else {  // 若s2当前节点值更小或相等(非递减)
        // 逻辑同上,处理s2节点
        if(head == NULL) {
            head = s2;
            tail = s2;
        } else {
            tail->next = s2;
            tail = s2;
        }
        s2 = s2->next;
    }
}

 链接剩余节点

if(s1) tail->next = s1;  // 若s1未遍历完,直接链接剩余节点
if(s2) tail->next = s2;  // 若s2未遍历完,直接链接剩余节点
  • 原因:循环结束后,至少有一个链表已遍历完,剩余节点本身有序,直接链接到新链表尾部即可。

返回结果

return head;  // 返回合并后新链表的头节点

总结

  • 时间复杂度O(m+n)(m、n 为两链表长度),仅需一次遍历。
  • 空间复杂度O(1),未创建新节点,仅通过指针操作合并。
  • 优点:逻辑清晰,边界处理完善,适合理解合并链表的核心思路。
:每次比较  s1 和  s2 指向的节点值,将较小的节点链接到新链表尾部,同时移动对应链表的遍历指针。

 5、链表分割

链表分割_牛客题霸_牛客网

分割链表

由题意可知:

本题要求将值借助x分界,再将分界后的结果合并一起,题目要求,不可以改变数据顺序,我们可以通过,双链表法来实现:

  • 创建两个链表,分别存储小于x、大于x的节点,最后将链表合并。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition
{
public:
    ListNode* partition(ListNode* pHead, int x)
    {
        ListNode *h1=NULL,*t1=NULL;
        ListNode *h2=NULL,*t2=NULL;
        ListNode * p=pHead;
        while(p)
        {
            if(p->valnext=p;
                    t1=p;
                }
            }
            else
            {
                if(h2==NULL)
                {
                    h2=p;
                    t2=p;
                }
                else
                {
                    t2->next=p;
                    t2=p;
                }
            }
            p=p->next;
        }
        if(t2)
        {
            t2->next=NULL;
        }
        if(t1)
        {
            t1->next=h2;
        }
        else if(t1==NULL&&h1==NULL&&h2)
        {
            return h2;
        }
        return h1;
    }
};

代码为C++代码,因为本题没有C提交的接口,但C++的语法兼容C,所以我们可以继续使用C语言知识,class类是以后讲解的内容。

讲解:

所有小于 x 的节点在前,大于等于 x 的节点在后,且两部分内部节点顺序保持原链表顺序。以下是详细讲解:

一、核心思路

  1. 定义两个子链表
    • h1/t1:存储 小于 x 的节点h1 为头指针,t1 为尾指针)。
    • h2/t2:存储 大于等于 x 的节点h2 为头指针,t2 为尾指针)。
  2. 遍历原链表
    逐个节点判断值与 x 的关系,通过 尾插法 接入对应子链表(保持原顺序)。
  3. 拼接两子链表
    将 h1 子链表的尾部(t1->next)连接到 h2 子链表的头部(h2),并处理边界情况(如某一子链表为空)。

二、代码逐段解析

1. 初始化指针
ListNode *h1=NULL,*t1=NULL;  // 小于 x 的链表:头指针h1,尾指针t1
ListNode *h2=NULL,*t2=NULL;  // 大于等于 x 的链表:头指针h2,尾指针t2
ListNode *p=pHead;           // 遍历原链表的指针
  • 初始状态:所有指针均为 NULL,表示两子链表为空。
2. 遍历原链表并分区
while(p) {  // 遍历原链表,p 为当前节点
    if(p->val < x) {  // 当前节点值 < x:接入 h1 子链表
        if(h1 == NULL) {  // h1 为空(首次插入)
            h1 = p;       // h1 指向第一个节点
            t1 = p;       // t1 也指向该节点(尾指针初始化为头节点)
        } else {          // h1 非空(后续插入)
            t1->next = p; // 尾指针 t1 的 next 指向新节点(尾插法)
            t1 = p;       // t1 后移到新的尾部
        }
    } else {  // 当前节点值 >= x:接入 h2 子链表(逻辑同上)
        if(h2 == NULL) {
            h2 = p;
            t2 = p;
        } else {
            t2->next = p;
            t2 = p;
        }
    }
    p = p->next;  // 遍历下一个节点
}
  • 关键逻辑
    • 尾插法:通过 t1->next = p 将新节点接在链表尾部,再移动 t1 到新尾部,确保子链表节点顺序与原链表一致。
    • 首次插入判断:当子链表为空(h1 == NULL 或 h2 == NULL)时,头指针和尾指针均指向当前节点;后续插入仅移动尾指针。
3. 处理边界情况和拼接链表
// ① 处理 h2 子链表的尾部:避免形成循环链表
if(t2) {  // 若 h2 子链表非空(t2 不为 NULL)
    t2->next = NULL;  // 将尾部节点的 next 置空(原链表可能有后续节点,需截断)
}
// ② 拼接 h1 和 h2 子链表
if(t1) {  // 若 h1 子链表非空(t1 不为 NULL)
    t1->next = h2;  // h1 尾部连接 h2 头部
} else if(t1 == NULL && h1 == NULL && h2) {  // 若 h1 为空且 h2 非空
    return h2;  // 直接返回 h2(原链表所有节点均 >= x)
}
// ③ 返回结果:h1 为新链表头(若 h1 为空,返回 h2;h2 为空则返回 NULL)
return h1;
  • 核心操作
    • 截断 h2 尾部t2->next = NULL 至关重要!若不处理,原链表中 h2 尾部节点的 next 可能指向 h1 中的节点,导致拼接后形成 循环链表
    • 拼接逻辑
      • 若 h1 非空(t1 != NULL),直接拼接 t1->next = h2
      • 若 h1 为空(所有节点均 >= x),则返回 h2
      • 若 h1 和 h2 均为空(原链表为空),返回 NULLh1 初始为 NULL)。

6.链表回文结构

链表的回文结构_牛客题霸_牛客网

 .链表回文结构

解题思路:

首先题目:

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。

给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

我们看题目可以得知,链表的长度小于等于900,那么我们可以这样做:

  • 创建一个数组将所有值存储,最后再进行左右端的比较。

例:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList
{
public:
    bool hui(int *a,int left,int right)
    {
        while(leftval;
            p=p->next;
        }
        return hui(node,0,i-1);
    }
};

讲解:

核心思路
  1. 链表转数组:遍历单链表,将所有节点值依次存入数组,将链表的“线性顺序”转换为数组的“随机访问”结构。
  2. 双指针验证回文:使用两个指针分别从数组头部(left=0)和尾部(right=i-1)向中间移动,逐位比较对应元素是否相等。若所有元素均相等,则为回文;否则不是。
bool hui(int *a, int left, int right) {
    while (left < right) {          // 当左指针在右指针左侧时循环
        if (a[left] == a[right]) {  // 若当前左右元素相等
            left++;                 // 左指针右移
            right--;                // 右指针左移
        } else {                    // 若元素不相等,直接返回false(非回文)
            return false;
        }
    }
    return true;                    // 所有元素比较完毕且相等,返回true(回文)
}

思路简单,不过多说明了。

总结

  以上就是今天要讲的内容,本篇文章涉及的知识点为:双向链表知识的相关内容,为本章节知识的内容,希望大家能喜欢我的文章,谢谢各位,接下来的内容我会很快更新。我将更新新知识。

posted @ 2025-12-23 12:37  clnchanpin  阅读(26)  评论(0)    收藏  举报