news 2026/10/2 2:55:59

轮转数组四种Python解法:从切片到O(1)原地反转的完整拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
轮转数组四种Python解法:从切片到O(1)原地反转的完整拆解

如果你刷力扣 Hot100 刷到第 15 题“轮转数组”,很容易被它朴素的名字骗过去。我第一次做这题,随手写一行 Python 切片就 AC 了,心里还在嘀咕,这也配进 Hot100?直到后来在面试里被追问“你能做到 O(1) 空间吗”,我才发现自己只是背下了切片,根本没有理解这道题背后的三件事:边界条件、原地修改、同一需求下四种写法的取舍。

这篇文章我会用 Python 把轮转数组从暴力到最优完整拆一遍。先讲清楚题目最容易被忽略的边界,再重点拆面试官最爱的三次反转法,然后分析 Python 特有的切片和 deque 写法,哪些能用于面试、哪些只能用于工程,最后补上环状替换法和一套可以直接自测的用例。适合刚开始刷 Hot100 的新人,也适合已经把题 AC 过、但想彻底搞懂“为什么这么做”的人。

1. 第15题“轮转数组”到底在考什么:两道隐藏的边角题

1.1 先读懂题:右移 k 位和右移 k%n 位是同一件事

力扣的原题编号是 189,在 Hot100 专题里排在第十五位。题目描述很简短:给你一个数组 nums 和一个整数 k,把数组里所有元素向右轮转 k 个位置。给两个例子就好懂多了:nums = [1,2,3,4,5,6,7], k = 3得到[5,6,7,1,2,3,4];nums = [-1,-100,3,99], k = 2得到[3,99,-1,-100]。

很多人做题时会默认 k 小于数组长度,这是第一个坑。比如nums = [1,2],k = 3,把数组右移 3 位,实际效果是右移 1 位,因为移动 2 位之后数组又回到原状,第 3 位其实是重复了一次完整的轮转。所以任何解法第一步都应该是k %= n,这里的 n 是数组长度。为什么取模?因为右移 n 次等于没移,这是一个周期为 n 的操作,取模只是把多余的周期拆掉。同理,如果 k 可能为负数,那就表示左移,需要先把负数矫正成正数,再做右移逻辑。

除了“k 大于 n”,还有两个边界也要重视:n = 1和k = 0。nums = [1],k = 1000000,取模后k = 0,原数组不变;nums = [1,2,3],k = 0也应当原样返回。这些边界不处理,在某些 Python 解法里会表现得很诡异,这就是下一节要说的。

1.2 Python 的负索引会把 k=0 变成一个隐蔽的坑

Python 负索引在处理 k=0 时有一个很少有人注意到的细节:-0等于0。切片法里常见写法是nums[:] = nums[-k:] + nums[:-k],当k = 0时,它实际计算的是nums[0:] + nums[:0],也就是整个数组拼接一个空数组,结果依然正确,不会报错。但它能碰巧工作,完全依赖-0 == 0这个 Python 语言特性,如果你不理解这一点,面试中被问到时很容易卡壳。

所以更稳妥的写法是:先k %= n,紧接着if k == 0: return,再去走后续的逻辑。这样从源头上避开负索引的语义混乱,也让代码的边界行为一目了然。

这里还有一个 Python 专属的坑:nums = nums[-k:] + nums[:-k]和nums[:] = nums[-k:] + nums[:-k],在 LeetCode 上结果完全不同。前者只是把函数内部的局部变量 nums 重新绑定到一个新列表,外部调用者拿到的还是原列表;后者通过切片赋值的语法,把新内容灌进了原列表对象,外部才会看到变化。这个坑无数人踩过,题解区经常有人问“为什么我返回了 nums 还是 WA”,多半就是写成了nums = ...而不是nums[:] = ...。

1.3 为什么它配得上一个 Hot100 席位

