news 2026/4/23 6:44:33

从零开始写算法——链表篇5:K个一组翻转链表 + 排序链表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始写算法——链表篇5:K个一组翻转链表 + 排序链表

在之前的链表学习中,我们掌握了基本的增删改查和双指针技巧。今天,我们要挑战链表操作的“深水区”。

我们将通过两个非常有代表性的题目:K个一组翻转链表链表排序,来探讨如何在复杂的指针变换中保持逻辑清晰,以及如何将分治算法完美应用到链表结构中。这两个问题不依赖额外的数据结构,完全依靠对next指针的掌控,是磨练代码基本功的绝佳素材。


一、 K个一组翻转链表 (Reverse Nodes in k-Group)

这个问题的难点不在于“翻转”,而在于“分组”和“缝合”。我们需要将链表切分成若干个长度为 k 的小段,对每一段进行内部翻转,然后再把这些翻转后的小段按照原来的顺序重新连起来。

1. 核心思路:维护“守门员” p0

如果只是单纯翻转一个链表,我们只需要precurnxt三个指针就够了。但在 K 个一组的场景下,每一组翻转后,都需要跟上一组的尾巴下一组的头建立连接。

因此,我们需要一个关键指针p0

  • p0 的定义:它始终指向当前待翻转这一组的前一个节点。

  • 作用:它就像一个“守门员”或“锚点”,负责在这一组翻转完毕后,把断开的链表重新缝合起来。

2. 代码解析

代码采用了先统计长度,再循环处理的逻辑。

C++代码实现:

class Solution { public: ListNode* reverseKGroup(ListNode* head, int k) { // 1. 统计链表总长度 ListNode* cur = head; int len = 0; while (cur) { len += 1; cur = cur->next; } // 2. 初始化 dummy 节点和 p0 ListNode dummy(0, head); ListNode* p0 = &dummy; cur = head; // cur 复位,准备开始干活 ListNode* pre = nullptr; // 3. 循环处理每一组,条件是剩余长度 >= k while (len >= k) { len -= k; // --- 内部翻转逻辑 (标准的链表翻转) --- for (int i = 0; i < k; ++i) { ListNode* nxt = cur->next; cur->next = pre; pre = cur; cur = nxt; } // --- 缝合逻辑 (最难理解的部分) --- // 此时状态: // p0 -> [原来的头...原来的尾] -> cur(下一组头) // 翻转后: // p0 依然指向 dummy/上一组尾 // pre 指向 [现在的头(原来的尾)] // p0->next 指向 [现在的尾(原来的头)] ListNode* nxt = p0->next; // nxt 暂存的是这一组“原来的头”,现在它变成了尾巴 // 步骤 A: 将 p0 连接到 现在的头 (pre) // 这一步修正了上一组和当前组的连接 p0->next->next = cur; p0->next = pre; // 步骤 B: 移动 p0 // p0 移动到这一组的尾巴 (也就是 nxt),为下一组做好准备 p0 = nxt; } return dummy.next; } };

3. 难点拆解:缝合过程

代码中最晦涩的这四行:

C++代码实现:

ListNode* nxt = p0->next; p0->next->next = cur; p0->next = pre; p0 = nxt;

图解:

我们可以这样理解:

  1. 锁定旧头nxt = p0->next。因为翻转前p0->next指向这一组的第一个元素(例如 1),翻转后它变成了这组的最后一个元素。

  2. 连接下组p0->next->next = cur。也就是让 1 指向下一组的开头(例如 4)。等价于1->next = 4

  3. 连接上组p0->next = pre。让p0指向这一组翻转后的新头(例如 3)。等价于dummy->next = 3

  4. 移动 p0p0 = nxt。当前组处理完了,p0跳到 1 的位置,蹲守在 4 的前面,准备处理下一组。

4. 复杂度分析

  • 时间复杂度:O(N)。我们需要先遍历一次求长度,然后再次遍历翻转。每个节点最多被访问两次,所以是线性的。

  • 空间复杂度:O(1)。我们可以看到,除了几个指针变量外,没有使用额外的数组或栈空间,是原地算法。


二、 链表排序 (Sort List)

在数组排序中,快速排序通常是首选。但在链表中,由于无法随机访问(无法 O(1) 获取中间元素),快速排序的性能会大打折扣。因此,归并排序 (Merge Sort)是链表排序的最佳拍档。

1. 核心思路:分治法 (Divide and Conquer)

归并排序的核心就三步:

  1. 找中点 (Cut):把链表从中间切断,分成左右两半。

  2. 递归 (Sort):对左右两半分别进行排序。

  3. 合并 (Merge):将两个有序的链表合并成一个有序链表。

2. 代码解析

C++代码实现:

class Solution { // 辅助函数1:找中点并断开链表 ListNode* getMidNode(ListNode* head) { ListNode* slow = head; ListNode* fast = head; ListNode* pre = nullptr; // 用于记录中点的前一个节点,方便断链 // 快慢指针找中点 while(fast && fast->next) { pre = slow; slow = slow->next; fast = fast->next->next; } // 关键操作:断开链表! // 如果不断开,这就不是分治,而是死循环 if (pre != nullptr) { pre->next = nullptr; } return slow; // slow 就是后半段的头 } // 辅助函数2:合并两个有序链表 (双指针拉拉链) ListNode* merge(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* cur = &dummy; while (l1 && l2) { if (l1->val < l2->val) { cur->next = l1; l1 = l1->next; } else { cur->next = l2; l2 = l2->next; } cur = cur->next; } // 接上剩余的部分 cur->next = l1 ? l1 : l2; return dummy.next; } public: ListNode* sortList(ListNode* head) { // 递归终止条件:链表为空或只有一个节点 if (head == nullptr || head->next == nullptr) { return head; } // 1. 分:找到中点,head 是前半段,head2 是后半段 ListNode* head2 = getMidNode(head); // 2. 治:递归排序左右两半 head = sortList(head); head2 = sortList(head2); // 3. 合:合并两个有序链表 return merge(head, head2); } };

3. 细节剖析

