当前位置: 首页 > news >正文

LeetCode算法日记 - Day 27: 计算右侧小于当前元素的个数、翻转对

目录

1. 计算右侧小于当前元素的个数

1.1 题目解析

1.2 解法

1.3 代码实现

2. 翻转对

2.1 题目解析

2.2 解法

2.3 代码实现


1. 计算右侧小于当前元素的个数

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

给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。

示例 1:

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

示例 2:

输入:nums = [-1]
输出:[0]

示例 3:

输入:nums = [-1,-1]
输出:[0,0]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

1.1 题目解析

  • 题目本质

    对每个位置 i,统计它右侧有多少元素 < nums[i]。这是“离线统计 + 有序累积”的典型问题:如果能让“右侧区域”保持有序,那么当一个左侧元素要“落位”时,就能一次性把比它小的右侧元素数目加上去。

  • 常规解法:对每个 i 向右扫一遍计数,或把每个 i 与其右边所有元素比较。
    复杂度 O(n²),n 最多 1e5,超时。

  • 问题分析:O(n²) 不行;要降低到 O(n log n)。要么用“平衡树/树状数组/线段树”边插边查,要么在“归并排序”的有序合并过程中顺手统计。

  • 思路转折:用 归并排序(分治)。关键是不直接搬值,而是搬“原始下标”

    • 比较用 nums[index[*]];

    • 计数写回 counts[index[*]]。
      这样既能用数值排队,又不丢原始位置,统计才能落对人。

1.2 解法

算法思想
维护一个下标数组 index,对下标做归并:

        降序归并 + 一次性加法:左右两段都保持降序。若 nums[left] > nums[right],则右半段从 cur2 到 right 的所有元素都更小,直接 += (right - cur2 + 1) 并把左元素出列;否则先出右元素。

i)初始化:index[i]=i,counts 全 0,准备 tmp 临时数组。

ii)分治:mergeSort(nums, left, right)。

iii)合并:

  • 指针 cur1 =left, cur2 =mid+1,k 写入 tmp。

  • 若 nums[index[cur1]] <= nums[index[cur2]]
    tmp[k++]=index[cur2++]。

  • 否则:counts[index[cur1]] += (right - j + 1)
    tmp[k++]=index[cur1++]。

  • 扫尾:
    while (cur1 <= mid) tmp[k++] = index[cur1++];
    while (cur2 <= right) tmp[k++] = index[cur2++];

  • 回写:
    for (int j = left; j <= right; j++) index[j] = tmp[j - left];

iiii)返回 counts 的列表形式。

易错点

  • 只搬下标,不搬值:比较写 nums[index[*]],统计写 counts[index[*]]。

  • 回写的是 index,不是 nums:index[j]=tmp[j-left] 或让 k 从 left 开始写

  • 注意 while 循环里比大小时判断的条件

  • 多层归并要累加:counts[...] += ...,不能覆盖。

  • tmp 不是答案数组,它只是合并时的 辅助排序区,归并用的临时缓冲区,帮助我们把当前区间 [left..right] 合并成有序;排序这一步是必须的,因为我们借助“有序”的过程,才能在 O(n log n) 里顺手统计“右边更小”的个数。

  • counts:是真正的答案数组,counts[cur1] 表示 nums[cur1] 右侧比它小的数量,最后返回它即可。

  • k++ 只是为了推进左半区的扫描,别忘了这个数组是记录下标而不是原始数组的值,所以要推进下标数组的下标值。

  • index 数组的作用是“保持数值和原始下标之间的映射关系不丢失”。

1.3 代码实现