把 Hot100 数组区附近的题拿出来对比,你会有感觉。排在它前面的是“合并区间”,排在它后面的是“除自身以外数组的乘积”。这三道题有个共同气质:都不需要冷门算法,考的其实是“你能不能把基础操作写干净”。轮转数组更是如此——暴力切片、额外数组、三次反转、环状替换,四种做法从 O(n) 空间一路优化到 O(1) 空间,同一个需求覆盖了时间复杂度、空间复杂度、原地修改、Python 引用传递这些高频面试点。

作为热身题,它简单到能让人快速进入刷题状态;作为考题,它又能立刻区分“背答案的人”和“懂原理的人”。这也是它看起来简单,却常年待在 Hot100 里的原因。

2. 三次反转法:O(1) 空间解法是怎么被“自然想到”的

2.1 从 AB 变成 BA:反转数组为什么是一把万能钥匙

三次反转法不是凭空冒出来的。把数组看成两段区间:前n - k个元素是 A,后k个元素是 B,原数组就是 AB,我们要的结果是 BA。一个数组整体倒序之后会得到reverse(B) + reverse(A),也就是 B 和 A 都变成了倒序。如果再分别把前 k 个区间和后 n-k 个区间各自倒序一次,reverse(B)会变回 B,reverse(A)会变回 A,整体恰好就是 BA。

用例子走一遍:nums = [1,2,3,4,5,6,7],n = 7,k = 3,A 是[1,2,3,4],B 是[5,6,7]。整体倒序得到[7,6,5,4,3,2,1];前三个元素是 B 的倒序[7,6,5],倒序之后变成[5,6,7];后四个元素是 A 的倒序[4,3,2,1],倒序之后变成[1,2,3,4]。整体拼起来就是[5,6,7,1,2,3,4],正好是答案。

为什么这个思路可以被现场推导出来?因为轮转的本质是两段区间的交换,而“整体反转加局部反转”是处理区间交换的经典手段。理解了这一层,你就不需要死记三次反转的步骤,而是可以从“AB 变 BA”这个目标出发,自己推出全部流程。

2.2 手写一个不依赖切片的区间反转

三次反转的核心操作是“倒序数组的某一段”,所以需要一个能反转任意区间的函数。在 Python 里,反转一段区间主要有三种写法:

  • nums[i:j] = nums[i:j][::-1]:简洁,但切片会产生临时新列表,严格说是 O(n) 空间;
  • nums[i:j] = reversed(nums[i:j]):同样会创建临时列表;
  • 双指针原地交换:完全满足 O(1) 空间。

面试和比赛里,我建议手写双指针版本:

def rotate(nums, k): n = len(nums) k %= n if k == 0: return def reverse(i, j): while i < j: nums[i], nums[j] = nums[j], nums[i] i += 1 j -= 1 reverse(0, n - 1) # 整体反转 reverse(0, k - 1) # 反转前 k 个 reverse(k, n - 1) # 反转后 n-k 个

这段代码有三个小细节值得注意。第一,reverse 的循环条件是i < j,不需要额外临时数组,交换完往中间收拢即可。第二,三次反转的区间边界是k-1和k,写错一个元素,整个结果就偏了。第三,Python 的多重赋值nums[i], nums[j] = nums[j], nums[i]是原子交换,这也是 Python 写算法题比较舒服的地方。

2.3 取模之后马上加一个 if k == 0 的 return

我在代码里先做k %= n,紧接着判断if k == 0: return。这个防御性写法做题时能省不少事。原因很简单:k=0 时三次反转会把数组从头到尾反转一遍,再局部反转两遍,结果虽然还是原数组,但白白跑了两个 O(n) 的循环。更重要的是,如果后面的代码逻辑依赖“k 一定大于 0”这个前提,提前 return 能避免很多分支判断上的麻烦。

这个习惯和写业务代码时“入参先校验再处理”是一个道理。刷题不只是为了 AC,也是在培养工程习惯。

