【LeetCode Hot100 刷题日记 (31/100)】25. K 个一组翻转链表 —— 链表、分组操作、递归、指针操作🔄

5 阅读7分钟

📌 题目链接:25. K 个一组翻转链表 - 力扣(LeetCode)

🔍 难度:困难 | 🏷️ 标签:链表、分组操作、递归、指针操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(若使用迭代)或 O(n/k)(若使用递归,因函数调用栈)


🧠 题目分析

本题要求将链表按 每 k 个节点为一组进行反转,若最后一组不足 k 个,则保持原顺序不变。关键限制条件是:

  • 不能仅修改节点值,必须实际交换节点指针
  • 要求 O(1) 额外空间(进阶要求),意味着不能借助数组、栈等辅助结构存储节点。

这是一道典型的链表分段处理 + 局部反转问题,考察对链表指针的精细控制能力,也是面试中高频出现的“链表变形”类题目。


🔑 核心算法及代码讲解

✅ 核心思想:递归 + 头插法局部反转

我们将整个链表划分为多个长度为 k 的子链表。对每个子链表:

  1. 先判断是否够 k 个节点
  2. 若不够,直接返回头节点(不反转)
  3. 若够,递归处理后续链表
  4. 再将当前 k 个节点反转,并拼接到已处理好的后半部分前面

💡 为什么用递归?
因为“处理完后面再处理前面”天然适合递归结构。递归能自动帮我们保存“下一组”的头节点,无需手动维护多个指针。

💡 头插法反转原理
每次从原链表头部取出一个节点,插入到新链表的头部,从而实现反转。这是链表反转的经典 O(1) 空间方法。

📜 代码详解(含逐行注释)

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        // Step 1: 检查当前是否有至少 k 个节点
        ListNode* cur = head;
        for (int i = 0; i < k; ++i) {
            if (cur == nullptr) {
                return head; // 不足 k 个,不反转,直接返回原头
            }
            cur = cur->next; // 移动 k 步,cur 指向第 k+1 个节点(即下一组的开头)
        }

        // Step 2: 递归处理后面的链表
        // 注意:此时 cur 是下一组的起始节点
        ListNode* dummy = new ListNode(); // 创建哑节点,作为当前组反转后的“新头”的前驱
        dummy->next = reverseKGroup(cur, k); // 递归结果接在 dummy 后面(即已处理好的后半部分)

        // Step 3: 头插法反转当前 k 个节点,并接到已处理好的部分前面
        // 此时 head 仍指向当前组的第一个节点,我们要把 [head, ..., 第k个] 反转
        for (int i = 0; i < k; ++i) {
            ListNode* temp = head->next;   // 保存下一个待处理节点
            head->next = dummy->next;      // 将 head 插入到 dummy 后面(头插)
            dummy->next = head;            // 更新 dummy 的 next 为新的头
            head = temp;                   // 移动 head 到下一个节点
        }

        // 返回反转后的新头(即 dummy->next)
        ListNode* result = dummy->next;
        delete dummy; // 可选:避免内存泄漏(LeetCode 通常不要求,但工程中建议)
        return result;
    }
};

关键点解析

  • cur 用于预检查长度,确保不会对不足 k 的组进行反转;
  • dummy 节点简化了头插法的边界处理(无需特判空链表);
  • 递归调用 reverseKGroup(cur, k) 返回的是下一组处理完后的头节点,我们只需把当前组反转后接上去即可;
  • 整个过程只使用了常数个额外指针,满足 O(1) 空间(忽略递归栈的话)。

⚠️ 注意:严格来说,递归解法的空间复杂度是 O(n/k),因为递归深度为 n/k。若要真正实现 O(1) 空间,需改用迭代写法(见下文扩展)。


🧩 解题思路(分步拆解)

  1. 预检查长度:从当前 head 出发,走 k 步,若中途遇到 nullptr,说明不足 k 个,直接返回 head
  2. 递归处理后继:假设从第 k+1 个节点开始的链表已经按规则处理完毕,得到其新头节点。
  3. 反转当前组:使用头插法将当前 k 个节点反转,并将反转后的链表头连接到“已处理好的后半部分”。
  4. 返回新头:当前组反转后的第一个节点即为整个链表的新头。

🎯 类比理解
就像一串珠子,每 k 颗剪下来翻转一次,再串回去。递归就是“先处理后面的珠子串,再处理前面的”。


📊 算法分析

