力扣解题-61. 旋转链表

0 阅读7分钟

力扣解题-61. 旋转链表

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

示例 1:

image.png

输入:head = [1,2,3,4,5], k = 2

输出:[4,5,1,2,3]

示例 2:

image.png

输入:head = [0,1,2], k = 4

输出:[2,0,1]

提示:

链表中节点的数目在范围 [0, 500] 内

-100 <= Node.val <= 100

0 <= k <= 2 * 10⁹

Related Topics

链表、双指针


第一次解答(超时)

解题思路

核心方法:逐次右移法,每次将链表最后一个节点移动到头部,重复k次完成旋转。该思路逻辑直观但效率极低,当k极大时(如2*10⁹)会触发超时,仅适合理解旋转的基本逻辑。

核心逻辑拆解

逐次右移的核心是“每次只移动一位,重复k次”:

  1. 边界处理:若链表为空(head==null)或k=0(无需旋转),直接返回原链表;
  2. 哑节点初始化:创建dummy哑节点(dummy.next=head),简化头节点操作;
  3. 逐次旋转
    • 循环k次,每次执行一次“最后节点移头部”操作;
    • 找到倒数第二个节点second(通过while(second.next!=null && second.next.next!=null)遍历);
    • 取出最后节点last=second.next,将second.next置为null(断开原尾部);
    • last.next指向原头节点firstdummy.next指向last(更新新头节点);
  4. 返回结果:返回dummy.next
性能问题分析
  • 时间复杂度:O(kn)(每次右移需遍历n个节点,k次总操作数=kn);
  • 空间复杂度:O(1)(仅使用指针变量);
  • 超时原因:k的取值可达2*10⁹,即使n=500,总操作数也会达到10¹²,远超时间限制;
  • 核心问题:未利用“旋转k次等价于旋转k%n次”的数学性质,做了大量无效操作。
    public ListNode rotateRight(ListNode head, int k) {
        if(head==null || k==0){
            return head;
        }
        ListNode dummy= new ListNode(0);
        dummy.next=head;
        //每次移动都是将最后一位指向第一位,然后将最后一位的前一位指向null
        for(int i=0;i<k;i++){
            //头节点
            ListNode first=dummy.next;
            //倒数第二个节点
            ListNode second=dummy;
            //循环找到倒数第二个节点
            while(second.next!=null && second.next.next!=null){
                second=second.next;
            }
            //尾巴节点
            ListNode last=second.next;
            //倒数第二个节点的next置为空
            second.next=null;
            //尾巴节点的next指向头节点
            last.next=first;
            //dummy的next指向尾巴节点
            dummy.next=last;
        }
        return dummy.next;
    }

第二次解答

解题思路

核心方法:闭环定位法(最优迭代版),先将链表连成环,再通过数学计算定位新头/新尾节点,仅需两次线性遍历即可完成旋转,时间复杂度O(n)、空间复杂度O(1),完美解决超时问题。

核心逻辑拆解(通俗版)

旋转链表的核心是“利用数学性质简化操作”:

  1. 边界优化:若链表为空/仅有一个节点/k=0,直接返回原链表(无需旋转);
  2. 第一步:统计长度+找尾节点
    • 定义tail指针从head开始遍历,统计链表长度n(初始为1);
    • 遍历结束后,tail指向原链表尾节点,n为总节点数;
  3. 第二步:简化旋转次数
    • 计算k=k%n(旋转n次等价于不旋转,减少无效操作);
    • k=0,直接返回原链表;
  4. 第三步:定位新尾/新头节点
    • 新尾节点:原链表中“倒数第k+1个节点”(即第n-k个节点),通过遍历n-k-1次找到newTail
    • 新头节点:newTail.next(原链表中“倒数第k个节点”);
  5. 第四步:重构链表
    • 断开newTail.next(新尾节点的next置为null);
    • 将原尾节点tail的next指向原头节点head(连成环后再断开,避免二次遍历找尾);
    • 返回新头节点newHead