3. Python 解法里的几道“捷径”:哪些能用于面试,哪些只能用于工程

3.1 切片轮转:一行 AC 背后的两个致命细节

切片解法是很多人第一次 AC 的写法:

def rotate(nums, k): n = len(nums) k %= n nums[:] = nums[-k:] + nums[:-k]

它确实短,但有两个致命细节,面试中必须能讲清楚。

第一个细节是为什么必须是nums[:] = ...,而不是nums = ...。函数里的nums = new_list只是让局部变量指向了一个新列表,调用者的列表完全没变;nums[:] = ...才是往原列表对象里灌新内容。LeetCode 的判题系统检查的是外部引用指向的那个列表,所以一定要用切片赋值。这也是 Python 参数传递里最经典的送命题之一。

第二个细节是空间复杂度。切片nums[-k:] + nums[:-k]会产生两个新列表切片,然后把它们拼接成第三个临时列表,最后才赋回去。虽然最终列表的引用没变,但过程里用了 O(n) 的额外空间。力扣上这样写能过,空间复杂度显示也是 O(n),可面试官一句“能不能优化空间”就能把人问住。切片法的定位应该是:本地快速验证、比赛抢时间、工程环境的日常编码。

3.2 deque.rotate:工程里最优雅、面试里最减分

Python 的 collections.deque 自带 rotate 方法,代码更短:

from collections import deque def rotate(nums, k): dq = deque(nums) dq.rotate(k) # 正数是右转,负数是左转 nums[:] = list(dq)

工程里这确实好用,尤其是做时间序列滚动窗口、队列轮转这类场景。deque 内部用分块链表存储,rotate 整体时间是 O(n),但常数很小,而且写起来不存在边界问题。但面试时我不建议主动掏 deque。原因有两个:第一,deque(nums)会复制整个数组,list(dq)又复制一次,空间 O(n) 躲不掉;第二,面试官想看的是你处理数组的能力,一个库函数把题目最重要的操作抽走了,题目等于没考。

它更适合作为“日常业务开发”的答案,而不是“面试算法题”的答案。如果你在面试里用了 deque,最好主动补一句“我知道它不是 O(1) 空间,只是工程上写着舒服”,这样至少不会让面试官觉得你只会调库。

3.3 判断“原地修改”的三个自测标准

我后来总结了一个判断标准,刷题时可以拿来对照自己的解法:

  1. 函数签名没有把 nums 重新绑定为一个新对象;
  2. 没有使用切片、list()、deque()、reversed() 这类会创建新容器的操作;
  3. 所有改动都通过索引赋值完成。

如果满足这三点,基本可以确认自己的解法是 O(1) 空间的原地算法。在轮转数组这道题里,满足这三条的只有三次反转法和环状替换法。这也是我建议你至少在本地手写一次双指针 reverse 的原因——只要 reverse 能写出来,三次反转法就永远处于“随时能调出来”的状态,面试被追问也不会慌。

4. 环状替换法:把坑踩完之后才真正理解的“O(n)”

4.1 模拟一次跳跃:从 index=0 出发会发生什么

环状替换法也叫跳跃替换。思路是:从某个位置出发,把当前位置的值放到它应该去的目标位置(当前下标 + k) % n,同时把目标位置原来的值接过来,再继续往下跳,直到回到出发点,这样就完成了一轮移动。

以nums = [1,2,3,4,5,6,7],k = 3为例,从下标 0 出发:

  • 下标 0 拿着 1,目标下标 3,把 1 放到下标 3,手里接住 4;
  • 下标 3 拿着 4,目标下标 6,把 4 放到下标 6,手里接住 7;
  • 下标 6 拿着 7,目标下标 2,把 7 放到下标 2,手里接住 3;
  • 下标 2 拿着 3,目标下标 5,把 3 放到下标 5,手里接住 6;
  • 下标 5 拿着 6,目标下标 1,把 6 放到下标 1,手里接住 2;
  • 下标 1 拿着 2,目标下标 4,把 2 放到下标 4,手里接住 5;
  • 下标 4 拿着 5,目标下标 0,把 5 放到下标 0,回到起点,正好移动了 7 个元素。

