LeetCode 315: 算右侧小于当前元素的个数(归并排序) —— 题解

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容: 《归并排序》

一.题目

315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

🎯 欢迎来到「计算右侧小于当前元素的个数」题解之旅! 本文将带你从"数一数每个数右边有几个比它小的"这一直观场景出发,深入理解归并排序 + 下标同步 的巧妙运用,并掌握如何在降序合并的瞬间把更小元素个数写回原下标 来在 O(n log n) 内算出每个位置的答案。

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 315 题,给定整数数组 nums,返回数组 count,其中 count[i] 是 nums[i] 右侧 比它严格小 的元素个数。本质上,暴力两两比较是 O(n²),而归并排序 合并两个有序段时,能一次性算出左段元素右侧有多少更小的 ,问题转化为分治过程中的批量计数。

  • 明确学习目标 :掌握降序合并 + 计数时机 ,理解下标数组 index 必须与数值数组同步 的原因,并熟练处理相等元素(<=) 与回填偏移等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 nums = [5,2,6,1] 输出 [2,1,1,0])。

本文将从问题转化、分治拆分、合并计数、下标同步 到代码实现,层层递进。即使你对逆序类计数 还不熟悉,我们也会从"右段已排好序,右段一个元素能一次性代表好几个更小的数"这一直觉出发,让你轻松抓住核心思想 ------边归并边记账,下标随身带。现在,让我们一起归并计数,算出每个位置右侧更小的个数吧!

二、算法分析

1. 问题分析(前置分析)

  • 题目要求:返回 count,count[i] 为 nums[i] 右侧严格小于它的元素个数。
  • 关键约束:n 可达 10⁵,暴力 O(n²) 会超时;要求严格小于 (相等不算);结果需按原始下标对应。
  • 核心思路:归并排序 升序合并时,两段各自有序 ;若左段当前元素更大,则右段剩余元素全部更小 ,可以一次性累加 它们的个数,再把计数写回该元素的原下标。

2. 算法策略(归并排序 + 降序合并 + 同步下标)

核心步骤:

  1. 初始化 :index[i] = i(记录每个位置的元素来自原数组哪个下标),结果数组 v 清零。
  2. 递归拆分 :取中点 mid = left + (right - left) / 2,分别处理 [left, mid] 与 [mid+1, right]。
  3. 降序合并 :cur1 指左段、cur2 指右段,大者先写入 tmp (nums[cur1] <= nums[cur2] 时取右段)。
  4. 计数时机 :当 nums[cur1] > nums[cur2](取左段 元素)时,右段剩余 [cur2, right] 全部更小 ,累加 v[index[cur1]] += right - cur2 + 1。
  5. 同步搬运 :每写一个值,同时写它的原下标 到 tmpIndex(数值与下标必须成对移动)。
  6. 回填 :nums[k] = tmpNums[k - left]、index[k] = tmpIndex[k - left]。

📊 示例 (nums = [5,2,6,1],期望 [2,1,1,0]):

阶段 区间 比较 操作 结果更新
合并 0,1 5 vs 2 5 > 2,取左段 5 v[0] += 1
合并 2,3 6 vs 1 6 > 1,取左段 6 v[2] += 1
合并 0,3 5 vs 6 5 ≤ 6,取右段 6 无
合并 0,3 5 vs 1 5 > 1,取左段 5 v[0] += 1(累计 2)
合并 0,3 2 vs 1 2 > 1,取左段 2 v[1] += 1
收尾 --- --- 搬入 1 无

三次计数后 v = [2,1,1,0] ✅,正好是每个元素右侧更小元素的个数。

3. 正确性说明

  • 公式正确 :右段为降序 ,[cur2, right] 中每个元素都 ≤ nums[cur2] ;若 nums[cur1] > nums[cur2],则它们全部严格小于 nums[cur1],个数恰为 right - cur2 + 1。
  • 计数时机正确 :只有"取左段元素"时才可能发现"右段有更小的";取右段元素时右段更大,不可能存在更小的,故不计数。
  • 下标正确 :每次搬运都数值与下标成对移动 ,v[index[cur1]] 精确写回该元素的原始位置,不会张冠李戴。
  • 跨层不重复 :每层只统计"左段元素 × 右段元素"之间的更小关系,段内部的关系在递归子层已统计,互不重叠。

