力扣解题-61. 旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例 1:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
示例 2:
输入: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次”:
- 边界处理:若链表为空(
head==null)或k=0(无需旋转),直接返回原链表; - 哑节点初始化:创建
dummy哑节点(dummy.next=head),简化头节点操作; - 逐次旋转:
- 循环k次,每次执行一次“最后节点移头部”操作;
- 找到倒数第二个节点
second(通过while(second.next!=null && second.next.next!=null)遍历); - 取出最后节点
last=second.next,将second.next置为null(断开原尾部); - 将
last.next指向原头节点first,dummy.next指向last(更新新头节点);
- 返回结果:返回
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),完美解决超时问题。
核心逻辑拆解(通俗版)
旋转链表的核心是“利用数学性质简化操作”:
- 边界优化:若链表为空/仅有一个节点/k=0,直接返回原链表(无需旋转);
- 第一步:统计长度+找尾节点:
- 定义
tail指针从head开始遍历,统计链表长度n(初始为1); - 遍历结束后,
tail指向原链表尾节点,n为总节点数;
- 定义
- 第二步:简化旋转次数:
- 计算
k=k%n(旋转n次等价于不旋转,减少无效操作); - 若
k=0,直接返回原链表;
- 计算
- 第三步:定位新尾/新头节点:
- 新尾节点:原链表中“倒数第k+1个节点”(即第
n-k个节点),通过遍历n-k-1次找到newTail; - 新头节点:
newTail.next(原链表中“倒数第k个节点”);
- 新尾节点:原链表中“倒数第k+1个节点”(即第
- 第四步:重构链表:
- 断开
newTail.next(新尾节点的next置为null); - 将原尾节点
tail的next指向原头节点head(连成环后再断开,避免二次遍历找尾); - 返回新头节点
newHead。
- 断开
具体步骤(以示例1 head=[1,2,3,4,5]、k=2为例)
| 步骤 | 操作 | 关键变量值 | 说明 |
|---|---|---|---|
| 1 | 统计长度n=5,tail=5 | n=5, tail=5 | 原链表尾节点为5 |
| 2 | 简化k=2%5=2 | k=2 | 无需简化 |
| 3 | 找newTail:遍历n-k-1=2次 | newTail=3 | 新尾节点为3 |
| 4 | newHead=newTail.next=4 | newHead=4 | 新头节点为4 |
| 5 | newTail.next=null | 3.next=null | 断开新尾节点 |
| 6 | tail.next=head=1 | 5.next=1 | 原尾连原头,形成环后断开 |
| 最终链表:4→5→1→2→3,与示例结果一致。 |
性能说明
- 时间复杂度:O(n)(仅两次线性遍历:统计长度+找新尾节点);
- 空间复杂度:O(1)(仅使用指针变量,无额外存储);
- 核心优势:
- 利用取模运算将k从10⁹级简化到n级,彻底解决超时问题;
- 仅两次遍历完成所有操作,执行效率最优;
- 连成环后再断开的逻辑,避免二次遍历找尾节点。
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)(递归调用栈深度);
- 优势:代码简洁,符合分治思想,适合理解递归处理链表的逻辑;
- 局限性:递归栈有额外空间开销,执行效率略低于迭代法,仅作思路拓展。
总结
- 逐次右移法(第一次解答):逻辑直观但超时,仅适合理解旋转基本逻辑,无工程价值;
- 闭环定位法(第二次解答):O(n)时间+O(1)空间,利用取模简化k,是工程首选的最优迭代解法;
- 双指针法(示例解法1):与闭环定位法逻辑等价,代码更简洁,双指针实现更贴合题型特征;
- 递归法(示例解法2):思路优雅但有栈开销,仅作拓展参考;
- 关键技巧:
- 核心优化:旋转k次等价于旋转k%n次,是解决k极大问题的关键;
- 定位逻辑:新头节点是原链表“倒数第k个节点”,新尾节点是“倒数第k+1个节点”;
- 效率原则:避免逐次旋转,优先通过数学计算直接定位目标节点。