在算法面试中,数组(Array)类题目往往看似简单,实则暗藏玄机。暴力解法通常很容易想到,但要在 O(n) 时间复杂度和 O(1) 空间复杂度(或受限条件)下完成,就需要一些巧妙的数学逻辑。
今天整理了两道非常经典的数组题目:轮转数组 和除自身以外数组的乘积,重点分析它们背后的解题思路。
一、轮转数组 (Rotate Array)
1. 题目简述
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
2. 解题思路:三次反转法
如果使用额外的数组来暂存数据,空间复杂度是 O(n),但这题通常考察的是原地操作,O(1)空间复杂度。
核心技巧在于"翻转"。我们可以把数组想象成两部分:需要移动到前面的尾部元素 和 需要后移的头部元素。
通过整体和局部的三次翻转,刚好可以将位置对调。
步骤演示:
假设数组为 [1, 2, 3, 4, 5, 6, 7],k = 3。
-
整体翻转 :
[7, 6, 5, 4, 3, 2, 1]- 此时,原本在尾部的
5, 6, 7跑到了最前面,但顺序是反的。
- 此时,原本在尾部的
-
翻转前 k 个 :
[5, 6, 7, 4, 3, 2, 1]- 前
k个元素顺序恢复正常。
- 前
-
翻转剩余部分 :
[5, 6, 7, 1, 2, 3, 4]-
后半部分元素顺序恢复正常。
-
完成!
-
3. 代码实现 (C++)
C++代码:
cpp
class Solution {
public:
void rotate(vector<int>& nums, int k) {
// 核心思路:整体反转 + 两次局部反转
// 坑点注意:k 可能大于数组长度,必须取模
k %= nums.size();
// 1. 翻转全部
reverse(nums.begin(), nums.end());
// 2. 翻转前 k 个
reverse(nums.begin(), nums.begin() + k);
// 3. 翻转剩余部分
reverse(nums.begin() + k, nums.end());
}
};
4. 复杂度分析
-
时间复杂度 :O(n)。
reverse函数本质是遍历交换,三次操作依然是线性时间。(reverse底层是依靠双指针实现的复杂度为O(n))。 -
空间复杂度:O(1)。没有使用额外的数组空间,仅用了常数个变量。
二、除自身以外数组的乘积 (Product of Array Except Self)
1. 题目简述
给你一个整数数组 nums,返回数组 ans,其中 ans[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
要求:不能使用除法,且在 O(n) 时间内完成。
2. 解题思路:前缀积与后缀积
如果能用除法,算出所有数的乘积再除以 nums[i] 即可(需考虑 0 的情况)。但题目禁止除法,我们就需要转换思路。
ans[i] 的值其实可以拆解为两部分:

我们可以利用空间换时间的思想,预处理出两个数组:
-
pre数组 :pre[i]表示i左边所有元素的乘积。 -
suf数组 :suf[i]表示i右边所有元素的乘积。
最后遍历一遍,将两者相乘即可。
3. 代码实现 (C++)
C++代码:
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> suf(n);
vector<int> pre(n);
vector<int> ans;
// 初始化边界:第一个数左边没数,视为1;最后一个数右边没数,视为1
pre[0] = 1;
suf[n - 1] = 1;
// 1. 计算前缀积
for (int i = 1; i < n; ++i) {
pre[i] = pre[i - 1] * nums[i - 1];
}
// 2. 计算后缀积 (从右向左遍历)
for (int i = n - 2; i >= 0; --i) {
suf[i] = suf[i + 1] * nums[i + 1];
}
// 3. 组合结果
for (int i = 0; i < n; ++i) {
ans.push_back(pre[i] * suf[i]);
}
return ans;
}
};
4. 复杂度分析
-
时间复杂度:O(n)。我们进行了三次独立的 for 循环遍历,总体是线性的。
-
空间复杂度 :O(n)。使用了
pre和suf两个辅助数组。- 进阶提示:如果题目要求 O(1) 空间(输出数组不计入空间),可以先用输出数组
ans存前缀积,再用一个变量动态维护后缀积来计算,从而省去pre和suf数组。
- 进阶提示:如果题目要求 O(1) 空间(输出数组不计入空间),可以先用输出数组
三、总结
这两道题虽然解法不同,但都体现了优化算法的常见方向:
-
找规律:如"轮转数组"中的反转规律,通过数学变换避免模拟移动的开销。
-
预处理:如"除自身以外数组的乘积",通过预先计算好前缀和后缀信息,避免了双重循环的重复计算。