项目分析
时间复杂度O(n):每个节点被访问常数次(一次用于检查长度,一次用于反转)
空间复杂度O(n/k)(递归栈深度);若用迭代可优化至 O(1)
稳定性稳定:未改变非 k 组节点的相对顺序
适用场景链表分块处理、批量反转、流式数据处理

💼 面试加分点

  • 能指出递归 vs 迭代的空间差异;
  • 能手写迭代版本(更符合“O(1) 空间”要求);
  • 能处理边界情况(k=1, k=n, 空链表等)。

💻 完整可运行代码(含测试)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 链表节点定义(LeetCode 已内置,此处为本地测试补充)
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        // Step 1: 检查当前是否有至少 k 个节点
        ListNode* cur = head;
        for (int i = 0; i < k; ++i) {
            if (cur == nullptr) {
                return head; // 不足 k 个,不反转
            }
            cur = cur->next;
        }

        // Step 2: 递归处理后面的链表
        ListNode* dummy = new ListNode(); // 哑节点,辅助反转
        dummy->next = reverseKGroup(cur, k); // cur 是下一组的开头

        // Step 3: 头插法反转当前 k 个节点,并接到已处理好的部分前面
        for (int i = 0; i < k; ++i) {
            ListNode* temp = head->next;
            head->next = dummy->next;
            dummy->next = head;
            head = temp;
        }

        ListNode* result = dummy->next;
        delete dummy; // 可选:避免内存泄漏(LeetCode 通常不要求)
        return result;
    }
};

// 辅助函数:创建链表
ListNode* createList(vector<int>& vals) {
    if (vals.empty()) return nullptr;
    ListNode* head = new ListNode(vals[0]);
    ListNode* cur = head;
    for (int i = 1; i < vals.size(); ++i) {
        cur->next = new ListNode(vals[i]);
        cur = cur->next;
    }
    return head;
}

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head) {
        cout << head->val;
        if (head->next) cout << " -> ";
        head = head->next;
    }
    cout << "\n";
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;

    // 测试用例 1
    vector<int> v1 = {1,2,3,4,5};
    ListNode* head1 = createList(v1);
    ListNode* res1 = sol.reverseKGroup(head1, 2);
    cout << "Test 1 (k=2): ";
    printList(res1); // 预期输出: 2 -> 1 -> 4 -> 3 -> 5

    // 测试用例 2
    vector<int> v2 = {1,2,3,4,5};
    ListNode* head2 = createList(v2);
    ListNode* res2 = sol.reverseKGroup(head2, 3);
    cout << "Test 2 (k=3): ";
    printList(res2); // 预期输出: 3 -> 2 -> 1 -> 4 -> 5

    // 测试用例 3: k=1(应原样返回)
    vector<int> v3 = {1,2,3};
    ListNode* head3 = createList(v3);
    ListNode* res3 = sol.reverseKGroup(head3, 1);
    cout << "Test 3 (k=1): ";
    printList(res3); // 预期输出: 1 -> 2 -> 3

    // 测试用例 4: k=5(刚好整除)
    vector<int> v4 = {1,2,3,4,5};
    ListNode* head4 = createList(v4);
    ListNode* res4 = sol.reverseKGroup(head4, 5);
    cout << "Test 4 (k=5): ";
    printList(res4); // 预期输出: 5 -> 4 -> 3 -> 2 -> 1

    return 0;
}

✅ 测试结果

Test 1 (k=2): 2 -> 1 -> 4 -> 3 -> 5
Test 2 (k=3): 3 -> 2 -> 1 -> 4 -> 5
Test 3 (k=1): 1 -> 2 -> 3
Test 4 (k=5): 5 -> 4 -> 3 -> 2 -> 1

🚀 扩展思考:如何实现真正的 O(1) 空间?(迭代版)

虽然递归简洁,但面试官可能要求迭代实现以满足严格 O(1) 空间。核心思路:

  1. 使用 dummy 节点统一处理头节点变更;
  2. 维护 pre(上一组的尾)、start(当前组头)、end(当前组尾);
  3. 每次找到 end,若不足 k 则 break;
  4. 反转 [start, end],并重新连接前后。

迭代版代码较长,但空间更优,建议掌握。



🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第32题 —— 26.删除排序数组中的重复项(简单)

🔹 题目:给定一个升序排列的数组,原地删除重复元素,使每个元素只出现一次,返回新长度。

🔹 核心思路双指针(快慢指针) —— 慢指针记录不重复位置,快指针遍历数组。

🔹 考点:双指针、原地修改、数组去重。

🔹 难度:简单,但却是“原地算法”的经典入门题,高频面试题!

💡 提示:不要使用额外数组!必须在 O(1) 空间内完成!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!