在算法面试中,链表操作是高频考点,而旋转链表(LeetCode 61)作为其中经典题目,不仅考察基础指针操作,更考验对边界条件的处理能力。本文将带你从零剖析三次反转法的核心逻辑,并分享我在实际编码中踩过的坑与修复过程,无论你使用 TypeScriptJavaScriptJavaPython 还是 C++,思路均可直接复用。

一、题目解读与核心要求

题目描述:给你一个链表的头节点 head,旋转链表,将链表每个节点向右移动 k 个位置。

示例:

输入输出解释
向右移动 2 步,最后 2 个节点移到头部
链表长度 3,4%3=1,向右移动 1 步
移动整倍数步,链表不变

核心要求

  • 时间复杂度:O(n)(仅遍历链表常数次)
  • 空间复杂度:O(1)(仅使用常数个指针,不额外开辟空间)

提示:旋转操作本质是将链表后 k 个节点整体移动到头部,但直接移动需要找到倒数第 k 个节点和前驱,代码较复杂。而“三次反转法”提供了更优雅的解法。

二、三次反转法核心思路

右旋转 k 步的本质是「将链表最后 k 个节点整体移到头部」。利用链表反转的特性,通过三次反转实现该逻辑:

  1. 反转整个链表:把最后 k 个节点翻到链表头部(顺序反转);
  2. 反转前 k 个节点:恢复这 k 个节点的原有顺序;
  3. 反转剩余 len-k 个节点:恢复剩余节点的原有顺序。

逻辑验证(以 [1,2,3,4,5], k=2 为例):

  • 原链表:1→2→3→4→5
  • 反转整个链表:5→4→3→2→1(最后 2 个节点 4,5 变前 2 个)
  • 反转前 2 个节点:4→5→3→2→1(恢复 4,5 顺序)
  • 反转剩余 3 个节点:4→5→1→2→3(恢复 1,2,3 顺序)
  • 最终结果:4→5→1→2→3(符合预期)

这种思路在 TypeScriptJavaScript 中实现时,注意引用传递的细节;在 C++ 中则需小心指针解引用。

三、踩坑过程与修复(从错误到正确)

下面分享我在编写代码时遇到的两个典型版本,以及修复要点。

版本 1:初始错误代码(典型问题汇总)

class Solution {
public:
void reverseList(ListNode* head,int left,int right){
    ListNode dummy(0,head);
    ListNode* p0=&dummy;
    for(int i=1;inext;
    }
    ListNode* p1=nullptr;
    ListNode* p2=p0->next;
    // 坑1:反转循环次数不足
    for(int i=1;inext;
        p2->next=p1;
        p1=p2;
        p2=p3;
    }
    p0->next->next=p2;
    p0->next=p1;
    // 坑2:局部变量修改,外部指针无变化
    head=dummy.next;
}
    ListNode* rotateRight(ListNode* head, int k) {
        // 坑3:长度计算错误(初始1,多算1)
        int len=1;
        ListNode* l=head;
        while(l){
            len++;
            l=l->next;
        }
        // 坑4:未处理k>len,无取模
        reverseList(head,1,len);
        reverseList(head,1,k);
        // 坑5:反转区间计算错误(len-k而非k+1)
        reverseList(head,len-k,len);
        return head;
    }
};

核心坑点拆解

坑点具体问题后果
反转函数参数错误 的 是值传递函数内修改 仅改变局部变量,外部链表无变化
反转循环次数错误(少执行 1 次)区间反转不完整,链表顺序混乱
长度计算错误 初始化, 内直接加 1长度多算 1,后续反转区间越界
未处理 无 取模 超出链表长度时,访问空指针()
反转区间错误第三次反转起始为 时区间为负数,程序崩溃
边界判断缺失无 直接返回逻辑旋转整倍数步时,仍执行反转,导致链表错乱

⚠️ 常见误区:许多初学者(包括我最初)在 JavaPython 中容易忽略值传递的特性,导致反转函数无法更新外部头指针。

版本 2:修复核心问题(引用传递 + 边界处理)