因为 n=7 和 k=3 互质,这一轮跳跃就覆盖了所有下标,一次循环完成整个轮转。这就是环状替换“每个元素只移动一次”的含义,总时间 O(n),而不是很多人误以为的 O(n*k)。

4.2 为什么有时需要多个起点:gcd 是核心

换一个例子:nums = [1,2,3,4,5,6],k = 2。从下标 0 出发:0 放 1 到下标 2,接 3;下标 2 放 3 到下标 4,接 5;下标 4 放 5 到下标 0,接 1,回到起点。这一轮只移动了 0、2、4 三个下标,1、3、5 完全没动。所以还需要从下标 1 再来一轮:1 放到 3,3 放到 5,5 回到 1,才把整个数组轮转完。

这里“需要几个起点”,答案就是gcd(n, k)。原因是:步长为 k 的跳跃,在模 n 的意义下,会把所有下标分到若干个循环轨道里,轨道的数量正好是 n 和 k 的最大公约数。gcd(6,2) = 2,所以需要两轮;gcd(7,3) = 1,所以一轮就够。写代码时直接math.gcd(n, k)算出起点数量,从 0 到 gcd-1 每个起点各做一轮即可。

4.3 代码实现与两个极易写错的细节

用 gcd 控制的写法非常清晰:

from math import gcd def rotate(nums, k): n = len(nums) k %= n if k == 0: return g = gcd(n, k) for start in range(g): cur = start prev = nums[cur] while True: nxt = (cur + k) % n prev, nums[nxt] = nums[nxt], prev cur = nxt if cur == start: break

这个写法我踩过三个坑,列出来给大家省时间。

第一,内层循环的出口是cur == start,不是count == n。如果用 count 控制循环,写法会别扭很多,而且很容易在 gcd>1 时提前 break,导致后面一堆元素没处理。

第二,每轮开始前一定要prev = nums[start]。有人会忘记重新取出发点的值,导致一轮循环里第一个目标位写入了上一轮残留的旧值。

第三,gcd(n, k)里的 k 最好用取模后的 k。虽然gcd(k,n)和gcd(k%n,n)结果一样,但取模后的 k 会让跳跃过程更符合直觉,排查时也更省力。

环状替换的理解成本比三次反转高,但画一遍上面的例子,其实比想象中简单。面试里它可以作为三次反转后的备选方案,讲出来比较加分,因为它体现了对“每个元素移动次数”的深刻理解。

5. 一次“从 AC 到 Offer”的完整复盘:复杂度对比与面试追问拆解

5.1 我在本地用五组测试验证这四种写法

光看代码不跑测试,很难确认边界处理没问题。我刷这道题时给自己准备了五组用例,全部用 assert 跑:

  • [1,2,3,4,5,6,7], k=3期望得到[5,6,7,1,2,3,4]
  • [-1,-100,3,99], k=2期望得到[3,99,-1,-100]
  • [1,2], k=3期望得到[2,1]
  • [1], k=0期望得到[1]
  • list(range(10000)), k=1000000007,先取模再和切片结果对照

验证代码可以写成这样:

def verify(rotate): nums = [1, 2, 3, 4, 5, 6, 7] rotate(nums, 3) assert nums == [5, 6, 7, 1, 2, 3, 4] nums = [-1, -100, 3, 99] rotate(nums, 2) assert nums == [3, 99, -1, -100] nums = [1, 2] rotate(nums, 3) assert nums == [2, 1] nums = [1] rotate(nums, 0) assert nums == [1]

建议不要用 print 肉眼看输出,全部用 assert,刷题时代替手写测试效率高很多。我第一次实现环状替换时就是靠这些用例发现 gcd 那个坑的。