import java.util.*;class Solution {int[] index;   // 记录每个元素的“原始下标”,在归并过程中只搬动下标int[] tmp;     // 临时数组,用来辅助归并排序(保存下标,不保存值)int[] counts;  // 结果数组,counts[i] = nums[i] 右侧比它小的数量public List<Integer> countSmaller(int[] nums) {int n = nums.length;tmp = new int[n];counts = new int[n];index = new int[n];// 初始化下标数组,初始时 index[i] = ifor (int i = 0; i < n; i++) {index[i] = i;}// 分治归并排序mergeSort(nums, 0, n - 1);// 把 counts 数组转成 List<Integer> 返回List<Integer> list = new ArrayList<>(n);for (int v : counts) list.add(v);return list;}public void mergeSort(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);int cur1 = left, cur2 = mid + 1, k = 0;// 合并两个有序区间(这里用“降序”方式)while (cur1 <= mid && cur2 <= right) {if (nums[index[cur1]] <= nums[index[cur2]]) {// 如果右边的值更大或相等,先把右边的下标放进 tmptmp[k++] = index[cur2++];} else {// 如果左边的值更大:// 说明右边 [cur2..right] 的元素全都比它小counts[index[cur1]] += (right - cur2 + 1);// 把这个左边元素的下标放进 tmptmp[k++] = index[cur1++];}}// 把左半区剩余元素拷贝进 tmpwhile (cur1 <= mid) tmp[k++] = index[cur1++];// 把右半区剩余元素拷贝进 tmpwhile (cur2 <= right) tmp[k++] = index[cur2++];// 回写:把 tmp 里的结果拷回到 index 数组对应区间for (int j = left; j <= right; j++) {index[j] = tmp[j - left];}}
}

复杂度分析

  • 时间:分治 + 合并为 O(n log n),每层合并线性。

  • 空间:index/tmp/counts 为 O(n),递归栈 O(log n)。

2. 翻转对

493. 翻转对 - 力扣(LeetCode)

给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对

你需要返回给定数组中的重要翻转对的数量。

示例 1:

输入: [1,3,2,3,1]
输出: 2

示例 2:

输入: [2,4,3,5,1]
输出: 3

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

2.1 题目解析

  • 题目本质:统计所有满足 i < j 且 nums[i] > 2 * nums[j] 的二元组数量。这是一个“离线统计 + 有序累积”的经典范式:如果左右两段各自有序,就能用双指针线性地数出跨段满足条件的配对数量。

  • 常规解法:双重循环枚举 (i, j),逐对判断是否 nums[i] > 2*nums[j]。时间复杂度 O(n^2),n 最多可到 5e4,显然超时。

  • 问题分析:要把复杂度压到 O(n log n)。两条常见路径:
    1)平衡树 / 树状数组 / 线段树做“边插边查”;
    2)分治归并:在“合并”前用双指针数跨段配对,再做标准归并。

  • 思路转折:采用分治归并。关键点:

    • 先数对,后归并:当左右段([left..mid] 与 [mid+1..right])各自已升序时,固定左段的 i,右段用 j 单调右移,线性统计满足 nums[i] > 2*nums[j] 的个数。

    • 用 long 比较避免溢出::写成 (long)nums[i] > 2L * (long)nums[j],不要用 /2.0 的浮点比较。

    • 归并阶段使用标准升序合并,保持下一层统计的前提(左右各自升序)。

2.2 解法

算法思想

分治:mergeSort(left, right)。在回溯(合并)前,借助“两侧各自升序”,对每个 i ∈ [left..mid],移动 j 直到不满足条件为止,累计 j - (mid + 1)。再做一次标准升序归并,保证整体有序供更高层使用。

i)递归到底:left >= right 返回。

ii)递归左右:mergeSort(left, mid)mergeSort(mid+1, right)

iii)先统计

  • j = mid + 1;

  • 对每个 i = left..mid:while (j <= right && (long)nums[i] > 2L * (long)nums[j]) j++;

  • 累加 result += (j - (mid + 1))。

iiii)再归并(升序):常规双指针把 [left..mid] 与 [mid+1..right] 合并到 tmp,再回写。

iiiii)返回最终 result。

