[LeetCode] 189. Rotate Array
题目
Given an array, rotate the array to the right by k steps, where k is non-negative.
Example 1:
1 | Input: [1,2,3,4,5,6,7] and k = 3 |
Example 2:
1 | Input: [-1,-100,3,99] and k = 2 |
Note:
Try to come up as many solutions as you can, there are at least 3 different ways to solve this problem.
Could you do it in-place with O(1) extra space?
解题报告
思路
这是一道原题相对简单的模拟题。要写出一个简单能够通过而不超时的方法并不难,只要利用至多k元素的空间,就可以完成数组的拆解和重新拼接。
难点在思考题中的 O(1) extra space 部分。这说明我们只能使用常数个临时元素,而这个常数不能随数组长度或事 k 的大小而变化。因此,我们使用 k 个元素的临时数组完成交换的想法是无法实现的。因此解法涉及到一些想法和数论知识。
方法一:暴力拆解
暴力拆解的解法就不多说了,就是将数组拆为两个部分再拼接,只需要计算好拆解的位置即可:
1 | class Solution(object): |
需要提示的是,虽然不进行第一步的取模操作也仍然能通过,我还是建议对 k 取模以保证解法的严谨性,因为题目中从未保证 k 是一个小于 n 的数字。
另外数组的更新需要使用 nums[:]
而非nums
,原因是nums
在函数中被重新赋值会被视作是本地变量,而非传入的参数(很复杂,需要研究 Python 原理了……)。
复杂度
该算法的时间复杂度可以视作 O(1),但数组的拆解利用了额外 O(n) 的空间。
方法二:循环更新
循环更新的方法更多的思考,但是也是为数不多时间复杂度够好也仅用 O(1) 空间复杂度的方法。
如果只给一个临时变量的话,很多人的第一想法一定是一位一位移、循环 k 次,就可以得到结果数组,但这种往往太过复杂;而第二想法就会是,把 0 位的数移到 k 位,再把 k 位的移到 2k 位,以此类推……
但是我们如何保证这种做法遍历一次数组呢?不如想想跳跃多少个 k 次可以回到原点点吧。为了简化问题,我们就从 0 开始跳跃,跳回到 0 点的条件便是所在位置是 n 的倍数。我们把 k 和 n = len(nums) 的关系简单分成三种:
- n 是 k 的倍数时:一共需要 次到达越界的 n 次,从而重新回到 0 点。
- n 和 k 互质时:由于互质,在 前,k 的倍数无法成为 n 的倍数,因此一共需要 n 次才能达到 n 的倍数,从而重新回到 0 点。
- 当 n 和 k 既不互素也不是倍数的时候:一共需要次跳跃,就能回到 0 点了。(为最小公倍数)
当这样理下来之后,我们就会发现三种情况完全就是一模一样的:不论 k 和 n 是什么关系,循环一次所需的跳跃步数一共是 k 和 n 的**最小公倍数除以 k **次。由于每一个数字在交换一次时就会处于自己应该在的地方,所以总共需要的交换的次数是 n 次,因此一共需要 次循环。(为最小公倍数,为最大公约数)
那么这最大公约数次的循环如果从 0 开始,第二个数字要怎么选呢?反过来想,如果我们选的下一个数字就是 1 会怎样呢?我们看看 0 的下一个数字 1 有什么看法。假如说 0 和 1 在一个循环中同时被更新了,那说明什么呢?简单来说,因为每 k 数会被更新一次,因此, ,进行化简后我们发现。对啦!这正是大名鼎鼎的拓展欧几里得公式,而等式右边的 1 说明了 k 和 n 是互质的两个数(如果有兴趣的话,你也可以证明 1 之后可以选 2,2 之后可以选 3……)。也就是说,在除了互质的情况下,如果我们的循环结束了,选择开始数字的下一个准没错。你问互质的情况怎么办?看看上面的推论,如果 k 和 n 互质的话,再回到 0 点的时候一圈数字就都更新完了。
当我们证明了这一切之后,写代码就变得容易了起来:
1 | class Solution(object): |
复杂度
由于每个数字只被更新一次,时间复杂度为 O(n);由于只用了常数个临时变量(count, start, curr, temp),空间复杂度为 O(1)
结语
时间换空间的算法深入人心,在结题报告中,我列举了两种算法,其中一种用时应当更短,但占用更多空间;而另一种用时稍长,但几乎不需要额外空间。除了记住这一题的解法,我觉得更重要的是掌握时间和空间的魔法吧╰( ͡° ͜ʖ ͡° )つ──☆*:・゚