题面
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入: nums = [-1,-100,3,99], k = 2 输出:[3,99,-1,-100] 解释: 向右轮转 1 步: [99,-1,-100,3] 向右轮转 2 步: [3,99,-1,-100]
提示:
1 <= nums.length <= 105-231 <= nums[i] <= 231 - 10 <= k <= 105
进阶:
- 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
- 你可以使用空间复杂度为
O(1)的 原地 算法解决这个问题吗?
分析
-
可以开辟一个额外的数组,先遍历至第 nums.size() - k 的位置,然后开始向新数组中填入数字,而新的 index 需要 mod nums.size() 来确保位置正确,这个方法需要额外的空间
-
可以在 nums.size()-k 位置,左右两侧分别进行反转,最后再整个反转新的数组,这个方法原地操作不需要额外的空间
-
代码实现
class Solution { public: void rotate(vector<int>& nums, int k) { k = k % nums.size(); int mid = nums.size() - k; for (int i = 0, j = mid - 1; j > i; i++, j--) { swap(nums[i], nums[j]); } for (int i = mid, j = nums.size() - 1; j > i; i++, j--) { swap(nums[i], nums[j]); } for (int i = 0, j = nums.size() - 1; j > i; i++, j--) { swap(nums[i], nums[j]); } } };
环状替换
-
方法一中使用额外数组的原因在于如果我们直接将每个数字放至它最后的位置,这样被放置位置的元素会被覆盖从而丢失。因此,从另一个角度,我们可以将被替换的元素保存在变量
temp中,从而避免了额外数组的开销 -
我们从位置 0 开始,最初令
temp = nums[0]。根据规则,位置 0 的元素会放至 的位置,令 ,此时交换temp和nums[x],完成位置 的更新。然后,我们考察位置 ,并交换temp和nums[(x + k) \bmod n],从而完成下一个位置的更新。不断进行上述过程,直至回到初始位置 0 -
容易发现,当回到初始位置 0 时,有些数字可能还没有遍历到,此时我们应该从下一个数字开始重复的过程,可是这个时候怎么才算遍历结束呢?我们不妨先考虑这样一个问题:从 0 开始不断遍历,最终回到起点 0 的过程中,我们遍历了多少个元素?
-
由于最终回到了起点,故该过程恰好走了整数数量的圈,不妨设为 圈;再设该过程总共遍历了 个元素。因此,我们有 ,即 一定为 的公倍数。又因为我们在第一次回到起点时就结束,因此 要尽可能小,故 就是 的最小公倍数 ,因此 就为
-
这说明单次遍历会访问到 个元素。为了访问到所有的元素,我们需要进行遍历的次数为
-
其中 指的是最大公约数
-
我们用下面的例子更具体地说明这个过程:
nums = [1, 2, 3, 4, 5, 6] k = 2 -
如果读者对上面的数学推导的理解有一定困难,也可以使用另外一种方式完成代码:使用单独的变量
count跟踪当前已经访问的元素数量,当count = n时,结束遍历过程 -
代码实现
class Solution { public: void rotate(vector<int>& nums, int k) { int n = nums.size(); k = k % n; int count = gcd(k, n); for (int start = 0; start < count; ++start) { int current = start; int prev = nums[start]; do { int next = (current + k) % n; swap(nums[next], prev); current = next; } while (start != current); } } };