当前位置: 首页 > news >正文

【LeetCode】61. 旋转链表

文章目录

  • 61. 旋转链表
    • 题目描述
    • 示例 1:
    • 示例 2:
    • 提示:
    • 解题思路
      • 问题深度分析
        • 问题本质
        • 核心思想
        • 数学原理详解
        • 算法对比
      • 算法流程图
        • 主算法流程(闭环取模法)
        • 闭环取模详细流程
        • 快慢双指针流程
        • 节点定位计算
      • 复杂度分析
        • 时间复杂度详解
        • 空间复杂度详解
      • 关键优化技巧
        • 技巧1:取模优化(避免无效旋转)
        • 技巧2:闭环取模核心实现
        • 技巧3:快慢双指针实现
        • 技巧4:数组收集法(教学用)
      • 边界情况处理
        • 边界1:空链表
        • 边界2:单节点
        • 边界3:k为0
        • 边界4:k是n的倍数
        • 边界5:k远大于n
      • 数学背景知识
        • 循环移位理论
        • 取模运算的重要性
      • 应用场景
      • 测试用例设计
        • 基础测试
        • 边界测试
        • 特殊测试
      • 常见错误与陷阱
        • 错误1:忘记取模
        • 错误2:步数计算错误
        • 错误3:断开前未保存新头
        • 错误4:未处理边界情况
      • 实战技巧总结
      • 进阶扩展
        • 扩展1:向左旋转
        • 扩展2:双向链表旋转
        • 扩展3:循环链表旋转
    • 代码实现
    • 测试结果
    • 核心收获
    • 应用拓展
    • 完整题解代码

61. 旋转链表

题目描述

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

示例 1:

rotate1

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

示例 2:

rotate2

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

提示:

  • 链表中节点的数目在范围 [0, 500] 内
  • -100 <= Node.val <= 100
  • 0 <= k <= 2 * 10^9

解题思路

问题深度分析

这是一道经典的链表旋转问题,核心挑战在于:如何在O(n)时间和O(1)空间内,将链表向右旋转k个位置?这涉及到闭环取模双指针定位的链表操作技巧。

问题本质

对于一个长度为n的单链表,向右旋转k个位置等价于:

  • 将链表的后k个节点移动到链表头部
  • 如果k > n,由于旋转具有周期性,实际只需旋转 k % n 次
  • 本质是找到新的头节点和尾节点,重新连接链表
核心思想

三种主流解法

  1. 闭环取模法(最优):先成环,定位新尾,断开成链
  2. 快慢双指针法:利用间距k的双指针定位倒数第k个节点
  3. 数组收集法:将节点存入数组,按索引重连(教学/对拍用)
数学原理详解

假设链表为 1->2->3->4->5,k=2:

原链表:1 -> 2 -> 3 -> 4 -> 5 -> null|___________________|n = 5向右旋转2次:
步骤1:找到新头位置 = n - k = 5 - 2 = 3(节点4)
步骤2:找到新尾位置 = n - k - 1 = 2(节点3)
步骤3:断开并重连结果:4 -> 5 -> 1 -> 2 -> 3 -> null

关键公式

  • 有效旋转次数:k' = k % n
  • 新头节点位置:第 n - k' 个节点(从1开始计数)
  • 新尾节点位置:第 n - k' - 1 个节点
  • 原尾节点需要连接到原头节点
算法对比
算法时间复杂度空间复杂度特点
闭环取模O(n)O(1)最优解法,一次遍历+定位+断开
快慢双指针O(n)O(1)思路清晰,适合面试表达
数组收集O(n)O(n)易理解,适合教学和对拍
暴力旋转k次O(k×n)O(1)效率低,仅作对比

注:闭环取模法和快慢双指针法是工程推荐解法

算法流程图