class Solution {
public:
    // 修复1:引用传递head,直接修改外部指针
    void reverseList(ListNode* &head,int left,int right){
        if (left == right) return; // 区间长度为1,无需反转
        ListNode dummy(0,head);
        ListNode* p0=&dummy;
        for(int i=1;inext;
        }
        ListNode* p1=nullptr;
        ListNode* p2=p0->next;
        // 修复2:反转循环次数(right-left+1次,确保完整反转)
        for(int i=0;inext;
            p2->next=p1;
            p1=p2;
            p2=p3;
        }
        p0->next->next=p2;
        p0->next=p1;
        head=dummy.next; // 引用传递生效,修改外部head
    }
    ListNode* rotateRight(ListNode* head, int k) {
        // 修复3:边界预处理(空链表/单节点/无需旋转)
        if (!head || !head->next || k == 0) return head;
        // 修复4:正确计算链表长度
        int len=0;
        ListNode* l=head;
        while(l){
            len++;
            l=l->next;
        }
        // 修复5:k取模,避免无效旋转
        k %= len;
        if (k == 0) return head; // 旋转整倍数,直接返回原链表
        // 修复6:修正第三次反转区间(k+1到len)
        reverseList(head,1,len);        // 反转整个链表
        reverseList(head,1,k);          // 反转前k个节点
        reverseList(head,k+1,len);      // 反转剩余节点
        return head;
    }
};

关键修复点说明

  • 引用传递ListNode* &head:函数内对 head 的修改会直接同步到外部,解决「反转操作无效」的核心问题(替代「返回新头节点再赋值」的方式,更简洁)。
  • 正确计算反转次数:区间 [left, right]right-left+1 个节点,需要反转 right-left+1 次(逐个节点反转指针)。
  • k 取模 + 边界判断k %= lenk 缩减到 [0, len-1],避免重复旋转;k==0 直接返回。
  • 修正反转区间:第三次反转起始位置为 k+1(剩余 len-k 个节点),而非 len-k

C++ 中,引用传递通过 & 实现;在 Python 中,虽然列表可变,但链表节点对象需通过 return 更新;TypeScript 则与 JavaScript 类似,注意对象引用。

四、最终最优实现(可直接提交)

class Solution {
public:
    // 反转链表指定区间(引用传递head)
    void reverseList(ListNode* &head, int left, int right) {
        if (left == right) return; // 无需求反转
        ListNode dummy(0, head);
        ListNode* p0 = &dummy;
        // 找到区间左边界的前驱节点
        for (int i = 1; i < left; ++i) {
            p0 = p0->next;
        }
        // 头插法反转区间内节点
        ListNode* pre = nullptr;
        ListNode* cur = p0->next;
        for (int i = 0; i < right - left + 1; ++i) {
            ListNode* nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        // 拼接反转后的区间
        p0->next->next = cur; // 原区间首节点指向剩余节点
        p0->next = pre;       // 前驱节点指向反转后的区间头
        head = dummy.next;    // 更新外部head
    }
    ListNode* rotateRight(ListNode* head, int k) {
        // 边界条件:空链表、单节点、无需旋转
        if (!head || !head->next || k == 0) return head;
        // 计算链表长度
        int len = 0;
        ListNode* curr = head;
        while (curr) {
            len++;
            curr = curr->next;
        }
        // 缩减k的范围(避免无效旋转)
        k %= len;
        if (k == 0) return head;
        // 三次反转实现右旋转
        reverseList(head, 1, len);
        reverseList(head, 1, k);
        reverseList(head, k + 1, len);
        return head;
    }
};

该版本已通过 LeetCode 所有测试用例,时间复杂度 O(n),空间复杂度 O(1)。如果你使用 Java,只需将引用传递改为返回新头节点即可。

[AFFILIATE_SLOT_1]

五、关键知识点总结

链表操作核心原则

  • 修改链表结构需操作「前驱节点」,而非节点本身;
  • 值传递的指针无法修改外部变量,需用 引用传递返回值 更新;
  • 所有访问 ptr->next 的操作,必须先判断 ptr 非空。

三次反转法优势

  • 时间复杂度 O(n):仅遍历链表常数次(计算长度 1 次 + 三次反转各 1 次);
  • 空间复杂度 O(1):仅使用常数个指针,无额外空间开销;
  • 逻辑可复用:反转区间的函数可直接用于其他链表反转问题(如反转指定区间、反转链表前 n 个节点)。

边界场景处理

  • 空链表 / 单节点:直接返回原链表;
  • k=0k%len=0:旋转整倍数步,链表不变;
  • k>len:取模缩减范围,避免无效操作。

[AFFILIATE_SLOT_2]

延伸思考:如果你正在学习 TypeScriptPython,可以将此解法与“快慢指针”法对比,体会不同思路的优劣。掌握链表反转的通用函数,对解决 LeetCode 92(反转链表 II) 等问题也大有裨益。

希望本文能帮你彻底吃透旋转链表这道题,下次面试遇到时,轻松写出无 Bug 的代码!

head = [1,2,3,4,5], k = 2[4,5,1,2,3]head = [0,1,2], k = 4[2,0,1]head = [1,2], k = 2[1,2]reverseListheadheadi<right-leftlen=1while(l)k>lenk%lenknullptr->nextlen-kk>lenk==0