7.6 分治-归并:LeetCode 315.计算右侧小于当前元素的个数


归并排序索引追踪法:LeetCode 315.计算右侧小于当前元素的个数


1. 题目链接

LeetCode 315. 计算右侧小于当前元素的个数
题目要求:给定一个整数数组 nums,返回一个数组 ret,其中 ret[i] 表示原数组中位于 nums[i] 右侧且比 nums[i] 小的元素个数。例如,输入 [5,2,6,1],输出 [2,1,1,0]


2. 题目描述
  • 输入:整数数组 nums,例如 [5,2,6,1]
  • 输出:数组 ret,其中 ret[i] 表示右侧比 nums[i] 小的元素数量。
  • 约束条件
    • 1 <= nums.length <= 1e5
    • -1e4 <= nums[i] <= 1e4

3. 示例分析

示例
输入:nums = [5,2,6,1]
输出:[2,1,1,0]

分步解析

  1. 初始索引映射
    • 元素与索引的初始关系为 [(5,0), (2,1), (6,2), (1,3)]
  2. 归并排序过程
    • 分解为 [5,2][6,1]
    • 左子数组排序后为 [2,5],索引变为 [1,0]
    • 右子数组排序后为 [1,6],索引变为 [3,2]
  3. 合并阶段统计
    • 合并 [2,5][1,6]
      • 2 > 1 → 右侧剩余元素 1 个(6),故 ret[1] += 1
      • 2 < 6 → 无统计。
      • 5 < 6 → 无统计。
      • 5 > 1 → 右侧剩余元素 0 个。
    • 合并结果为 [1,2,5,6],索引为 [3,1,0,2]
  4. 最终结果:根据索引映射累加统计值,得到 [2,1,1,0]

4. 算法思路
归并排序与索引映射的结合
  1. 索引数组的作用
    • 维护 index 数组,记录每个元素的原始位置。排序过程中元素位置变化,但通过 index 数组始终能追踪其原始下标。
  2. 合并阶段的统计逻辑
    • 当左子数组元素 nums[cur1] > nums[cur2] 时,右子数组剩余元素(cur2right)均小于 nums[cur1],统计数量为 right - cur2 + 1,结果累加到 ret[index[cur1]]
  3. 分治策略
    • 递归分解数组,排序过程中同步统计右侧小元素数量。
步骤拆解
  1. 初始化索引数组index[i] = i,表示元素的初始位置。
  2. 归并排序递归分解
    • 分解至单个元素,逐层合并并统计。
  3. 合并排序与统计
    • 双指针遍历左右子数组,按降序合并(便于统计右侧小元素)。
    • 更新索引数组 tmpIndex 和临时数组 tmpNums

5. 边界条件与注意事项
  1. 递归终止条件
    • left >= right 时,子数组长度为0或1,无需处理。
  2. 索引数组更新
    • 合并时必须同步交换元素的索引,确保能正确映射到原位置。
  3. 临时数组大小
    • tmpNumstmpIndex 的大小需足够(代码中设为 500010,可处理 n ≤ 1e5 的情况)。
  4. 时间复杂度
    • 归并排序的时间复杂度为 O(n log n)
  5. 稳定性问题
    • 合并时按降序排列,若 nums[cur1] == nums[cur2],优先插入右元素以避免重复统计。

6. 代码实现与解析
class Solution {
public:
    vector<int> ret, index;      // ret存储结果,index追踪元素原始位置
    int tmpNums[500010], tmpIndex[500010]; // 固定大小临时数组(防溢出)

    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        ret.resize(n, 0);        // 初始化结果数组
        index.resize(n);
        for (int i = 0; i < n; i++) 
            index[i] = i;        // 初始索引:元素与下标一一对应
        mergeSort(nums, 0, n - 1);
        return ret;
    }

    void mergeSort(vector<int>& nums, int left, int right) {
        if (left >= right) return;

        int mid = (left + right) / 2;
        mergeSort(nums, left, mid);    // 递归处理左子数组
        mergeSort(nums, mid + 1, 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 {
                // 左元素 > 右元素:统计右侧剩余元素数量
                ret[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];     // 同步更新索引
        }
    }
};
代码逐行解析
  1. 全局变量

    • ret:存储最终结果,ret[i] 对应原始元素 nums[i] 的统计值。
    • index:记录元素的原始下标,排序过程中随元素移动。
    • tmpNumstmpIndex:固定大小的临时数组,用于合并阶段的排序和索引更新。
  2. 初始化阶段

    • index[i] = i:建立元素与初始下标的映射关系。
  3. 合并阶段的核心逻辑

    • 降序排列:优先插入较大的元素,确保右侧剩余元素均小于当前左元素。
    • 统计触发条件:当 nums[cur1] > nums[cur2] 时,右子数组剩余元素数量为 right - cur2 + 1,结果累加到 ret[index[cur1]]
  4. 索引更新

    • 每次插入元素到临时数组时,同步记录其原始下标 index[cur1]index[cur2]
  5. 数据回写

    • 将临时数组的数据按合并后的顺序覆盖原数组,同时更新 index 数组。

总结

本解法通过归并排序的变种,在排序过程中利用索引数组追踪元素原始位置,巧妙统计右侧小元素的数量。算法的时间复杂度为 O(n log n),适用于大规模数据场景。关键在于合并阶段的降序排列与统计逻辑,以及索引数组的同步更新。此方法可扩展至其他分治统计问题,如翻转对、区间和的计数等。

你可能感兴趣的:(#,1.1leeCode算法习题,leetcode,算法,数据结构)