主算法流程(闭环取模法)
开始: 输入head, k
链表为空?
返回null
只有一个节点?
返回head
遍历链表计算长度n
记录尾节点tail
计算: k = k % n
k == 0?
返回head 无需旋转
闭环: tail.next = head
定位新尾: 从head走n-k-1步
记录新头: newHead = newTail.next
断开: newTail.next = nil
返回newHead
闭环取模详细流程
闭环取模核心思想
第一阶段: 成环
遍历找到尾节点tail
tail.next = head
此时形成环形链表
第二阶段: 定位
计算新尾位置 = n-k-1
从head开始走n-k-1步
到达新尾节点newTail
新头 = newTail.next
第三阶段: 断开
newTail.next = nil
断开环, 形成新链表
返回newHead
快慢双指针流程
快慢双指针核心思想
初始化
fast = head, slow = head
fast先走k步
fast到达尾部?
fast和slow同步前进
slow停在新尾位置
新头 = slow.next
断开: slow.next = nil
连接: tail.next = head
返回newHead
节点定位计算
节点定位数学计算
已知信息
链表长度 n
旋转次数 k
取模运算
有效旋转 k' = k % n
避免无效旋转
位置计算
新头索引 = n - k'
新尾索引 = n - k' - 1
示例: n=5, k=2
k' = 2 % 5 = 2
新头索引 = 5 - 2 = 3 第4个节点
新尾索引 = 5 - 2 - 1 = 2 第3个节点

复杂度分析

时间复杂度详解

闭环取模法:O(n)

  • 第一次遍历:计算长度n,O(n)
  • 第二次定位:走n-k步找新尾,O(n)
  • 断开连接:O(1)
  • 总计:O(n) + O(n) + O(1) = O(n)

快慢双指针法:O(n)

  • 第一次遍历:计算长度n(可选),O(n)
  • fast先走k步:O(k),最坏O(n)
  • fast和slow同步:O(n-k)
  • 总计:O(n)

数组收集法:O(n)

  • 遍历收集:O(n)
  • 重新连接:O(n)
  • 总计:O(n)
空间复杂度详解

闭环取模法:O(1)

  • 只需要常数个指针变量:head, tail, newHead, newTail
  • 不使用额外的数据结构

快慢双指针法:O(1)

  • 只需要fast, slow两个指针
  • 同样是常数空间

数组收集法:O(n)

  • 需要存储所有n个节点的指针
  • 空间换时间的思路(虽然时间复杂度相同)

关键优化技巧