易错点

  • 不要边合并 边用 (right - cur2 + 1) 一把加:那是普通逆序对 nums[i] > nums[j] 的套路,对 > 2*nums[j] 不成立。

  • 比较必须用 long:2 * nums[j] 在 int 里可能溢出。

  • 不要用/2.0:浮点精度 + 负数边界容易出错,也不必要。

  • 双计数陷阱:要么用全局 result,要么函数返回值累计,二者不要混用

  • 合并要升序:保证下一层“先数对”的前提(左右各自已升序)。

  • 指针单调:统计阶段 j 只右移不回退,整体线性。

2.3 代码实现

import java.util.*;class Solution {int[] tmp;int result;public int reversePairs(int[] nums) {tmp = new int[nums.length];mergeSort(nums, 0, nums.length - 1);return result;}// 只做副作用统计与排序,不用返回值累计public void mergeSort(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);// 先统计:左右两段各自已升序int j = mid + 1;for (int i = left; i <= mid; i++) {while (j <= right && (long)nums[i] > 2L * (long)nums[j]) j++;// 右半段 [mid+1, j-1] 都满足 nums[i] > 2*nums[?]result += (j - (mid + 1));}// 再归并:标准升序合并int cur1 = left, cur2 = mid + 1, k = 0;while (cur1 <= mid && cur2 <= right) {if (nums[cur1] <= nums[cur2]) {tmp[k++] = nums[cur1++];} else {tmp[k++] = nums[cur2++];}}while (cur1 <= mid)  tmp[k++] = nums[cur1++];while (cur2 <= right) tmp[k++] = nums[cur2++];// 回写for (int t = 0; t < k; t++) {nums[left + t] = tmp[t];}}
}

复杂度分析

  • 时间:每层“先数对”用两个单调指针线性扫描 O(n),合并 O(n),层数 O(log n),总体 O(n log n)。

  • 空间:辅助数组 tmp O(n),递归栈 O(log n)。

http://www.xdnf.cn/news/1406899.html

相关文章:

  • 高校心理教育辅导系统的设计与实现|基于SpringBoot高校心理教育辅导系统的设计与实现
  • USB虚拟化应用5:VirtualFIDO2 虚拟硬件安全密钥,智能卡,yubico,支持X,FB,GITHUB等各种网站双重认证,让你的账户登录绝对安全
  • 在集群级别应用 Pod 安全标准
  • opencv 梯度提取
  • 数据化管理是什么意思?企业该如何进行数据化管理
  • 《SVA断言系统学习之路》【01】即时断言概览
  • 北京博乐科技有限公司2025届程序技术类笔试题
  • 性能测试工具-SkyWalking
  • 元宇宙与旅游产业:虚实融合的文旅新体验
  • Python毕业设计推荐:基于Django+MySQL的养老社区服务管理系统
  • 从 WPF 到 Avalonia 的迁移系列实战篇4:控件模板与 TemplatedControl
  • UniApp 基础开发第一步:HBuilderX 安装与环境配置
  • 【AI智能体技术】如何学习多智能体系统知识并实现SOTA算法?
  • SDL3.0 学习随笔:其一
  • 自底向上了解CPU的运算
  • 嵌入式常见架构
  • 【MYSQL】从混乱到清晰:联合查询帮你打通数据孤岛
  • 算法:插入排序
  • 公益免费二级域名
  • 解锁Tensor Core性能:深入探索CUDA Warp矩阵操作
  • Junior Engineer浅谈CAS
  • 【百度】C++开发(25届提前批 一面)面经
  • 时序数据库
  • GitHub 热榜项目 - 日榜(2025-08-31)
  • 使用cursor claude sonnet4的一些感受
  • PY32F002不小心设置了SWD复用的恢复
  • Chrome++插件与GreenChrome:增强Chrome浏览器功能
  • Spring Boot 3.0 应用 HTTP 到 HTTPS 技术改造方案
  • 《潮汐调和分析原理和应用》之四S_Tide使用2
  • Java中不太常见的语法-总结