大四双非春招学习记录-K 个一组反转链表

手撕 K 个一组反转链表,这些细节你必须知道!

前言

大四双非春招学习记录

LeetCode 第 25 题「K 个一组翻转链表」是链表类题目中的困难题,也是面试中的高频考题。很多同学看到「困难」标签就望而却步,但实际上,只要掌握了核心思路,这道题并没有想象中那么难。

本文将带你从零开始,逐步攻克这道题,并总结出通用的解题模板。

一、题目理解

1.1 题目描述

给你一个链表,每 k 个节点一组进行反转,返回反转后的链表。如果节点总数不是 k 的整数倍,最后剩余的节点保持原有顺序。

示例 1:

复制代码
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

图解:

复制代码
1 -> 2 -> 3 -> 4 -> 5
↓ 反转第一组
2 -> 1 -> 3 -> 4 -> 5
↓ 反转第二组
2 -> 1 -> 4 -> 3 -> 5

示例 2:

复制代码
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]

1.2 关键信息提取

  • ✅ 每 k 个节点一组进行反转
  • ✅ 不足 k 个的节点保持原样
  • ✅ 需要原地修改链表(不能只改值)
  • ✅ 空间复杂度要求 O(1)

二、前置知识

在攻克这道题之前,你需要掌握以下基础:

2.1 链表的基本操作

javascript 复制代码
// 链表节点定义
function ListNode(val, next) {
    this.val = val === undefined ? 0 : val;
    this.next = next === undefined ? null : next;
}

// 遍历链表
let cur = head;
while (cur) {
    console.log(cur.val);
    cur = cur.next;
}

2.2 反转整个链表

javascript 复制代码
function reverseList(head) {
    let prev = null;
    let cur = head;
    while (cur) {
        let next = cur.next;  // 保存下一个节点
        cur.next = prev;       // 反转指针
        prev = cur;            // 移动 prev
        cur = next;            // 移动 cur
    }
    return prev;  // 返回新的头节点
}

图解反转过程:

复制代码
初始: null <- 1 -> 2 -> 3
第一步: null <- 1 <- 2 -> 3
第二步: null <- 1 <- 2 <- 3
完成: 3 -> 2 -> 1 -> null

2.3 虚拟头节点技巧

虚拟头节点(dummy node)是链表题目中常用的技巧,可以统一处理边界情况

javascript 复制代码
// 没有虚拟头节点
let newHead = head;
if (条件) {
    newHead = head.next; // 需要特殊处理
}

// 使用虚拟头节点
let dummy = new ListNode(0);
dummy.next = head;
let prev = dummy; // 统一处理,不需要特殊判断

三、核心思路

3.1 整体流程

K 个一组反转链表的核心思路可以概括为 4 步:

复制代码
┌─────────────────────────────────────┐
│  1. 找到当前组的头节点和尾节点        │
│  2. 保存下一组的起始节点              │
│  3. 反转当前组                       │
│  4. 连接回原链表                     │
└─────────────────────────────────────┘
                ↓
        重复以上步骤直到结束

3.2 图解整体流程

1->2->3->4->5, k=2 为例:

复制代码
初始状态:
dummy → 1 → 2 → 3 → 4 → 5 → null
  ↑
 prev

第1组(节点1-2):
dummy → 1 → 2 → 3 → 4 → 5 → null
        ↑   ↑
      head tail

反转后:
dummy → 2 → 1 → 3 → 4 → 5 → null
        ↑   ↑
      head tail

连接并移动指针:
dummy → 2 → 1 → 3 → 4 → 5 → null
            ↑   ↑
          prev head(下一组起点)

第2组(节点3-4):
dummy → 2 → 1 → 3 → 4 → 5 → null
                ↑   ↑
              head tail

反转后:
dummy → 2 → 1 → 4 → 3 → 5 → null
                ↑   ↑
              head tail

连接并移动指针:
dummy → 2 → 1 → 4 → 3 → 5 → null
                    ↑
                  prev
                   head(null,结束)

四、代码实现

4.1 完整代码(迭代法)

javascript 复制代码
var reverseKGroup = function(head, k) {
    // 边界情况
    if (!head || k === 1) return head;
    
    // 创建虚拟头节点
    let dummy = new ListNode(0);
    dummy.next = head;
    let prev = dummy;  // prev 指向每组的前一个节点
    let cur = head;
    
    // 先计算链表长度
    let len = 0;
    let p = head;
    while (p) {
        len++;
        p = p.next;
    }
    
    // 需要反转的组数
    let groups = Math.floor(len / k);
    
    // 反转每一组
    for (let i = 0; i < groups; i++) {
        // 反转当前组的 k 个节点
        for (let j = 1; j < k; j++) {
            let next = cur.next;           // 要移动的节点
            cur.next = next.next;          // 跳过 next 节点
            next.next = prev.next;         // next 指向当前组的头
            prev.next = next;              // prev 指向新的头
        }
        // 移动指针到下一组
        prev = cur;
        cur = cur.next;
    }
    
    return dummy.next;
};

