轮转数组是算法面试中的经典问题,题目要求将一个数组中的元素向右移动k个位置。这个问题看似简单,但蕴含着多种解法思路,能够很好地考察程序员对数组操作、空间复杂度和时间复杂度的理解。
给定一个整数数组nums,我们需要将数组元素向右轮转k个位置。这里的"轮转"指的是将数组末尾的元素移动到数组开头,这个过程重复k次。
以示例1为例:
这个过程可以分解为三步:
在实际编码中,我们需要考虑几个边界条件:
最直观的思路是每次将数组元素向右移动一位,重复k次。这种方法的时间复杂度是O(n*k),空间复杂度是O(1)。对于大规模数组(如题目中的10^5长度),这种解法会超时。
cpp复制void rotate(vector<int>& nums, int k) {
int n = nums.size();
k %= n;
for (int i = 0; i < k; i++) {
int temp = nums[n-1];
for (int j = n-1; j > 0; j--) {
nums[j] = nums[j-1];
}
nums[0] = temp;
}
}
我们可以创建一个新数组,将原数组的元素按照轮转后的位置放入新数组。这种方法的时间复杂度是O(n),空间复杂度也是O(n)。
cpp复制void rotate(vector<int>& nums, int k) {
int n = nums.size();
vector<int> temp(n);
for (int i = 0; i < n; i++) {
temp[(i+k)%n] = nums[i];
}
nums = temp;
}
这是一种更高效的原地算法,通过将元素直接放置到它们最终的位置上。这种方法的时间复杂度是O(n),空间复杂度是O(1)。
cpp复制void rotate(vector<int>& nums, int k) {
int n = nums.size();
k %= n;
int count = 0;
for (int start = 0; count < n; start++) {
int current = start;
int prev = nums[start];
do {
int next = (current + k) % n;
swap(nums[next], prev);
current = next;
count++;
} while (start != current);
}
}
最优雅的解法是利用数组翻转的特性。这个方法基于一个关键的观察:当我们向右轮转数组k次时,数组末尾的k个元素会移动到数组开头,而其余元素会向后移动k个位置。
具体步骤如下:
cpp复制class Solution {
public:
void reverse(vector<int>& nums, int start, int end) {
while (start < end) {
swap(nums[start], nums[end]);
start += 1;
end -= 1;
}
}
void rotate(vector<int>& nums, int k) {
k %= nums.size();
reverse(nums, 0, nums.size() - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.size() - 1);
}
};
让我们通过示例来验证这个算法的正确性。以nums = [1,2,3,4,5,6,7], k=3为例:
最终结果确实是我们期望的[5,6,7,1,2,3,4]。
在实际编码中,有几个关键点需要注意:
cpp复制void rotate(vector<int>& nums, int k) {
if (nums.empty() || nums.size() == 1) return;
k %= nums.size();
if (k == 0) return;
// 翻转操作...
}
让我们比较几种解法的性能:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(n*k) | O(1) | 不推荐,仅用于理解问题 |
| 额外数组 | O(n) | O(n) | 空间允许时简单可靠 |
| 环状替换 | O(n) | O(1) | 原地算法,但实现较复杂 |
| 数组翻转 | O(n) | O(1) | 最优解,简洁高效 |
在实际面试或编程中,推荐使用数组翻转的方法,因为它既满足了原地操作的要求,又具有简洁的实现和优秀的性能。
这是最常见的错误。如果没有k %= nums.size()这一步,当k>n时会导致数组越界。
在实现翻转函数时,容易混淆start和end参数。确保start是区间起始索引,end是区间结束索引(包含)。
忘记处理空数组、单元素数组或k=0的情况,虽然不影响主要逻辑,但体现了代码的健壮性。
调试技巧:
同样的思路可以用于向左轮转数组,只需调整翻转的顺序:
cpp复制void rotateLeft(vector<int>& nums, int k) {
k %= nums.size();
reverse(nums, 0, k - 1);
reverse(nums, k, nums.size() - 1);
reverse(nums, 0, nums.size() - 1);
}
同样的算法可以应用于字符串的轮转:
cpp复制void rotateString(string &s, int k) {
k %= s.length();
reverse(s.begin(), s.end());
reverse(s.begin(), s.begin() + k);
reverse(s.begin() + k, s.end());
}
对于二维数组的轮转(如图像旋转),可以使用类似的翻转思路,但需要考虑行列转换。
轮转数组算法在实际中有多种应用:
在面试中遇到这个问题时:
记住:面试官不仅关注最终答案,更看重解题过程和思考方式。清晰的解释和稳健的代码同样重要。