具体步骤(以示例1 head=[1,2,3,4,5]、k=2为例)
步骤操作关键变量值说明
1统计长度n=5,tail=5n=5, tail=5原链表尾节点为5
2简化k=2%5=2k=2无需简化
3找newTail:遍历n-k-1=2次newTail=3新尾节点为3
4newHead=newTail.next=4newHead=4新头节点为4
5newTail.next=null3.next=null断开新尾节点
6tail.next=head=15.next=1原尾连原头,形成环后断开
最终链表:4→5→1→2→3,与示例结果一致。
性能说明
  • 时间复杂度:O(n)(仅两次线性遍历:统计长度+找新尾节点);
  • 空间复杂度:O(1)(仅使用指针变量,无额外存储);
  • 核心优势:
    1. 利用取模运算将k从10⁹级简化到n级,彻底解决超时问题;
    2. 仅两次遍历完成所有操作,执行效率最优;
    3. 连成环后再断开的逻辑,避免二次遍历找尾节点。
    public ListNode rotateRight(ListNode head, int k) {
            if (head == null || head.next == null || k == 0) {
                return head;
            }
            //找到链表长度和尾巴节点
            int n=1;
            ListNode tail = head;
            while(tail.next!=null){
                tail=tail.next;
                n++;
            }
            //取模,减少无效旋转
            k=k%n;
            //说明不用旋转
            if(k==0){
                return head;
            }
            //找到新的尾巴节点
            ListNode newTail = head;
            for(int i=1;i<n-k;i++) {
                newTail = newTail.next;
            }
            //新的头节点就是新的尾巴节点的下一个
            ListNode newHead = newTail.next;
            //要将新的尾巴节点的next断开,这样newHead才能成为新的头节点
            //断开后,链表变成两段 前半段是head,后半段是newHead
            newTail.next=null;
            //newHead的最后一个节点就是旧的尾巴节点,连接旧的尾巴和新的头节点
            tail.next=head;
            return newHead;
    }

示例解答

解题思路

解法1:双指针法(思路等价,实现不同)

核心方法:快慢指针定位新头节点,利用快慢指针间距k的特性,一次遍历定位新头/新尾节点,逻辑与闭环定位法一致,但实现方式更贴合“双指针”题型特征。

代码实现
public ListNode rotateRight(ListNode head, int k) {
    if (head == null || head.next == null || k == 0) {
        return head;
    }
    // 第一步:统计长度
    int n = 0;
    ListNode curr = head;
    while (curr != null) {
        n++;
        curr = curr.next;
    }
    k %= n;
    if (k == 0) {
        return head;
    }
    // 第二步:快慢指针,快指针先走k步
    ListNode fast = head;
    ListNode slow = head;
    for (int i = 0; i < k; i++) {
        fast = fast.next;
    }
    // 第三步:同步移动,快指针到尾时,慢指针到新尾前一个节点
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }
    // 第四步:重构链表
    ListNode newHead = slow.next;
    slow.next = null;
    fast.next = head;
    return newHead;
}
优势说明
  • 逻辑等价:与闭环定位法的核心数学逻辑一致(k%n简化次数,定位n-k节点);
  • 实现差异:通过快慢指针替代“先找尾再找新尾”的两次遍历,代码更简洁;
  • 性能一致:时间复杂度O(n),空间复杂度O(1),仅遍历方式不同。
解法2:递归法(思路拓展)

核心方法:递归缩小旋转规模,每次递归将旋转次数k简化为k%n,再定位新头节点,逻辑优雅但递归深度最多为n(无栈溢出风险,n≤500)。

代码实现
public ListNode rotateRight(ListNode head, int k) {
    // 边界条件
    if (head == null || head.next == null || k == 0) {
        return head;
    }
    // 统计长度
    int n = 1;
    ListNode tail = head;
    while (tail.next != null) {
        tail = tail.next;
        n++;
    }
    // 简化k
    k %= n;
    if (k == 0) {
        return head;
    }
    // 递归终止:k=1时,直接移动最后节点到头部
    if (k == 1) {
        ListNode newHead = tail;
        // 找倒数第二个节点
        ListNode prev = head;
        while (prev.next != tail) {
            prev = prev.next;
        }
        prev.next = null;
        tail.next = head;
        return newHead;
    }
    // 递归:先旋转1次,再递归旋转k-1次
    ListNode rotated = rotateRight(head, 1);
    return rotateRight(rotated, k - 1);
}
适用场景说明
  • 时间复杂度:O(n)(递归本质仍是线性遍历,k%n后递归深度≤n);
  • 空间复杂度:O(n)(递归调用栈深度);
  • 优势:代码简洁,符合分治思想,适合理解递归处理链表的逻辑;
  • 局限性:递归栈有额外空间开销,执行效率略低于迭代法,仅作思路拓展。

总结

  1. 逐次右移法(第一次解答):逻辑直观但超时,仅适合理解旋转基本逻辑,无工程价值;
  2. 闭环定位法(第二次解答):O(n)时间+O(1)空间,利用取模简化k,是工程首选的最优迭代解法;
  3. 双指针法(示例解法1):与闭环定位法逻辑等价,代码更简洁,双指针实现更贴合题型特征;
  4. 递归法(示例解法2):思路优雅但有栈开销,仅作拓展参考;
  5. 关键技巧:
    • 核心优化:旋转k次等价于旋转k%n次,是解决k极大问题的关键;
    • 定位逻辑:新头节点是原链表“倒数第k个节点”,新尾节点是“倒数第k+1个节点”;
    • 效率原则:避免逐次旋转,优先通过数学计算直接定位目标节点。