5.2 一张表看清四种方案的取舍

把四种方案放到一张表里,面试时脑内对比非常直观:

方案时间复杂度额外空间是否原地面试推荐度
三次反转O(n)O(1)是强烈推荐
环状替换O(n)O(1)是推荐,加分项
切片拼接O(n)O(n)否(有临时列表)不推荐主动讲
deque.rotateO(n)O(n)否工程可用,面试慎用

如果面试只有 10 分钟写这道题,直接选三次反转,因为它最容易证明正确性,也不引入额外空间。如果面试官问“还有没有别的原地方法”,再把环状替换拿出来。如果只是闲聊工程能力,可以说“生产环境我可能用 deque.rotate 或切片,因为可读性更好”。关键是要能流畅说出每种方案的适用场景,而不是只会一种。

5.3 面试官顺着这道题会追问的四层问题

第一层,k 比数组长度大怎么办?回答:k %= n,因为右移 n 次等于没移。

第二层,如果 k 是负数,表示左移怎么办?回答:先把 k 矫正成正数,比如k = (k % n + n) % n,再走右移逻辑;如果坚持用切片,左移 k 位就是nums[:] = nums[k:] + nums[:k]。重点是能说清楚正负方向和取模的关系。

第三层,能不能原地且 O(1) 空间?回答:三次反转或环状替换,把流程讲清楚。这一步最能拉好感。

第四层,为什么 LeetCode 里nums = nums[-k:] + nums[:-k]不生效?回答:函数内的局部变量重新绑定不影响外部列表,要用nums[:] = ...原地修改。这个问题看似 Python 细节,实际考察的是对语言内存模型和参数传递的理解。

还有一个变体也容易被问到:如果题目允许返回新数组呢?那直接切片就是最优答案,O(n) 时间和 O(n) 空间都合理,没必要三次反转。能在不同约束下选择不同方案,这才叫真正掌握。

6. 轮转数组的变体地图:从这道题串起数组类面试题

6.1 它在 Hot100 里的位置与“数组题家族”

Hot100 的数组部分很有规律。合并区间考“排序后扫一遍”,除自身以外数组的乘积考“前缀信息”,轮转数组考“原地重排”。它们放在一起看,你会发现数组类题目的核心其实就是三件事:怎么遍历、怎么交换、怎么用额外空间换时间。轮转数组恰好把“交换”和“额外空间”两个维度都覆盖到了。这也是它出现在榜单中段的理由——不是让你背答案,是让你借它建立原地修改的直觉。

刷题的时候不建议孤立刷。每做完一道数组题,可以顺手看看它在榜单里的邻居,想一想“这道题如果加一个原地限制,解法会不会变”。轮转数组就是“原地限制改变一切”的典型例子。

6.2 左旋转字符串、旋转链表、旋转图像的关联

轮转数组不是一个孤立知识点,它和几道常见题有直接血缘关系。

剑指 Offer 58-II 左旋转字符串:字符串左移,用三次反转同样能解,区别是字符串不可变,要先转成列表或直接切片。

LeetCode 61 旋转链表:把链表连成环,再在合适位置断开。核心也是“计算真正移动的步数”和“找到断点”。链表版比数组版多了找断点的操作,但取模的思路一模一样。

LeetCode 48 旋转图像:矩阵顺时针旋转 90 度,经典做法是转置之后上下翻转。它和环状替换一样靠“交换”操作,只是从一维坐标换到了二维坐标。

如果你刷完这道题,能顺手把这三道过一遍,会发现自己对“旋转”这一类问题的理解会一下子立体很多。

6.3 轮转思想在业务代码里的三种形态

最后说点题外话,轮转不只是面试题。我写业务时至少见过三种轮转思想的应用。第一种是时间序列滚动窗口,比如实时指标只保留最近一小时的数据,用deque(maxlen=60)直接控制窗口长度;第二种是环形缓冲区,嵌入式或者消息队列里很常见,核心就是(head + offset) % capacity这种取模;第三种是数据重排,比如按权重轮询一批待执行任务、把最近常用的项挪到列表头部,本质上都是围绕一个有序序列做位移。