技巧1:取模优化(避免无效旋转)
// ❌ 未优化:可能导致多次无效遍历
func rotateRight(head *ListNode, k int) *ListNode {for i := 0; i < k; i++ {// 每次旋转一个位置}
}// ✅ 优化:取模后只旋转必要的次数
func rotateRight(head *ListNode, k int) *ListNode {// 先计算长度n := getLength(head)k = k % n  // 关键优化// 只需旋转k%n次
}

效果:当k=1000000, n=5时,优化后只需旋转0次

技巧2:闭环取模核心实现
// 闭环取模法 - 最优解法
func rotateRightRing(head *ListNode, k int) *ListNode {if head == nil || head.next == nil || k == 0 {return head}// 1. 计算长度并获取尾节点n := 1tail := headfor tail.Next != nil {tail = tail.Nextn++}// 2. 取模优化k %= nif k == 0 {return head  // 无需旋转}// 3. 闭环tail.Next = head// 4. 定位新尾(走n-k-1步)newTail := headfor i := 0; i < n-k-1; i++ {newTail = newTail.Next}// 5. 记录新头并断开newHead := newTail.NextnewTail.Next = nilreturn newHead
}

核心要点

  • 一次遍历获取长度和尾节点
  • 取模避免无效旋转
  • 成环后精确定位新尾
  • 断开时先保存新头再置空
技巧3:快慢双指针实现
// 快慢双指针法
func rotateRightTwoPointers(head *ListNode, k int) *ListNode {if head == nil || head.next == nil || k == 0 {return head}// 1. 计算长度n := 0for cur := head; cur != nil; cur = cur.Next {n++}// 2. 取模k %= nif k == 0 {return head}// 3. 快指针先走k步fast, slow := head, headfor i := 0; i < k; i++ {fast = fast.Next}// 4. 双指针同步前进,直到fast到尾部for fast.Next != nil {fast = fast.Nextslow = slow.Next}// 5. slow此时在新尾位置newHead := slow.Nextslow.Next = nilfast.Next = headreturn newHead
}

优势

  • 思路直观:间距k的双指针定位
  • 适合面试表达
  • 代码清晰
技巧4:数组收集法(教学用)
// 数组收集法 - 用于教学和对拍
func rotateRightArray(head *ListNode, k int) *ListNode {if head == nil || head.next == nil || k == 0 {return head}// 1. 收集所有节点nodes := make([]*ListNode, 0)for cur := head; cur != nil; cur = cur.Next {nodes = append(nodes, cur)}// 2. 计算新头索引n := len(nodes)k %= nif k == 0 {return head}newHeadIdx := n - k// 3. 重新连接// 断开新尾的nextnodes[newHeadIdx-1].Next = nil// 原尾连接原头nodes[n-1].Next = nodes[0]return nodes[newHeadIdx]
}

用途

  • 可视化理解
  • 快速验证正确性
  • 对拍其他实现

边界情况处理

边界1:空链表
输入:head = null, k = 5
输出:null
处理:直接返回null
边界2:单节点
输入:head = [1], k = 3
输出:[1]
处理:单节点旋转后仍是自己,直接返回
边界3:k为0
输入:head = [1,2,3], k = 0
输出:[1,2,3]
处理:无需旋转,直接返回原链表
边界4:k是n的倍数
输入:head = [1,2], k = 4 (n=2, k%n=0)
输出:[1,2]
处理:旋转n次等于没旋转,取模后k=0
边界5:k远大于n
输入:head = [1,2,3], k = 2000000000
输出:需要取模后计算
处理:k % n 避免超时和溢出

数学背景知识

循环移位理论

链表旋转本质是循环移位操作:

  • 向右旋转k位 = 向左旋转(n-k)位
  • 旋转n位 = 不旋转(周期性)
  • 旋转操作可以组合:旋转a次再旋转b次 = 旋转(a+b)%n次
取模运算的重要性
k % n 的意义:
- 当k < n时:k % n = k(保持不变)
- 当k = n时:k % n = 0(旋转一圈回到原位)
- 当k > n时:k % n 去除完整的循环

示例:

n=5的链表,旋转k=12次:
12 % 5 = 2
等价于只旋转2次
节省了10次无效操作

应用场景

  1. 循环队列:实现循环缓冲区的旋转
  2. 数据流处理:滑动窗口的循环移位
  3. 密码学:置换加密中的循环移位
  4. 游戏开发:环形地图的视角旋转
  5. 音频处理:循环采样的位置调整

测试用例设计

基础测试
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
说明:标准旋转场景输入:head = [0,1,2], k = 4
输出:[2,0,1]
说明:k > n的情况,4%3=1
边界测试
输入:head = null, k = 5
输出:null
说明:空链表输入:head = [1], k = 99
输出:[1]
说明:单节点输入:head = [1,2], k = 2
输出:[1,2]
说明:k%n=0
特殊测试
输入:head = [1,2,3,4,5], k = 0
输出:[1,2,3,4,5]
说明:k=0不旋转输入:head = [1,2,3,4,5], k = 5
输出:[1,2,3,4,5]
说明:旋转一圈输入:head = [1,2,3,4,5], k = 3
输出:[3,4,5,1,2]
说明:旋转超过一半长度

常见错误与陷阱

错误1:忘记取模
// ❌ 错误:k很大时会超时
func rotateRight(head *ListNode, k int) *ListNode {for i := 0; i < k; i++ {// 每次旋转一位}
}// ✅ 正确:先取模
k %= n
错误2:步数计算错误
// ❌ 错误:新尾位置错误
newTail := head
for i := 0; i < n-k; i++ {  // 应该是n-k-1newTail = newTail.Next
}// ✅ 正确:
for i := 0; i < n-k-1; i++ {newTail = newTail.Next
}
错误3:断开前未保存新头
// ❌ 错误:先断开会丢失新头引用
newTail.Next = nil
newHead := newTail.Next  // 此时为nil// ✅ 正确:先保存再断开
newHead := newTail.Next
newTail.Next = nil
错误4:未处理边界情况
// ❌ 错误:未检查空链表
func rotateRight(head *ListNode, k int) *ListNode {n := 0cur := headfor cur != nil {  // head为nil时直接出错n++cur = cur.Next}
}// ✅ 正确:先检查
if head == nil || head.Next == nil {return head
}

实战技巧总结

  1. 取模优化:始终先计算k%n,避免无效操作
  2. 一次遍历:在计算长度的同时记录尾节点
  3. 指针保存:操作前先保存关键节点引用
  4. 边界优先:先处理特殊情况,再处理一般情况
  5. 思路清晰:选择最直观的方法(闭环或双指针)
  6. 代码复用:将辅助函数(如计算长度)独立出来

进阶扩展

扩展1:向左旋转
// 向左旋转k位 = 向右旋转n-k位
func rotateLeft(head *ListNode, k int) *ListNode {n := getLength(head)k %= nreturn rotateRight(head, n-k)
}
扩展2:双向链表旋转
// 双向链表可以双向遍历,优化定位
type DListNode struct {Val  intNext *DListNodePrev *DListNode
}func rotateDList(head *DListNode, k int) *DListNode {// 可以选择从头或从尾开始if k < n/2 {// 从头向后走} else {// 从尾向前走(利用Prev指针)}
}
扩展3:循环链表旋转
// 如果链表本身就是环形
func rotateCircular(head *ListNode, k int) *ListNode {// 不需要成环和断开// 直接定位新头即可
}

代码实现

本题提供了三种不同的解法,重点掌握闭环取模法。

测试结果

测试用例闭环取模快慢双指针数组重连
基础测试
边界测试
大k值(10^9)
性能测试最优最优较慢

注:数组法空间复杂度O(n),仅用于教学

核心收获

  1. 取模思想:利用旋转的周期性优化性能
  2. 闭环技巧:链表成环可以简化旋转操作
  3. 双指针定位:间距k的双指针精确定位目标节点
  4. 空间优化:O(1)空间解决链表重排问题

应用拓展

  • 循环队列和循环缓冲区
  • 链表的循环移位和重排
  • 数据流的滑动窗口处理
  • 加密算法中的置换操作

完整题解代码

package mainimport ("fmt"
)type ListNode struct {Val  intNext *ListNode
}// ===== 方法一:闭环取模 + 断开(最优常规解) =====
// 思路:
// 1) 边界:空/单节点/ k==0 -> 原链表
// 2) 计算长度 len,并找到尾节点 tail,使 tail.Next=head,形成环
// 3) k %= len,若 k==0 直接断环返回
// 4) 新尾在第 len-k 个节点,新头为新尾的 Next,将新尾.Next 置空
// 时间 O(n),空间 O(1)
func rotateRightRing(head *ListNode, k int) *ListNode {if head == nil || head.Next == nil || k == 0 {return head}// 计算长度并拿到尾n := 1tail := headfor tail.Next != nil {tail = tail.Nextn++}k %= nif k == 0 {return head}// 闭环tail.Next = head// 新尾: 走 n-k-1 步到达;新头: 新尾.Nextsteps := n - k - 1newTail := headfor i := 0; i < steps; i++ {newTail = newTail.Next}newHead := newTail.NextnewTail.Next = nilreturn newHead
}// ===== 方法二:快慢指针(双指针等效解) =====
// 让 fast 先走 k%len 步,然后 slow 与 fast 同步前进直到 fast 到尾;
// slow 停在“新尾”,slow.Next 为“新头”。最后把尾部与头断开并连接尾到原头。
// 时间 O(n),空间 O(1)
func rotateRightTwoPointers(head *ListNode, k int) *ListNode {if head == nil || head.Next == nil || k == 0 {return head}// 计算长度n := 0for cur := head; cur != nil; cur = cur.Next {n++}k %= nif k == 0 {return head}fast, slow := head, headfor i := 0; i < k; i++ {fast = fast.Next}// 同步走到尾for fast.Next != nil {fast = fast.Nextslow = slow.Next}// slow 是新尾,slow.Next 新头newHead := slow.Nextslow.Next = nilfast.Next = headreturn newHead
}// ===== 方法三:数组收集节点指针(易理解) =====
// 将节点指针装入切片,按索引重连;空间 O(n),适合讲解与对拍
func rotateRightArray(head *ListNode, k int) *ListNode {if head == nil || head.Next == nil || k == 0 {return head}nodes := make([]*ListNode, 0, 64)for cur := head; cur != nil; cur = cur.Next {nodes = append(nodes, cur)}n := len(nodes)k %= nif k == 0 {return head}// 新头索引newHeadIdx := (n - k) % nnewHead := nodes[newHeadIdx]// 断开与重连nodes[(newHeadIdx-1+n)%n].Next = nilnodes[n-1].Next = nodes[0]return newHead
}// ===== 构建/打印/辅助 =====
func buildList(vals []int) *ListNode {if len(vals) == 0 {return nil}head := &ListNode{Val: vals[0]}cur := headfor i := 1; i < len(vals); i++ {cur.Next = &ListNode{Val: vals[i]}cur = cur.Next}return head
}func listToSlice(head *ListNode) []int {var out []intfor head != nil {out = append(out, head.Val)head = head.Next}return out
}func main() {cases := []struct {in   []intk    intwant []int}{{[]int{1, 2, 3, 4, 5}, 2, []int{4, 5, 1, 2, 3}},{[]int{0, 1, 2}, 4, []int{2, 0, 1}},{[]int{}, 5, []int{}},{[]int{1}, 0, []int{1}},{[]int{1}, 3, []int{1}},{[]int{1, 2}, 2, []int{1, 2}},{[]int{1, 2}, 3, []int{2, 1}},}methods := []struct {name stringfn   func(*ListNode, int) *ListNode}{{"闭环取模", rotateRightRing},{"快慢双指针", rotateRightTwoPointers},{"数组重连", rotateRightArray},}fmt.Println("61. 旋转链表 - 多解法对比")for _, c := range cases {fmt.Printf("in=%v k=%d\n", c.in, c.k)for _, m := range methods {got := listToSlice(m.fn(buildList(c.in), c.k))status := "✅"if fmt.Sprint(got) != fmt.Sprint(c.want) {status = "❌"}fmt.Printf("  %-8s => %v %s\n", m.name, got, status)}fmt.Printf("  期望 => %v\n", c.want)fmt.Println("------------------------------")}
}
http://www.dtcms.com/a/466888.html

相关文章:

  • 整站seo优化哪家好电商设计网站培训
  • 【GD32】启动过程-程序计数器(PC)
  • 茶艺实训室:为学习者打造专业茶艺实操平台
  • 机械设计网站推荐贵州建设监理网站培训通知栏
  • 常州网站制作策划手机制作网站主页软件
  • 淘宝天猫优惠券网站怎么做工作啦
  • H3C 实现ACL 访问控制
  • 【北京迅为】iTOP-4412精英版使用手册-第三十七章 Hello_Driver_Module
  • 1 建设好自媒体门户网站网站备案要几天
  • GESP C++等级认证三级13-操作string2-2
  • 富连网网站开发数字营销成功案例
  • 我的网站 dedecms网站开发模式分为
  • 【附代码】Jupyter 多进程调用 seaborn 并保留格式
  • 正规手机网站建设平台之梦一个系统做多个网站
  • 服务器数据恢复—Raid5多盘掉线,存储如何“起死回生”?
  • 郑州网站推广价vue.js合作做网站么
  • [嵌入式系统-85]:GPU内部结构
  • 珠海网站建设哪个平台好wordpress的html
  • 网站开发佛山南京微信网站建设
  • 沈阳市住房和城乡建设局网站首页wordpress会员互动
  • 建站行业现状探讨有哪些网站可以自己做加视频
  • RPA是什么?企业如何借助有赞平台实现订单与会员自动化
  • cpp03:小项目Da
  • wordpress 商品站网站建设 猴王网络
  • 整站seo优化一般多少钱仿it资讯类网站源码
  • 如何建设一个静态网站宝塔怎么做网站的301跳转
  • 做静态网站有什么建议佛山家居网站全网营销
  • 【武大图书馆事件全过程】免费分享
  • SVN 抓取状态
  • Shell 脚本编程全解析:从入门到企业级实战