数据结构:反转链表(reverse the linked list)

目录

通过交换元素值实现反转(reverse by swapping elements)

 滑动指针(sliding pointers)

使用滑动指针反转链表(Reversing a Linked List using Sliding Pointers)

对比分析

如何用递归(Recursion)反转一个单链表


通过交换元素值实现反转(reverse by swapping elements)

为什么我们要讲这个“交换值法”而不是先讲“指针反转”?

因为指针反转属于结构反转,要修改链表的连接方式(更复杂)
而“交换值法”是数据反转,结构不变,只是调换节点里的值,适合入门理解。

(可以参考类似问题:

数据结构:数组:反转数组(Reverse the Array)-CSDN博客

数据结构:反转字符串(Reversing a String)_string反转字符串方法-CSDN博客)

第一性分析:什么是“反转链表”?

假设你有如下链表:

head → [1] → [3] → [5] → [7] → NULL

反转之后应该是:

head → [7] → [5] → [3] → [1] → NULL

你可以看到:链表结构(指针连接顺序)完全不动,只是每个节点的 data 内容被调换了。

那么,第一性问题来了:

❓ 单链表只能顺着走,你怎么访问“最后一个节点”?

我们要反转链表的“数据内容”,但不能直接随机访问链表,因为单链表只能一个方向逐个访问。

所以,我们换一个角度:

用数组作为“中间容器”,把链表的数据转移出来操作,然后再拷贝回链表。

为什么可以这样做?

  • 数组支持随机访问,反转变得容易

  • 链表结构不变,只是内容(data 字段)被替换

  • 指针不涉及改动 → 安全、简单

① 创建数组,边遍历链表边复制 data

int arr[SIZE];  // 假设链表最多 SIZE 个节点
int count = 0;struct Node* temp = head;
while (temp != NULL) {arr[count++] = temp->data;temp = temp->next;
}

② 再次遍历链表,这次写入反转值

temp = head;
for (int i = count - 1; i >= 0; i--) {temp->data = arr[i];temp = temp->next;
}

⚠️ 注意:

  • 你需要预估链表最多多少个节点(用 #define SIZE 100

  • 或者你可以动态分配数组

完整代码实现

#include <stdio.h>
#include <stdlib.h>#define SIZE 100  // 预设最大节点数struct Node {int data;struct Node* next;
};void reverseDataWithArray(struct Node* head) {int arr[SIZE];int count = 0;struct Node* temp = head;// Step 1: 复制 data 到数组中while (temp != NULL) {arr[count++] = temp->data;temp = temp->next;}// Step 2: 再次遍历链表,写入反转值temp = head;for (int i = count - 1; i >= 0; i--) {temp->data = arr[i];temp = temp->next;}
}void printList(struct Node* head) {while (head != NULL) {printf("%d → ", head->data);head = head->next;}printf("NULL\n");
}struct Node* createList() {struct Node* a = malloc(sizeof(struct Node));struct Node* b = malloc(sizeof(struct Node));struct Node* c = malloc(sizeof(struct Node));struct Node* d = malloc(sizeof(struct Node));a->data = 10; a->next = b;b->data = 20; b->next = c;c->data = 30; c->next = d;d->data = 40; d->next = NULL;return a;
}int main() {struct Node* head = createList();printf("Original list:\n");printList(head);reverseDataWithArray(head);printf("After reversing data (using array):\n");printList(head);return 0;
}

 滑动指针(sliding pointers)

第一性推导:从链表遍历说起

单个指针遍历链表:

你最开始遍历链表时,通常会写这样的代码:

struct Node* temp = head;
while (temp != NULL) {// 做点什么,比如打印 temp->datatemp = temp->next;
}

这没问题,但注意:

当你执行 temp = temp->next; 之后,你就永远失去了对原来 temp 的访问

 ❗问题出现:

如果你想在遍历的过程中修改链表结构,比如:

  • 删除当前节点

  • 插入新节点

  • 调换两个节点顺序

  • 反转链表指向方向

你会发现只用一个 temp 指针是不够的。在某些操作后失去访问某个节点 → 链表断开 → 崩溃 

举个例子:尝试改变指针方向

想象链表这样:

head → [10] → [20] → [30] → NULL

 你拿着 curr = head,执行:

curr->next = NULL;

你现在就彻底失去了对 [20][30] 的访问!因为链条被断开,你再也找不到它们

🔍 第一性结论:

在进行结构操作(比如反转、插入、删除)时,你必须在操作之前保存“下一步”的信息