所以别觉得排序、轮转这类题离工程很远,它们只是换了一层业务外衣,底层逻辑完全一样。

最后给一个我自己的刷题习惯。轮转数组这道题,我两年里在不同场景下遇到三四次,每次都是同一个套路:先口头答出三种做法的复杂度,再在白板上手写三次反转。能做到这么顺,不是记性好,而是第一次刷的时候把四种写法都用 assert 跑了一遍,并把k %= n这一行牢牢记住了。你现在刷到这道题,不妨也花 20 分钟把三次反转和环状替换各手写一遍,再亲自验证一次nums[:]和nums =的区别。这些功夫不会白费,下一次再见到轮转数组,你大概率不用思考就能写对。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:55:15

基于PyTorch实现RankIQA图像质量评估模型:排序学习与回归微调

简介&#xff1a;面向计算机视觉课程设计与期末大作业&#xff0c;提供了一套基于PyTorch实现的RankIQA图像质量评估模型完整源码。项目采用排序学习机制训练无参考图像质量评估模型&#xff0c;包含从数据准备、模型构建、损失函数设计到训练评估的完整流程&#xff0c;特别适…

作者头像 李华
网站建设 2026/10/2 2:54:35

SSA-XGBoost小样本回归优化:原理、Matlab实现与工业落地

简介&#xff1a;本资源是一套基于麻雀算法&#xff08;SSA&#xff09;优化XGBoost模型的完整数据回归预测解决方案&#xff0c;面向机器学习初学者、智能优化算法研究者及Matlab工程实践者&#xff0c;解决传统XGBoost超参数人工调优效率低、易过拟合的问题。压缩包共10个文件…

作者头像 李华
网站建设 2026/10/2 2:54:10

从零搭建Claude多Agent协作流水线并实现终端可视化监控

最近接了个偏工程向的任务&#xff1a;要把一堆原本靠单个 Claude Code 实例零散执行的活儿&#xff0c;改造成一条多 Agent 协作流水线&#xff0c;同时在终端里加一个能实时看到每个子任务进度、Token 消耗和运行状态的监控面板。前后折腾了小两周&#xff0c;安装环节翻车、…

作者头像 李华
网站建设 2026/10/2 2:53:29

2026跨境电商云成本突围:国际云代理商与架构优化全解析

做跨境电商这行&#xff0c;最容易忽视的往往不是流量投放&#xff0c;也不是选品供应链&#xff0c;而是藏在后台那串越来越难看的云账单。我看过太多团队&#xff0c;年初信誓旦旦要利润翻倍&#xff0c;年底一算账&#xff0c;光云资源就吃掉了毛利的十几个点。到了 2026 年…

作者头像 李华
网站建设 2026/10/2 2:53:20

PyCharm连接WSL2 Conda解释器:高频报错根因与排查完整指南

如果你也跟我一样&#xff0c;在 Windows 上装好了 pycharm2024&#xff0c;又听人说"开发环境放 WSL2 里才干净"&#xff0c;于是兴冲冲跑去给 conda 配环境&#xff0c;结果折腾一晚上连解释器都添加不进去——那这篇文章就是给你准备的。我上个月把项目从纯 Windo…

作者头像 李华
网站建设 2026/10/2 2:52:28

戴尔交换机与Juniper对接:LACP链路聚合配置实战与踩坑总结

干网络这行&#xff0c;迟早会遇到一对组合&#xff1a;一边是戴尔交换机&#xff0c;一边是Juniper交换机&#xff0c;中间要跑业务流量&#xff0c;还得保证带宽和冗余。大多数人第一反应是“拉两根网线&#xff0c;起个port-channel不就完了吗&#xff1f;”但真上手以后才发…

作者头像 李华