📌 例子:为什么 right - cur2 + 1 恰好是更小个数

合并 [5,2] 与 [6,1](右段降序):取左段 5 时,右段剩余 [1] 全部更小 → 1 个;取左段 2 时,右段剩余仍是 [1] → 1 个。两个左段元素各自"看见"了自己右侧更小的元素,个数就是右段剩余长度,公式直接生效。

4. 实现细节

  • 初始化:tmpNums、tmpIndex 为成员静态数组 ,容量需 > n (n ≤ 10⁵,取 500005 足够);index[i] = i、v.resize(n)。
  • 边界防护:if (left >= right) return;(单元素无更小元素);mid = left + (right - left) / 2(防溢出);回填时偏移必须减 left (tmpNums[k - left]),且 nums 与 index 两条都要回填。
  • 复杂度:时间 O(n log n) (log n 层合并,每层总扫描 O(n)),空间 O(n) (两个静态数组 + index/v)。
  • 关键操作 :v[index[cur1]] += right - cur2 + 1;(写回原下标的计数)、tmpIndex[i++] = index[cur2++];(下标同步搬运)。

📌 例子:为什么必须同步维护 index

nums = [1,5,2]:初次合并后 [1,5] 被整理为 [5,1],index[0] 变成 1 (值 5 的原下标)。最终合并取到值 5 时,若写 v[cur1] 会记到 v[0](错),而 v[index[cur1]] = v[1] 才正确(值 5 在原始数组的下标是 1);最终 v = [0,1,0] ✅------排序打乱了位置,答案却必须按原下标落位 ,这就是 index 存在的意义。

5. 返回值

  • 返回 v:每个原始下标对应的右侧更小元素个数,对应题目"返回每个元素右侧小于它的元素个数数组"。

三.代码

cpp 复制代码
class Solution
{
    int tmpNums[500005];    // 临时数组:合并时存放数值
    int tmpIndex[500005];   // 临时数组:合并时存放下标(与数值同步移动)
    vector<int> v;          // 结果:v[i] = nums[i] 右侧严格小于它的元素个数
    vector<int> index;      // 当前位置的元素来自原数组的哪个下标

public:
    vector<int> countSmaller(vector<int>& nums)
    {
        int n = nums.size();
        v.resize(n);
        index.resize(n);
        for (int i = 0; i < n; i++)
        {
            index[i] = i;       // 初始时位置 i 的元素就来自原下标 i
        }
        mergeSort(nums, 0, n - 1);
        return v;
    }

    void mergeSort(vector<int>& nums, int left, int right)
    {
        // 1. 递归终止:单元素无"右侧更小元素"
        if (left >= right)
        {
            return;
        }

        // 2. 取中点,分别排序左右两段
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid + 1, right);

        // 3. 降序合并:大者优先写入 tmp
        int cur1 = left;        // 左段指针
        int cur2 = mid + 1;     // 右段指针
        int i = 0;              // tmp 写入位置

        while (cur1 <= mid && cur2 <= right)
        {
            if (nums[cur1] <= nums[cur2])
            {
                // 右段元素更大或相等:先取右段(相等不计入"更小")
                tmpNums[i] = nums[cur2];
                tmpIndex[i++] = index[cur2++];
            }
            else
            {
                // 左段元素更大:右段剩余 [cur2, right] 全部比它小
                v[index[cur1]] += right - cur2 + 1;
                tmpNums[i] = nums[cur1];
                tmpIndex[i++] = index[cur1++];
            }
        }

        // 4. 收尾:剩余元素与下标成对搬运
        while (cur1 <= mid)
        {
            tmpNums[i] = nums[cur1];
            tmpIndex[i++] = index[cur1++];
        }
        while (cur2 <= right)
        {
            tmpNums[i] = nums[cur2];
            tmpIndex[i++] = index[cur2++];
        }

        // 5. 回填:数值与下标两条都要还原,tmp 从 0 写起故减去 left
        for (int k = left; k <= right; k++)
        {
            nums[k] = tmpNums[k - left];
            index[k] = tmpIndex[k - left];
        }
    }
};

四、易错点分析