所以你就需要多个指针同时存在,它们滑动地一起遍历链表,互相帮助保存位置

 这就是“滑动指针”的由来。

滑动指针是指你使用多个指针同时向前推进,每个指针记录链表当前不同的“位置状态”,它们就像滑动的窗口一样,彼此协同完成操作。

举例说明最基本的三个滑动指针:

假设我们有这些:

struct Node* prev = NULL;
struct Node* curr = head;
struct Node* next = NULL;

含义如下:

指针名称含义
prev当前节点的前一个节点
curr当前正在处理的节点
next当前节点的下一个节点(提前保存)

它们在遍历中这样滑动:

每次迭代:

  1. 提前保存 curr->nextnext

  2. 修改 curr->next = prev(如果你打算反转)

  3. 整体滑动向前:

prev = curr;
curr = next;

 图示(从左往右):

prev → curr → next → ...

下一轮:

     prev → curr → next → ...

 什么时候需要滑动指针?

你需要三个滑动指针的操作包括但不限于:

  •  反转链表

  •  删除当前节点

  •  插入节点前后结构

  •  在不丢失链表结构的情况下做复杂重构

⚠️ 为什么不只用一个或两个指针?

你可以自己试一下用一个或两个指针做“指针反转”这种操作,你会发现:

总是有一个节点你无法访问,就没法完成链表重构,操作顺序也错了。

滑动指针让你在每一步都有“前后备份”,所以操作安全可控。

 操作顺序

next = curr->next;     // 提前保存下一步
curr->next = prev;     // 操作当前指针(反转、插入、删改)
prev = curr;           // 所有指针前滑
curr = next;

💡 第一性总结:

概念解释
为什么用多个指针避免失去访问链表节点的能力
指针的职责prev:记录已处理区域,curr:当前操作,next:保存未来节点
应用场景修改链表结构时必须使用
本质思想一边操作链表,一边维持三个位置的状态,让链不断、访问安全

使用滑动指针反转链表(Reversing a Linked List using Sliding Pointers)

注意:不是改变节点里面的值,而是改变指针方向。

🚨 单链表只能从前往后遍历

你不能往回走。所以:

每当你打算修改 curr->next,你必须 提前保存 curr->next

所以,我们必须使用三个滑动指针:

struct Node* prev = NULL;
struct Node* curr = head;
struct Node* next = NULL;
名字含义
prev当前节点反转后应该指向的“前一个”
curr当前正在访问的节点
next提前保存的下一个节点

第一性步骤:每一次“滑动”都做这三步

  1. next = curr->next;  → 保存下一步

  2. curr->next = prev; → 改变指向方向

  3. prev = curr; curr = next; → 所有指针前移

空链表、一个节点怎么办?

完全没问题。curr == NULL 时循环直接跳过。

一个节点时,只走一轮,head 自动变成自己,反转不出错

 

步骤目的
保存下一节点防止修改 curr->next 后丢失后续链
修改 curr->next实现反转(原来向右,现在向左)
滑动三个指针向前推进处理下一组节点
更新 head将最终的 prev 作为新的链表头

一步步构建反转函数:reverseUsingPointers