4.2 更易理解的版本(带辅助函数)

javascript 复制代码
var reverseKGroup = function(head, k) {
    // 创建虚拟头节点
    let dummy = new ListNode(0);
    dummy.next = head;
    let prev = dummy;
    
    while (head) {
        // 1. 找到当前组的尾节点
        let tail = prev;
        for (let i = 0; i < k; i++) {
            tail = tail.next;
            if (!tail) return dummy.next;  // 不足 k 个,直接返回
        }
        
        // 2. 保存下一组的起始节点
        let nextGroup = tail.next;
        
        // 3. 反转当前组 [head, tail]
        [head, tail] = reverseList(head, tail);
        
        // 4. 连接回原链表
        prev.next = head;
        tail.next = nextGroup;
        
        // 5. 移动指针到下一组
        prev = tail;
        head = nextGroup;
    }
    
    return dummy.next;
};

// 反转链表的一部分 [head, tail]
function reverseList(head, tail) {
    let prev = tail.next;  // 关键:prev 指向 tail 的下一个节点
    let cur = head;
    while (prev !== tail) {
        let next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return [tail, head];  // 返回新的头尾
}

4.3 递归解法(简洁优雅)

javascript 复制代码
var reverseKGroup = function(head, k) {
    // 找到第 k+1 个节点
    let tail = head;
    for (let i = 0; i < k; i++) {
        if (!tail) return head;  // 不足 k 个,不反转
        tail = tail.next;
    }
    
    // 反转前 k 个节点
    let newHead = reverseFirstK(head, k);
    
    // 递归处理后续节点
    head.next = reverseKGroup(tail, k);
    
    return newHead;
};

// 反转前 k 个节点,返回新的头节点
function reverseFirstK(head, k) {
    let prev = null;
    let cur = head;
    for (let i = 0; i < k; i++) {
        let next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
}

五、关键点详解

5.1 为什么需要虚拟头节点?

javascript 复制代码
// 没有虚拟头节点时,第一组需要特殊处理
let newHead = head;
if (第一组需要反转) {
    newHead = 反转后的头;
}

// 使用虚拟头节点,统一处理
let dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;  // prev 始终指向"前一个节点"

5.2 反转函数中的 prev 为什么要指向 tail.next?

javascript 复制代码
function reverseList(head, tail) {
    let prev = tail.next;  // 关键点!
    let cur = head;
    while (prev !== tail) {
        let next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return [tail, head];
}

图解说明:

复制代码
反转前:head → ... → tail → nextGroup → ...
               ↓
反转时:prev 应该指向 nextGroup
这样反转后,tail 的 next 自然就指向 nextGroup

5.3 为什么反转后要返回 [tail, head]?

javascript 复制代码
// 反转前
[head] → ... → [tail] → nextGroup

// 反转后
[tail] → ... → [head] → nextGroup
// ↑新头    ↑新尾

// 所以返回 [新头, 新尾] = [tail, head]

六、常见错误与调试

错误 1:指针丢失

javascript 复制代码
// ❌ 错误写法
let next = cur.next;
cur.next = prev;
prev = cur;
cur = cur.next;  // 此时 cur.next 已经被修改!

// ✅ 正确写法
let next = cur.next;
cur.next = prev;
prev = cur;
cur = next;  // 使用之前保存的 next

错误 2:边界判断错误

javascript 复制代码
// ❌ 错误:提前 break 会导致逻辑混乱
for (let i = 0; i < k; i++) {
    tail = tail.next;
    if (!tail) break;
}

// ✅ 正确:直接返回
for (let i = 0; i < k; i++) {
    tail = tail.next;
    if (!tail) return dummy.next;
}

错误 3:连接顺序错误

javascript 复制代码
// ❌ 错误
prev.next = tail;    // tail 是反转前的尾
head.next = nextGroup;

// ✅ 正确
prev.next = head;    // head 现在是反转后的头
tail.next = nextGroup;

七、测试用例

javascript 复制代码
// 辅助函数:数组转链表
function arrayToList(arr) {
    let dummy = new ListNode(0);
    let cur = dummy;
    for (let val of arr) {
        cur.next = new ListNode(val);
        cur = cur.next;
    }
    return dummy.next;
}

// 辅助函数:链表转数组
function listToArray(head) {
    let result = [];
    while (head) {
        result.push(head.val);
        head = head.next;
    }
    return result;
}

// 测试用例1:正常情况
let head1 = arrayToList([1,2,3,4,5]);
console.log(listToArray(reverseKGroup(head1, 2))); // [2,1,4,3,5]

// 测试用例2:k=1
let head2 = arrayToList([1,2,3,4,5]);
console.log(listToArray(reverseKGroup(head2, 1))); // [1,2,3,4,5]

// 测试用例3:k 大于链表长度
let head3 = arrayToList([1,2,3]);
console.log(listToArray(reverseKGroup(head3, 5))); // [1,2,3]

// 测试用例4:正好整数倍
let head4 = arrayToList([1,2,3,4]);
console.log(listToArray(reverseKGroup(head4, 2))); // [2,1,4,3]

// 测试用例5:空链表
console.log(listToArray(reverseKGroup(null, 2))); // []

// 测试用例6:单节点
let head6 = arrayToList([1]);
console.log(listToArray(reverseKGroup(head6, 2))); // [1]

八、复杂度分析

解法 时间复杂度 空间复杂度
迭代法 O(n) O(1)
递归法 O(n) O(n/k)(递归栈)
  • 时间复杂度:每个节点被访问常数次,总体 O(n)
  • 空间复杂度:迭代法 O(1),递归法 O(n/k)(递归深度)

九、相关题目推荐

掌握了这道题,下面这些题目会变得简单很多:

题目 难度 相似度 核心区别
206. 反转链表 简单 ⭐⭐⭐ 整体反转
92. 反转链表 II 中等 ⭐⭐⭐⭐ 反转指定区间
24. 两两交换链表中的节点 中等 ⭐⭐⭐⭐⭐ k=2 的特例
61. 旋转链表 中等 ⭐⭐ 链表旋转

十、面试技巧

面试官可能会问的问题:

Q1:能否用递归实现?时间复杂度是多少?

可以,递归实现更简洁,但空间复杂度 O(n/k)(递归栈深度)。如果 k 很大,可能会导致栈溢出。

Q2:如果 k=0 怎么办?

k 是正整数,题目保证 k>0。但可以和面试官讨论边界处理。

Q3:如何测试你的代码?

可以从以下角度测试:

  • 空链表
  • 单节点链表
  • k=1
  • k=链表长度
  • 链表长度正好是 k 的整数倍
  • 链表长度不是 k 的整数倍

Q4:能优化吗?

可以先遍历一次计算长度,避免在循环中重复检查边界。

十一、总结口诀

复制代码
创建虚拟头,prev 指向它
循环条件 head 不空
找够 k 个点,不够就回家
保存下一组,反转当前它
连接前后段,指针往后拉
重复以上步,直到结束啦

写在最后

K 个一组反转链表虽然标记为「困难」,但它本质上是「反转链表」+「分组处理」的组合。只要掌握了基础的反转算法,理解了指针的操作,这道题就能迎刃而解。

记住核心心法

  1. 先找到一组
  2. 反转它
  3. 接回去
  4. 找下一组

建议多画图、多调试,把指针的变化过程在脑海中过一遍。当你能够清晰地画出每一步的指针变化,代码自然就写出来了。

如果这篇文章对你有帮助,欢迎点赞收藏!也欢迎在评论区交流讨论~

相关推荐
奶人五毛拉人一块2 小时前
模板与vector的学习
数据结构·学习·迭代器·vector·模板
ambition202422 小时前
【算法详解】飞机降落问题:DFS剪枝解决调度问题
c语言·数据结构·c++·算法·深度优先·图搜索算法
EnglishJun2 小时前
ARM嵌入式学习(十八)--- Linux的内核编译和启动
linux·运维·学习
I Promise342 小时前
C++ 基础数据结构与 STL 容器详解
开发语言·数据结构·c++
星幻元宇VR2 小时前
VR旋转蛋椅:沉浸式安全科普新体验
科技·学习·安全·vr·虚拟现实
ZhiqianXia2 小时前
PyTorch 学习笔记(12):ATen C++ 算子引擎的完整架构之旅
pytorch·笔记·学习
旖-旎2 小时前
链表(两两交换链表中的节点)(2)
数据结构·c++·学习·算法·链表·力控
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB的分片管理(17)
数据库·学习·mongodb
世人万千丶2 小时前
Flutter 框架跨平台鸿蒙开发 - 嫉妒分析器应用
学习·flutter·华为·开源·harmonyos·鸿蒙