难点1:这是降序合并,计数发生在"取左段"时

cpp 复制代码
if (nums[cur1] <= nums[cur2])
{
    tmpNums[i] = nums[cur2];              // 取右段(大者先)
}
else
{
    v[index[cur1]] += right - cur2 + 1;   // 取左段时才计数
}

与 LCR 170 的"升序合并 + 取右段计数"恰好镜像 :本题要求"右侧更小 ",所以让大者先出 (降序),当左段元素更大时才说明右段剩余"全部更小",此时计数 right - cur2 + 1。若照搬 170 的升序写法,计数方向与公式全部对不上,结果错误。

难点2:index 必须与 nums 全程同步

cpp 复制代码
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];    // 数值走一步,下标也走一步

排序会打乱元素位置 ,但答案必须按原始下标 落位。因此每次搬运、每个收尾、每次回填都要成对处理 nums 与 index:漏搬下标、或回填只还原了 nums,都会让 v[index[...]] 写到错误位置------"数值与下标是绑定的两条轨道",这是本题区别于 170 题的最大特征。

难点3:取右段元素时绝不能计数

cpp 复制代码
if (nums[cur1] <= nums[cur2])
{
    // 这里只有搬运,没有 v += ...
}

当右段元素更大(或相等)时,它右侧(右段内部)后续元素只会更小或相等 ,但它本身不是"比左段元素小"------左段元素右侧没有发生"更小"关系。若在这个分支也累加,就会把"更大的元素"误当成"更小的"计数,结果偏大。

难点4:<= 让相等元素不计入(严格小于)

cpp 复制代码
if (nums[cur1] <= nums[cur2])

题目要求严格小于 。nums[cur1] == nums[cur2] 时用 <= 取右段,不累加 ,正确;若写成 <,相等时会走 else 累加 right - cur2 + 1,把相等的元素也算成"更小" ,结果偏大。一个符号决定答案是否包含等号。

五、流程图

🎯 闭幕

🎉 恭喜你完成了「计算右侧小于当前元素的个数」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践 在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 代码用 v[index[cur1]] += right - cur2 + 1 。请问为什么不能写成 v[cur1] += ... ?用 nums = [1,5,2] 说明。
  • index 数组在每次搬运和回填时都要同步。请问如果只还原 nums、漏还原 index 会发生什么?
  • 右段元素被取出时不计数 。请问如果在这个分支也累加会得到什么?它对应的是"更大"还是"更小"?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏 👤 关注作者,获取更多题解 💬 留言交流你的疑问或优化思路

📌 深入思考答案

  • 因为排序后元素位置已变,cur1 只是"当前数组位置",不代表原始下标。[1,5,2] 初次合并后值 5 位于位置 0 但原下标是 1,若写 v[cur1] 会记到 v[0];必须写 v[index[cur1]] = v[1] 才正确,最终得到 [0,1,0]。
  • 只还原 nums 会让 index 仍停留在旧状态,与数值不再对应 ;后续合并时 v[index[...]] 会写到错误位置,结果整体错乱------数值与下标必须始终成对移动、成对回填。
  • 右段元素被取出说明它更大或相等 ,其右侧后续元素即使更小,也与"左段元素右侧更小"无关;在此累加会把更大的元素 误算成"更小个数",v 偏大。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
像风一样自由20202 小时前
2.如何用FastAPI把一个Python算法封装成HTTP服务
python·算法·fastapi
D e re k ZH2 小时前
保序回归:从 PAVA 到可并堆的单调约束优化
c++·算法·数据挖掘·回归·kotlin
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:字典树前缀树与单词检索统计
c++·python·算法·bfs·信息学竞赛
文丑颜不良啊2 小时前
LeetCode1401~1420题目解析
java·leetcode
Nuanyt2 小时前
LeetCode Hot 100 链表专题2 3 题 (Java)
java·笔记·算法·leetcode·链表
文丑颜不良啊2 小时前
LeetCode1341~1360题目解析
java·leetcode
海绵宝宝转agent3 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
octopus_c3 小时前
C++ 运算符重载详解 && 日期类的实现
开发语言·c++
青少儿编程课堂3 小时前
2026蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:堆排序与优先队列解析
c++·python·算法·bfs·信息学竞赛