void reverseUsingPointers(struct Node** head) {struct Node* prev = NULL;struct Node* curr = *head;struct Node* next = NULL;while (curr != NULL) {// Step 1: 提前保存下一步next = curr->next;// Step 2: 改变方向curr->next = prev;// Step 3: 所有指针滑动prev = curr;curr = next;}// 最终 prev 是新的头部*head = prev;
}

对比分析

我们要比较两种单链表反转方式:

方法名称核心思路
方法一:拷贝数据法将链表的 data 拷贝到数组,反转后再写回
方法二:滑动指针法(改变结构)修改链表中每个节点的 next 指针方向

我们将从它们的本质、适用性、性能、安全性等方面一一分析。

1. 操作对象本质

 数据拷贝法:

只动数据,不动结构(next 不变)

arr[i] = temp->data;
temp->data = arr[j];

所以它适合在结构不能变动的情况下做反转(如考试题、受限数据结构)

滑动指针法:

完全不动数据,只动结构(指针方向反过来)

curr->next = prev;

这本质上是“链表重构”,适合任何允许你改变链结构的环境,效率也更高

 2. 数据结构的复杂性影响

❗ 如果每个节点只存一个 int,拷贝没什么压力

但现实中常常这样设计链表节点:

struct Node {char name[100];int id;float salary;struct Node* next;
};

这时候每个节点里的 data 是一个结构体(甚至可能更复杂)

→ 如果你用“拷贝数据法”,你必须完整复制这些字段

→ 可能需要自定义拷贝函数、深拷贝、数组容量等问题

当节点的 data 很复杂(结构体、嵌套对象、大量数据)时,滑动指针法明显更好

3. 时间复杂度对比

操作时间复杂度原因
数据拷贝法O(n) + O(n) = O(n)两次遍历:拷贝 + 写回
滑动指针法O(n)一次遍历:边滑动边反转指针

虽然复杂度都是 O(n),但滑动指针法更省,只有一次遍历和一次拷贝

4. 空间复杂度对比

方法空间开销说明
拷贝数据法O(n) 额外数组空间存储每个 data 的值
滑动指针法O(1)(只用 3 个指针)不需要额外数组

5. 安全性与一致性

 滑动指针法:

  • 不会拷贝错误

  • 不关心 data 内容是什么(结构再复杂都不管)

  • 不会遇到内存越界等数组问题

 数据拷贝法:

  • 要保证数组空间够大

  • 要小心拷贝结构体或深拷贝问题

  • 容易写错或越界(尤其是动态链表)

因此,滑动指针法更通用、更安全、更节省空间,更适合实际工程和复杂数据结构, 数据拷贝法仅适合学习、数据简单、结构不允许改动时使用 。

你应该选择滑动指针法的理由总结:

  1. 不管节点里放什么,它都不在乎

  2. 永远只用三个指针,不会爆内存

  3. 一次遍历搞定,比拷贝更省时间

  4. 改变结构更符合“链表反转”的语义


如何用递归(Recursion)反转一个单链表

第一性原则出发点:什么是递归?

递归是指:一个函数在内部调用自己,并且有:

  1. 基本情况(Base Case):终止条件

  2. 递归情况(Recursive Case):把当前问题拆分成更小问题交给自己来做

第一步:递归函数的思考

❓我们要问:我们可以用递归处理谁?

答案是:

让递归先反转链表的后面部分,我们只处理“当前节点和它的 next”

所以递归函数的含义可以设定为:

void Reverse(Node* prev, Node* curr)
  • curr:当前节点(当前要处理的)

  • prev:当前节点 curr 的前驱(上一个)

这是一个滑动指针式递归反转方案。

你会从头开始调用:

Reverse(NULL, head);

每一次递归,你都把当前 curr 当作下一轮的 prev,curr->next 当作下一轮的 curr

也就是说,每一层你都像这样“向前推进”:

Reverse(curr, curr->next);

一层层推进(调用栈):

调用层prevcurr
1NULL1
212
323
434
54NULL ← base case

第二步:Base Case(终止条件)

当你递归到最后一个节点,链表已经不能再往下了:

if (curr == NULL) {first = prev;return;
}
  • 如果当前处理节点 curr 已经是 NULL,说明我们走到了原链表的末尾

  • 此时 prev 是最后一个有效节点,应该变成新的头节点

  • 所以设置 first = prev(这个变量是全局的链表头)

第三步:Recursive Case(递归处理)

每一层执行:

Reverse(curr, curr->next);

也就是说:

  • 下一轮,curr 成为 prev

  • curr->next 成为新的 curr

这相当于向后滑动一格,保持一个“两个指针的窗口”,像滑动指针一样

第四步:反转指针

递归完成之后,再回来时:

curr->next = prev;

你把当前节点的 next 指向前一个节点

🔄 反转动作在哪里发生?

不是在前进时做的,而是在递归“回溯”过程中做的

用第一性逐层模拟

假设链表为:[1] → [2] → [3] → NULL

初始调用:

Reverse(NULL, 1)

→ 递归进入 Reverse(1, 2)
→ 递归进入 Reverse(2, 3)
→ 递归进入 Reverse(3, NULL)
→ 此时:curr == NULL, 执行:first = prev → 即 first = 3 

然后回溯阶段

curr = 3, prev = 2 → 3->next = 2  
curr = 2, prev = 1 → 2->next = 1  
curr = 1, prev = NULL → 1->next = NULL

得到新链表

first → [3] → [2] → [1] → NULL
阶段操作含义
递归推进Reverse(curr, curr->next)保存前一个和当前节点,深入到末尾
递归终止if (curr == NULL)到达末尾,设置新的链表头 first = prev
回溯反转指向curr->next = prev每层反转指向,把当前节点指向前一个节点

完整代码

void Reverse(Node* prev, Node* curr) {if (curr != NULL) {Reverse(curr, curr->next);  // 向后推进curr->next = prev;          // 回溯过程中修改指向} else {first = prev;            // 递归到底,设置新头}
}

为什么这种方法优雅?

  • 结构清晰,变量少(只用两个滑动指针)

  • 不需要返回值,不需要临时数组

  • 完全递归思维,没有额外变量干扰

  • 最后由最深层设置头结点,不需要手动更新 head = ...

改进建议(可选):

如果你希望去掉全局变量 first,可以把第三个参数传进去(如 Node** head
或者让 Reverse 返回新的头结点(需要改写递归结构)

但在这个思路在学习递归本质、滑动窗口逻辑上是极好的。

⚠️ 补充:递归的缺点

虽然递归很优雅,但它有函数调用栈的开销,在链表很长(如几万节点)时,可能会造成栈溢出

如果你要处理长链表,推荐用迭代方式(滑动指针法)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/92017.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/92017.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C#】基于SharpCompress实现压缩包解压功能

1.SharpCompress安装 在vs的nuget下搜索安装SharpCompress&#xff0c;如图所示2.解压缩包功能实现 /// <summary> /// 解压压缩包 /// </summary> /// <param name"filePath">压缩包文件路径</param> /// <param name"directoryPat…

mybatis连接PGSQL中对于json和jsonb的处理方法

pgsql数据库表字段设置了jsonb格式&#xff1b;在java的实体里使用String或者对象转换会一直提示一个错误&#xff1a; Caused by: org.postgresql.util.PSQLException: ERROR: column “xx” is of type jsonb but expression is of type character varying 需要加一个转换方法…

Spring AI Alibaba Graph 深度解析:原理、架构与应用实践

1. 引言概述 1.1 什么是 Spring AI Alibaba Graph Spring AI Alibaba Graph 是阿里云团队基于 Spring AI 生态开发的一个强大的工作流编排框架&#xff0c;专门用于构建复杂的 AI 应用。它采用声明式编程模型&#xff0c;通过图结构来定义和管理 AI 工作流&#xff0c;让开发…

C++少儿编程(二十一)—软件执行流程

让我们将以下程序视为用C编写的示例程序。步骤1&#xff1a;预处理器将源代码转换为扩展代码。当您运行程序时&#xff0c;源代码首先被发送到称为预处理器的工具。预处理器主要做两件事&#xff1a;它会从程序中删除注释。它扩展了预处理器指令&#xff0c;如宏或文件包含。它…

精通Webpack搭建Vue2.0项目脚手架指南

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;在Web应用程序开发中&#xff0c;Vue 2.0因其虚拟DOM、单文件组件、增强的生命周期钩子和Vuex及Vue Router状态管理与路由解决方案&#xff0c;成为了提高开发效率和代码组织性的关键。Webpack作为必不可少的模…

无偿分享120套开源数据可视化大屏H5模板

数据可视化跨越了语言、技术和专业的边界&#xff0c;是能够推动实现跨界沟通&#xff0c;实现国际间跨行业的创新的工具。正如画家用颜料表达自我&#xff0c;作者用文字讲述故事&#xff0c;而统计人员用数字沟通 ...... 同样&#xff0c;数据可视化的核心还是传达信息。而设…

Qt按键响应

信号与槽机制是一个非常强大的事件通信机制&#xff0c;是 Qt 最核心的机制之一&#xff0c;初学者掌握它之后&#xff0c;几乎可以做任何交互操作。信号&#xff08;Signal&#xff09; 是一种“事件”或“通知”&#xff0c;比如按钮被点击、文本改变、窗口关闭等。 槽&#…

【Git】常见命令整理

Git分区与操作关系&#xff1a;Working Directory&#xff08;工作区&#xff0c;对于本地的编辑和修改在此进行&#xff09;->Staging Area&#xff08;暂存区/Index&#xff0c;在工作区进行git add操作后的位置&#xff09;->Git Repository&#xff08;本地仓库&…

Linux-Shell脚本基础用法

1.变量定义变量命名规则&#xff1a;可以包含字母&#xff0c;数字&#xff0c;下划线&#xff0c;首字母不能用数字开头&#xff0c;中间不能又空格&#xff1b;为变量赋值等号之间不能为空格&#xff1b;变量命名不能使用标点符号&#xff0c;不能使用bash的关键字&#xff1…

JS中的Map和WeakMap区别和联系

JavaScript 中 Map 与 WeakMap 的区别、联系及示例核心区别特性MapWeakMap键的类型允许任意类型的键&#xff08;对象、原始值&#xff09;键必须是对象&#xff08;非原始值&#xff09;垃圾回收强引用键 → 阻止垃圾回收弱引用键 → 不影响垃圾回收可遍历性支持遍历&#xff…

Linux 环境 libpq加载异常导致psql 连接 PostgreSQL 库失败失败案例

文章目录局点现象定位结论局点环境补充知识点如下库文件加载顺序关键事实&#xff1a;您系统中的证据&#xff1a;优先级对比表&#xff1a;解决方案强化&#xff1a;最终检查&#xff1a;本局点解决方法局点现象 数据库 mdm 升级失败检查日志, 发现是由于 psql 连接数据库报错…

C# XML 文件

在 C# 中处理 XML 文件是非常常见的操作&#xff0c;可以使用System.Xml命名空间中的类来实现。以下是一些常用的 XML 操作示例&#xff1a; 手册链接&#xff1a; System.Xml 命名空间 XmlDocument 创建一个xml数据格式的文档 XmlDocument xml new XmlDocument(); Xml…

LOVON——面向足式Open-Vocabulary的物体导航:LLM做任务分解、YOLO11做目标检测,最后L2MM将指令和视觉映射为动作(且解决动态模糊)

前言 因为项目需要(比如我们在做的两个展厅讲解订单)&#xff0c;近期我一直在研究VLN相关&#xff0c;有些工作哪怕暂时还没开源(将来可能会开源)&#xff0c;但也依然会解读&#xff0c;比如好处之一是构建完整的VLN知识体系&#xff0c;本文便是其中一例 我在解读过程中&am…

【Django】-3- 处理HTTP响应

HttpResponse 家族” 的常用操作&#x1f31f;1. 设置状态码 &#x1f44b;状态码是服务器告诉客户端 “请求处理结果” 的数字暗号&#xff08;比如 404 表示 “没找到页面”&#xff09;。Django 里有 3 种设置方式&#xff1a;方式 1&#xff1a;直接写数字&#xff08;简单…

《React Router深解:复杂路由场景下的性能优化与导航流畅性构建》

路由系统是连接用户操作与应用功能的中枢神经,而React Router作为React生态中处理路由逻辑的核心工具,其在复杂应用中的表现直接决定着用户体验的优劣。当应用规模扩张至数十甚至上百个路由,嵌套层级跨越多层,导航控制中的性能问题便会逐渐凸显——从首屏加载的延迟到路由切…

网络与信息安全有哪些岗位:(4)应急响应工程师

想知道网络与信息安全领域有哪些具体岗位吗&#xff1f; 网络与信息安全有哪些岗位&#xff1a;&#xff08;1&#xff09;网络安全工程师-CSDN博客 网络与信息安全有哪些岗位&#xff1a;&#xff08;2&#xff09;渗透测试工程师_网络安全渗透工程师-CSDN博客 网络与信息安…

Leetcode 3634. Minimum Removals to Balance Array

Leetcode 3634. Minimum Removals to Balance Array 1. 解题思路2. 代码实现 题目链接&#xff1a;3634. Minimum Removals to Balance Array 1. 解题思路 这一题思路上就是一个滑动窗口的思路。 我们首先将整个数组有序排列&#xff0c;然后分别从左向右考察每一个元素作为…

C#/.NET/.NET Core优秀项目和框架2025年7月简报

前言 每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架&#xff08;每周至少会推荐两个优秀的项目和框架当然节假日除外&#xff09;&#xff0c;推文中有项目和框架的详细介绍、功能特点、使用方式以及部分功能截图等。注意&#xff1a;排名不分先后&#xff0c;都是十分…

第 10 篇:深度学习的“军火库”——CNN、RNN与Transformer,AI如何看懂世界?

《人工智能AI之机器学习基石》系列⑩ 专栏核心理念: 用通俗语言讲清楚机器学习的核心原理,强调“洞察 + 技术理解 + 应用连接”,构建一个完整的、富有启发性的知识体系。 引

深度学习—功能性函数代码 common.py

函数&#xff1a;返回GPU def try_gpu(i0): #save"""如果存在&#xff0c;则返回gpu(i)&#xff0c;否则返回cpu()"""if torch.cuda.device_count() > i 1: # 如果存在第 i 个 GPUreturn torch.device(fcuda:{i}) # 返回第 i 个 GPU 设…