  • 找中点技巧:使用快慢指针(Slow/Fast Pointers)。快指针一次走两步,慢指针一次走一步。当快指针走到头时,慢指针刚好在中间。

  • 断链操作:在getMidNode中,我们需要维护一个pre指针指向慢指针的前一个节点。找到中点后,执行pre->next = nullptr。这一步至关重要,它将大链表物理上拆分成了两个独立的链表,否则递归无法收敛。

  • Base Caseif (head == nullptr || head->next == nullptr)。这是递归的出口,当链表被拆解成单个节点时,单个节点天然就是有序的。

4. 复杂度分析

  • 时间复杂度:O(N log N)。

    • 分治的过程就像一棵二叉树,层数为 log N。

    • 每一层合并操作需要遍历所有节点,耗时 N。

    • 总耗时为 N * log N。

  • 空间复杂度:O(log N)。

    • 虽然我们没有申请数组,但是递归调用需要使用系统栈。

    • 栈的深度取决于递归的层数,也就是链表切分的次数,即 log N。

    • 注:如果采用自底向上的迭代式归并排序,空间复杂度可以优化到 O(1),但代码可读性会变差,递归写法通常是标准解法。


三、 总结

这两道题目代表了链表操作的两个高峰:

  1. K个一组翻转:考验的是局部操作与整体连接的协调能力。通过引入p0指针,我们将复杂的切分和重连问题转化为了标准的“反转+缝合”流程。

  2. 链表排序:考验的是分治思想在链表上的落地。通过快慢指针解决切分问题,通过递归解决排序问题,这是解决复杂链表问题的通用模板。

掌握了这两个“大杀器”,再回头看普通的链表反转或合并,就会觉得游刃有余了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 14:23:23

腾讯混元视频生成模型:打破闭源技术垄断的开源革命

腾讯混元视频生成模型&#xff1a;打破闭源技术垄断的开源革命 【免费下载链接】HunyuanVideo 项目地址: https://ai.gitcode.com/hf_mirrors/tencent/HunyuanVideo 在文生视频技术快速迭代的今天&#xff0c;开发者们面临着一个共同的困境&#xff1a;要么选择性能有限…

作者头像 李华
网站建设 2026/4/22 18:10:58

GetQzonehistory:一键备份QQ空间说说的终极解决方案

在数字记忆日益珍贵的今天&#xff0c;QQ空间承载着我们太多青春回忆。那些年写过的说说、上传的照片、收到的留言&#xff0c;都是无法复制的人生片段。GetQzonehistory作为一款专业的QQ空间数据备份工具&#xff0c;让每个人都能轻松保存这些珍贵数字内容。 【免费下载链接】…

作者头像 李华
网站建设 2026/4/22 21:07:41

普中51单片机学习笔记-DS1302实时时钟芯片

芯片简介DS1302是Dallas Semiconductor&#xff08;现为Maxim Integrated&#xff09;推出的涓流充电实时时钟芯片&#xff0c;主要特点&#xff1a;实时时钟功能&#xff1a;秒、分、时、日、月、星期、年&#xff08;2000年闰年补偿&#xff09;31字节RAM&#xff1a;用于数据…

作者头像 李华
网站建设 2026/4/17 13:22:13

基于Nginx和Python的动态站点安装配置

1.8 Nginx 部署 Python Web 项目实战教程 1.8.1 Django 项目部署 核心原理 Django 是 Python 重量级 Web 框架&#xff0c;自带开发服务器仅适用于调试&#xff0c;生产环境需搭配 uWSGI&#xff08;WSGI 服务器&#xff09; Nginx&#xff08;反向代理&#xff09;&#xff1a…

作者头像 李华
网站建设 2026/4/21 21:55:24

BMAD-METHOD:重新定义AI时代的人机协作开发模式

BMAD-METHOD&#xff1a;重新定义AI时代的人机协作开发模式 【免费下载链接】BMAD-METHOD Breakthrough Method for Agile Ai Driven Development 项目地址: https://gitcode.com/gh_mirrors/bm/BMAD-METHOD 在人工智能技术迅猛发展的今天&#xff0c;开发者面临着前所未…

作者头像 李华
网站建设 2026/4/18 16:08:03

SharedArrayBuffer 和 Atomics API 详解(附:Atomics 对象方法总结表)

由于Spectre和Meltdown的漏洞&#xff0c;所有主流浏览器在2018年1月就禁用了sharedArrayBuffer。从2019年开始&#xff0c;有些浏览器开始逐步重新启用这一特性。既不克隆&#xff0c;也不转移&#xff0c;sharedArrayBuffer作为ArrayBuffer能够在不同浏览器上下文间共享。在把…

作者头像 李华