算法原理
核心是将元素的移动看作几个独立的闭环。
1. 核心目标:元素去哪儿?
首先,我们要明确每个元素的最终归宿。在一个大小为 n
的数组中,向右旋转 k
步,意味着:
- 索引
i
处的元素,应该移动到索引(i + k) % n
处。 - 索引
(i + k) % n
处的元素,应该移动到((i + k) % n + k) % n
,即(i + 2k) % n
处。 - ...以此类推。
我们可以把这个移动关系看作一条"锁链"。
2. 最简单的情况:一个大圈 (Single Cycle)
我们先从最理想的情况开始:所有元素都属于同一个"圈"。
例子:n = 7
, k = 3
0
号位的元素要去3
号位。3
号位的元素要去6
号位。6
号位的元素要去(6 + 3) % 7 = 2
号位。2
号位的元素要去5
号位。5
号位的元素要去(5 + 3) % 7 = 1
号位。1
号位的元素要去4
号位。4
号位的元素要去(4 + 3) % 7 = 0
号位。
我们发现,从 0 号位出发,沿着"要去的位置"一直走,最终会回到 0 号位,并且不重不漏地经过了每一个位置。
0 -> 3 -> 6 -> 2 -> 5 -> 1 -> 4 -> 0
既然它们形成了一个完整的闭环,我们就可以这样操作:
-
保存起点 :先把
0
号位的元素nums[0]
拿出来,存在一个临时变量temp
里。现在0
号位就"空"了。 -
开始换位:
- 把
4
号位的元素放到0
号位。现在4
号位"空"了。 - 把
1
号位的元素放到4
号位。现在1
号位"空"了。 - ...
- 把
3
号位的元素放到6
号位。现在3
号位"空"了。
- 把
-
终点复位 :最后,把我们一开始保存的
temp
(也就是最初0
号位的元素) 放入最后空出来的3
号位。
这样,我们只用了一个临时变量的空间,就完成了所有元素的交换。
3. 复杂的情况:多个独立的圈 (Multiple Cycles)
那是不是所有情况都这么理想呢?并不是。
例子:n = 6
, k = 2
-
从
0
号位出发:0 -> 2 -> 4 -> (4 + 2) % 6 = 0
。- 这是一个小圈!它只涉及了
0, 2, 4
三个位置的元素。 1, 3, 5
号位置的元素完全没有被触碰到。
- 这是一个小圈!它只涉及了
-
我们从未被触碰过的
1
号位再出发:1 -> 3 -> 5 -> (5 + 2) % 6 = 1
。- 这是另一个独立的小圈!它只涉及了
1, 3, 5
三个位置。
- 这是另一个独立的小圈!它只涉及了
在这个例子里,整个数组被巧妙地分解成了两个完全独立的圈。我们只需要对每个圈分别执行上面"单圈"的换位逻辑,就能完成整个数组的旋转。
4. 原理的核心:最大公约数 (GCD)
现在最关键的问题来了:到底会有多少个独立的圈呢?
数学家们已经证明,这个圈的数量不多不少,正好是数组大小 n
和旋转步数 k
的最大公约数 ,即 gcd(n, k)
。
让我们验证一下:
n = 7, k = 3
:gcd(7, 3) = 1
。所以只有 1 个大圈。n = 6, k = 2
:gcd(6, 2) = 2
。所以有 2 个独立的圈。n = 12, k = 8
:gcd(12, 8) = 4
。所以会有 4 个独立的圈,每个圈里有12 / 4 = 3
个元素。
并且,这 gcd(n, k)
个圈的起始点,正好就是数组的前 gcd(n, k)
个位置:0, 1, 2, ..., gcd(n, k) - 1
。
5. 总结:完整的算法原理
综合以上分析,我们可以得出"循环换位"算法的完整逻辑:
-
确定圈数 :计算
g = gcd(n, k)
。这告诉我们总共有g
个独立的换位循环。 -
遍历每个圈 :写一个外层循环,从
i = 0
到g - 1
。每一次循环处理一个完整的圈,这个圈的起始点就是i
。 -
对圈内进行换位:在内层循环里,执行我们上面"单圈"的逻辑:
- 用
temp
保存nums[i]
的值。 - 从
i
号位开始,沿着"锁链"把前一个位置的元素搬过来,直到绕回i
。 - 把
temp
放到最后空出的位置。
- 用
-
当外层循环结束时,所有
g
个圈都已完成换位,整个数组也就旋转完毕了。
这个算法的精妙之处在于,它通过数论(最大公约数)预见了数组内元素的移动模式,并将一个复杂的操作分解为几个简单、独立的循环,从而在不使用额外数组空间的情况下,高效地完成了任务。
代码实现
py
import math
def solution(nums, n, k):
g = math.gcd(n, k) # 计算小圈的个数
k %= n
for i in range(g):
tmp = nums[i] # 保存每圈的第一个元素
cur = i
while True:
pre = (cur - k) % n # 计算出当前位置在旋转后应存放元素的位置索引
if pre == i: # 追溯到圈的起点,本圈结束
break
nums[cur] = nums[pre]
cur = pre
nums[cur] = tmp # 将最开始保存的元素(temp),放到最后空出的位置上
if __name__ == "__main__":
n = int(input())
nums = list(map(int, input().split()))
k = int(input())
solution(nums, n, k)
print(" ".join(map(str, nums)))