在算法面试和编程竞赛中,"计算右侧小于当前元素的个数"是一道经典的数组处理题目。题目要求我们为数组中的每个元素统计其右侧比它小的元素数量,最终返回一个统计结果数组。这道题看似简单,但直接使用暴力解法(双重循环)的时间复杂度高达O(n²),在数据量较大时(比如n=10⁵)会严重超时。
举个例子,对于输入数组[5,2,6,1],正确的输出应该是[2,1,1,0]。因为:
最直观的解法是使用双重循环:
cpp复制vector<int> countSmaller(vector<int>& nums) {
vector<int> res(nums.size(), 0);
for(int i = 0; i < nums.size(); i++) {
for(int j = i+1; j < nums.size(); j++) {
if(nums[j] < nums[i]) res[i]++;
}
}
return res;
}
这种解法虽然简单,但时间复杂度为O(n²),当n=10⁵时需要执行约100亿次比较,显然无法接受。
我们可以利用归并排序的特性来优化这个统计过程。归并排序的时间复杂度为O(nlogn),非常适合处理大规模数据。关键在于如何在排序过程中统计"右侧较小元素"的数量。
核心思路是:
由于归并排序会打乱元素的原始位置,我们需要一个额外的索引数组来跟踪每个元素的原始位置。这样在统计时才能将结果累加到正确的位置上。
我们需要准备以下数据结构:
tmpNums:临时存储合并过程中的元素值tmpIndex:临时存储合并过程中元素的原始索引v:最终的结果数组,记录每个元素右侧比它小的数量index:记录每个元素的原始索引cpp复制class Solution {
int tmpNums[500005]; // 临时数组存储元素值
int tmpIndex[500005]; // 临时数组存储原始索引
vector<int> v; // 结果数组
vector<int> index; // 原始索引数组
};
主函数负责初始化数据结构并启动归并排序:
cpp复制vector<int> countSmaller(vector<int>& nums) {
int n = nums.size();
v.resize(n, 0); // 初始化结果数组为0
index.resize(n);
for(int i = 0; i < n; i++) {
index[i] = i; // 初始化索引数组
}
mergeSort(nums, 0, n-1); // 开始归并排序
return v;
}
归并排序采用分治策略:
cpp复制void mergeSort(vector<int>& nums, int left, int right) {
if(left >= right) return; // 递归终止条件
int mid = left + (right - left)/2; // 计算中点
mergeSort(nums, left, mid); // 递归排序左半部分
mergeSort(nums, mid+1, right); // 递归排序右半部分
// 合并两个有序区间
merge(nums, left, mid, right);
}
合并过程是算法的核心,这里采用降序合并:
cpp复制void merge(vector<int>& nums, int left, int mid, int right) {
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) {
if(nums[cur1] <= nums[cur2]) {
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
} else {
v[index[cur1]] += right - cur2 + 1; // 关键统计步骤
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
}
// 处理剩余元素
while(cur1 <= mid) {
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
while(cur2 <= right) {
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
// 将临时数组拷贝回原数组
for(int j = left; j <= right; j++) {
nums[j] = tmpNums[j - left];
index[j] = tmpIndex[j - left];
}
}
在合并过程中,当发现左半部分的当前元素nums[cur1]大于右半部分的当前元素nums[cur2]时:
nums[cur2]是右半部分剩余元素中最大的cur2到right)都比nums[cur1]小right - cur2 + 1累加到结果中由于归并排序会改变元素的原始位置,我们必须通过index数组来跟踪每个元素的原始位置。这样在统计时才能将结果累加到正确的位置上。
采用降序合并(而非升序)可以让我们在发现nums[cur1] > nums[cur2]时,立即知道右半部分剩余的所有元素都比nums[cur1]小,从而简化统计过程。
如果发现统计结果不正确,可以检查:
right - cur2 + 1特别注意以下边界情况:
可以在合并过程中打印中间结果:
cpp复制cout << "Merging [" << left << "," << mid << "] and [" << mid+1 << "," << right << "]" << endl;
cout << "Current v: ";
for(auto x : v) cout << x << " ";
cout << endl;
这种基于归并排序的统计方法还可以解决:
除了归并排序,还可以使用:
在实际应用中,归并排序解法通常是最优选择,因为它既高效又相对容易实现。