排序题目:翻转对

news/2024/12/21 21:44:18/

文章目录

  • 题目
    • 标题和出处
    • 难度
    • 题目描述
      • 要求
      • 示例
      • 数据范围
  • 解法一
    • 思路和算法
    • 代码
    • 复杂度分析
  • 解法二
    • 预备知识
    • 思路和算法
    • 代码
    • 复杂度分析
  • 解法三
    • 预备知识
    • 思路和算法
    • 代码
    • 复杂度分析

题目

标题和出处

标题:翻转对

出处:493. 翻转对

难度

8 级

题目描述

要求

给定一个整数数组 nums \texttt{nums} nums,返回数组中的翻转对的数量。

如果下标对 (i, j) \texttt{(i, j)} (i, j) 满足 0 ≤ i < j < nums.length \texttt{0} \le \texttt{i} < \texttt{j} < \texttt{nums.length} 0i<j<nums.length nums[i] > 2 × nums[j] \texttt{nums[i]} > \texttt{2} \times \texttt{nums[j]} nums[i]>2×nums[j],则称为翻转对。

示例

示例 1:

输入: nums = [1,3,2,3,1] \texttt{nums = [1,3,2,3,1]} nums = [1,3,2,3,1]
输出: 2 \texttt{2} 2

示例 2:

输入: nums = [2,4,3,5,1] \texttt{nums = [2,4,3,5,1]} nums = [2,4,3,5,1]
输出: 3 \texttt{3} 3

数据范围

  • 1 ≤ nums.length ≤ 5 × 10 4 \texttt{1} \le \texttt{nums.length} \le \texttt{5} \times \texttt{10}^\texttt{4} 1nums.length5×104
  • -2 31 ≤ nums[i] ≤ 2 31 − 1 \texttt{-2}^\texttt{31} \le \texttt{nums[i]} \le \texttt{2}^\texttt{31} - \texttt{1} -231nums[i]2311

解法一

思路和算法

对于长度为 n n n 的数组,最朴素的计算翻转对的数量的方法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。由于数组 nums \textit{nums} nums 的长度最大为 5 × 1 0 4 5 \times 10^4 5×104,因此 O ( n 2 ) O(n^2) O(n2) 的时间复杂度过高,必须使用时间复杂度更低的方法。

由于翻转对和排序相关,因此可以使用排序的思想计算翻转对的数量。归并排序的过程中,每次将两个升序子数组合并,合并的过程中即可发现这两个升序子数组中的翻转对。因此可以在归并排序的过程中完成翻转对数量的计算。

考虑下标范围 [ low , high ] [\textit{low}, \textit{high}] [low,high] 的子数组。当 low ≥ high \textit{low} \ge \textit{high} lowhigh 时,子数组的长度是 0 0 0 1 1 1,此时子数组中不存在翻转对。当 low < high \textit{low} < \textit{high} low<high 时,子数组的长度大于等于 2 2 2,将子数组分成两个更短的子数组,两个更短的子数组排序之后合并,同时计算翻转对。具体做法如下。

  1. low \textit{low} low high \textit{high} high 的平均值 mid \textit{mid} mid,将下标范围 [ low , high ] [\textit{low}, \textit{high}] [low,high] 的子数组分成下标范围 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 和下标范围 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 的两个子数组。

  2. 对两个子数组分别排序并计算翻转对的数量,得到两个升序的子数组和两个子数组分别包含的翻转对数量。

  3. 计算两个升序子数组之间的翻转对数量。为了计算两个升序子数组之间的翻转对数量,需要定义两个指针 i i i j j j,初始时分别指向两个升序子数组的开始位置,即 i = low i = \textit{low} i=low j = mid + 1 j = \textit{mid} + 1 j=mid+1。当 i ≤ mid i \le \textit{mid} imid j ≤ high j \le \textit{high} jhigh 时,根据指针 i i i j j j 指向的元素计算翻转对数量。

    • 如果 nums [ i ] > 2 × nums [ j ] \textit{nums}[i] > 2 \times \textit{nums}[j] nums[i]>2×nums[j],则 ( i , j ) (i, j) (i,j) 是翻转对,由于对于任意 i ≤ k ≤ mid i \le k \le \textit{mid} ikmid 都有 nums [ i ] ≤ nums [ k ] \textit{nums}[i] \le \textit{nums}[k] nums[i]nums[k],因此 nums [ k ] > 2 × nums [ j ] \textit{nums}[k] > 2 \times \textit{nums}[j] nums[k]>2×nums[j],即 ( k , j ) (k, j) (k,j) 是翻转对,因此以 j j j 作为右侧下标的翻转对数量是 mid − i + 1 \textit{mid} - i + 1 midi+1。更新翻转对数量之后,将 j j j 指向的下标加 1 1 1

    • 如果 nums [ i ] ≤ 2 × nums [ j ] \textit{nums}[i] \le 2 \times \textit{nums}[j] nums[i]2×nums[j],则 ( i , j ) (i, j) (i,j) 不是翻转对,将 i i i 指向的下标加 1 1 1

  4. 计算两个升序子数组之间的翻转对数量之后,合并两个升序子数组。

当整个数组排序结束时即可得到整个数组的翻转对数量。

由于计算两个升序子数组之间的翻转对数量以及合并两个升序子数组都需要遍历两个升序子数组,因此计算翻转对数量并没有提升总时间复杂度,总时间复杂度和原始归并排序一样是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

归并排序可以使用自顶向下的方式递归实现,也可以使用自底向上的方式迭代实现。对于同一个数组,使用自顶向下和自底向上两种方式实现的中间过程可能有所区别,但是都能得到正确的结果。

代码

以下代码为归并排序的自顶向下实现。

class Solution {public int reversePairs(int[] nums) {return mergeSortAndCount(nums, 0, nums.length - 1);}public int mergeSortAndCount(int[] nums, int low, int high) {if (low >= high) {return 0;}int mid = low + (high - low) / 2;int count = mergeSortAndCount(nums, low, mid) + mergeSortAndCount(nums, mid + 1, high);int i = low, j = mid + 1;while (i <= mid && j <= high) {if ((long) nums[i] > 2 * (long) nums[j]) {count += mid - i + 1;j++;} else {i++;}}merge(nums, low, mid, high);return count;}public void merge(int[] nums, int low, int mid, int high) {int currLength = high - low + 1;int[] temp = new int[currLength];int i = low, j = mid + 1, k = 0;while (i <= mid && j <= high) {if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}while (i <= mid) {temp[k++] = nums[i++];}while (j <= high) {temp[k++] = nums[j++];}System.arraycopy(temp, 0, nums, low, currLength);}
}

以下代码为归并排序的自底向上实现。

class Solution {public int reversePairs(int[] nums) {int count = 0;int length = nums.length;for (int halfLength = 1, currLength = 2; halfLength < length; halfLength *= 2, currLength *= 2) {for (int low = 0; low < length - halfLength; low += currLength) {int mid = low + halfLength - 1;int high = Math.min(low + currLength - 1, length - 1);int i = low, j = mid + 1;while (i <= mid && j <= high) {if ((long) nums[i] > 2 * (long) nums[j]) {count += mid - i + 1;j++;} else {i++;}}merge(nums, low, mid, high);}}return count;}public void merge(int[] nums, int low, int mid, int high) {int currLength = high - low + 1;int[] temp = new int[currLength];int i = low, j = mid + 1, k = 0;while (i <= mid && j <= high) {if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}while (i <= mid) {temp[k++] = nums[i++];}while (j <= high) {temp[k++] = nums[j++];}System.arraycopy(temp, 0, nums, low, currLength);}
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。归并排序的时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。自顶向下实现时需要递归调用栈的空间是 O ( log ⁡ n ) O(\log n) O(logn),自底向上实现时可以省略递归调用栈的空间。无论是自顶向下实现还是自底向上实现,归并过程需要 O ( n ) O(n) O(n) 的辅助空间。

解法二

预备知识

该解法涉及到二分查找和线段树。

二分查找的作用是在有序序列中查找目标值,或者在特定范围中查找符合要求的最大值或最小值。

线段树是一种二叉搜索树,将一个区间划分成两个更短的区间。线段树中的每个叶结点都是长度为 1 1 1 的区间,称为单元区间。

线段树支持区间的快速查询和修改。对于长度为 n n n 的区间,使用线段树查询特定子区间的元素个数以及修改特定子区间内的元素个数的时间是 O ( log ⁡ n ) O(\log n) O(logn)

有时,为了降低线段树的空间复杂度,需要使用离散化。

思路和算法

对于长度为 n n n 的数组,其中不同元素的个数最多有 n n n 个。由于计算翻转对数量只需要考虑元素的相对大小,因此可以使用离散化将区间长度限制在 n n n 以内。离散化的方法是计算数组 nums \textit{nums} nums 中的每个元素的名次,计算名次的方法是:新建数组 sorted \textit{sorted} sorted 并将数组 nums \textit{nums} nums 中的所有元素复制到数组 sorted \textit{sorted} sorted 中,然后对数组 sorted \textit{sorted} sorted 排序排序之后, sorted [ i ] \textit{sorted}[i] sorted[i] 的名次为 i i i,如果 i > 0 i > 0 i>0 sorted [ i ] = sorted [ i − 1 ] \textit{sorted}[i] = \textit{sorted}[i - 1] sorted[i]=sorted[i1] sorted [ i ] \textit{sorted}[i] sorted[i] 的名次与 sorted [ i − 1 ] \textit{sorted}[i - 1] sorted[i1] 的名次相同。每个元素的名次都在范围 [ 0 , n − 1 ] [0, n - 1] [0,n1] 中,表示数组中的小于该元素的元素个数。

创建线段树,用于存储数组 nums \textit{nums} nums 中的每个元素的名次。从左到右遍历数组 nums \textit{nums} nums,对于每个元素 num \textit{num} num,其左边的元素已经遍历过,其左边的元素中的每个大于 2 × num 2 \times \textit{num} 2×num 的元素都与 num \textit{num} num 组成一个翻转对,因此得到 num \textit{num} num 的名次 rank \textit{rank} rank,并使用二分查找得到大于等于 2 × num + 1 2 \times \textit{num} + 1 2×num+1 的元素的最小名次 minRank \textit{minRank} minRank,计算线段树的子区间 [ minRank , n − 1 ] [\textit{minRank}, n - 1] [minRank,n1] 中的元素个数,该元素个数即为已经遍历过的元素中的大于 2 × num 2 \times \textit{num} 2×num 的元素个数,将翻转对数量增加该元素个数。更新翻转对数量之后,将 rank \textit{rank} rank 添加到线段树中,继续遍历后面的元素并更新翻转对数量。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的翻转对数量。

代码

class Solution {public int reversePairs(int[] nums) {int count = 0;Map<Integer, Integer> ranks = getRanks(nums);int length = nums.length;int[] sorted = new int[length];for (int i = 0; i < length; i++) {sorted[i] = nums[i];}Arrays.sort(sorted);SegmentTree st = new SegmentTree(length);for (int i = 0; i < length; i++) {int rank = ranks.get(nums[i]);int minRank = binarySearch(sorted, (long) 2 * nums[i] + 1);count += st.getCount(minRank, length - 1);st.add(rank);}return count;}public Map<Integer, Integer> getRanks(int[] nums) {int length = nums.length;int[] sorted = new int[length];System.arraycopy(nums, 0, sorted, 0, length);Arrays.sort(sorted);Map<Integer, Integer> ranks = new HashMap<Integer, Integer>();for (int i = 0; i < length; i++) {int num = sorted[i];if (i == 0 || num > sorted[i - 1]) {ranks.put(num, i);}}return ranks;}public int binarySearch(int[] nums, long target) {int low = 0, high = nums.length;while (low < high) {int mid = low + (high - low) / 2;if (nums[mid] >= target) {high = mid;} else {low = mid + 1;}}return low;}
}class SegmentTree {int length;int[] tree;public SegmentTree(int length) {this.length = length;this.tree = new int[length * 4];}public int getCount(int start, int end) {return getCount(start, end, 0, 0, length - 1);}public void add(int rank) {add(rank, 0, 0, length - 1);}private int getCount(int rangeStart, int rangeEnd, int index, int treeStart, int treeEnd) {if (rangeStart > rangeEnd) {return 0;}if (rangeStart == treeStart && rangeEnd == treeEnd) {return tree[index];}int mid = treeStart + (treeEnd - treeStart) / 2;if (rangeEnd <= mid) {return getCount(rangeStart, rangeEnd, index * 2 + 1, treeStart, mid);} else if (rangeStart > mid) {return getCount(rangeStart, rangeEnd, index * 2 + 2, mid + 1, treeEnd);} else {return getCount(rangeStart, mid, index * 2 + 1, treeStart, mid) + getCount(mid + 1, rangeEnd, index * 2 + 2, mid + 1, treeEnd);}}private void add(int rank, int index, int start, int end) {if (start == end) {tree[index]++;return;}int mid = start + (end - start) / 2;if (rank <= mid) {add(rank, index * 2 + 1, start, mid);} else {add(rank, index * 2 + 2, mid + 1, end);}tree[index] = tree[index * 2 + 1] + tree[index * 2 + 2];}
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。每个元素在线段树中的查询和更新操作都需要 O ( log ⁡ n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建用于排序的数组和线段树需要 O ( n ) O(n) O(n) 的空间。

解法三

预备知识

该解法涉及到二分查找和树状数组。

二分查找的作用是在有序序列中查找目标值,或者在特定范围中查找符合要求的最大值或最小值。

树状数组也称二叉索引树,由 Peter M. Fenwick 发明,因此又称 Fenwick 树。树状数组支持快速计算数组的前缀和与区间和,以及快速修改。对于长度为 n n n 的区间,使用树状数组查询特定子区间的区间和以及修改特定子区间内的元素值的时间是 O ( log ⁡ n ) O(\log n) O(logn)

有时,为了降低树状数组的空间复杂度,需要使用离散化。

思路和算法

计算翻转对数量也可以使用树状数组实现。

首先使用离散化将区间长度限制在 n n n 以内,然后创建树状数组,用于存储数组 nums \textit{nums} nums 中的每个元素的名次。从左到右遍历数组 nums \textit{nums} nums,对于每个元素 num \textit{num} num,得到 num \textit{num} num 的名次 rank \textit{rank} rank,并使用二分查找得到大于等于 2 × num + 1 2 \times \textit{num} + 1 2×num+1 的元素的最小名次 minRank \textit{minRank} minRank,计算树状数组的子区间 [ minRank , n − 1 ] [\textit{minRank}, n - 1] [minRank,n1] 中的元素个数,该元素个数即为已经遍历过的元素中的大于 2 × num 2 \times \textit{num} 2×num 的元素个数,将翻转对数量增加该元素个数。更新翻转对数量之后,将 rank \textit{rank} rank 添加到树状数组中,继续遍历后面的元素并更新翻转对数量。遍历结束之后,即可得到数组 nums \textit{nums} nums 中的翻转对数量。

代码

class Solution {public int reversePairs(int[] nums) {int count = 0;Map<Integer, Integer> ranks = getRanks(nums);int length = nums.length;int[] sorted = new int[length];for (int i = 0; i < length; i++) {sorted[i] = nums[i];}Arrays.sort(sorted);BinaryIndexedTree bit = new BinaryIndexedTree(length);for (int i = 0; i < length; i++) {int rank = ranks.get(nums[i]);int minRank = binarySearch(sorted, (long) 2 * nums[i] + 1);count += bit.getCount(minRank, length - 1);bit.add(rank);}return count;}public Map<Integer, Integer> getRanks(int[] nums) {int length = nums.length;int[] sorted = new int[length];System.arraycopy(nums, 0, sorted, 0, length);Arrays.sort(sorted);Map<Integer, Integer> ranks = new HashMap<Integer, Integer>();for (int i = 0; i < length; i++) {int num = sorted[i];if (i == 0 || num > sorted[i - 1]) {ranks.put(num, i);}}return ranks;}public int binarySearch(int[] nums, long target) {int low = 0, high = nums.length;while (low < high) {int mid = low + (high - low) / 2;if (nums[mid] >= target) {high = mid;} else {low = mid + 1;}}return low;}
}class BinaryIndexedTree {int length;int[] tree;public BinaryIndexedTree(int length) {this.length = length;this.tree = new int[length + 1];}public int getCount(int start, int end) {return getPrefixSum(end + 1) - getPrefixSum(start);}public void add(int index) {index++;while (index <= length) {tree[index]++;index += lowbit(index);}}private int getPrefixSum(int index) {int sum = 0;while (index > 0) {sum += tree[index];index -= lowbit(index);}return sum;}private static int lowbit(int x) {return x & (-x);}
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度。每个元素在树状数组中的查询和更新操作都需要 O ( log ⁡ n ) O(\log n) O(logn) 的时间,时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度。创建用于排序的数组和树状数组需要 O ( n ) O(n) O(n) 的空间。


http://www.ppmy.cn/news/1534310.html

相关文章

通信工程学习:什么是IP网际协议

IP&#xff1a;网际协议 IP网际协议&#xff08;Internet Protocol&#xff0c;简称IP&#xff09;是整个TCP/IP协议栈中的核心协议之一&#xff0c;它负责在网络中传送数据包&#xff0c;并提供寻址和路由功能。以下是对IP网际协议的详细解释&#xff1a; 一、对IP网际协议的…

把白底照片变蓝色用什么软件免费 批量更换证件照底色怎么弄

作为专业的修图师&#xff0c;有时候也会接手证件照修图和换底色工作&#xff0c;这种情况下&#xff0c;需要换底色的照片也许达到上百张。为了提高工作效率&#xff0c;一般需要批量快速修图&#xff0c;那么使用什么软件工具能够给各式不同的照片批量更换背景色呢&#xff1…

自学网络安全(黑客技术)90天学习计划

&#x1f91f; 基于入门网络安全/黑客打造的&#xff1a;&#x1f449;黑客&网络安全入门&进阶学习资源包 前言 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”…

绘制随k变化的等熵面积比公式

xmax 4; Ma 0.1:0.05:xmax; figure; hold on; xlim([0,xmax]); ylim([0,10]);% 预定义k值的向量 k_values 1.2:0.1:1.4;% 创建一个细胞数组来存储图例标签 legendStrings cell(1, length(k_values));% 绘制每条曲线并记录图例标签 lines []; for idx 1:length(k_values)k…

项目-坦克大战学习笔记-绘制坦克

在上一节我们绘制地图的时候调用了gudin类存储参数&#xff0c;现在我&#xff0c;需要绘制玩家就需要调用wanjia类中的参数 首先&#xff0c;我们在玩家类中声明一个构造函数&#xff0c;需要传参的有坐标和速度&#xff0c;在里面默认设置玩家坦克4个方向的图片对象以及默认…

使用 classification_report 评估 scikit-learn 中的分类模型

介绍 在机器学习领域&#xff0c;评估分类模型的性能至关重要。scikit-learn 是一个功能强大的 Python 机器学习工具&#xff0c;提供了多种模型评估工具。其中最有用的函数之一是 classification_report&#xff0c;它可以全面概述分类模型的关键指标。在这篇文章中&#xff…

pdf怎么编辑修改内容?详细介绍6款pdf编辑器功能

■ pdf怎么编辑修改内容&#xff1f; PDF&#xff08;Portable Document Format&#xff09;作为一种广泛使用的文件格式&#xff0c;具有特点包括兼容性强、易于传输、文件安全性高、跨平台性、可读性强、完整性、可搜索性、安全性、可压缩性。 PDF文件本身是不可以直接进行编…

WPF中的XAML详解

XAML介绍 XAML(Extensible Application Markup Language)(发音&#xff1a;zammel)可扩展应用程序标记语言。XAML是为构建应用程序用户界面而创建的一种新的描述性语言。XAML提供了一种便于扩展和定位的语法来定义和程序逻辑分离的用户界面&#xff0c;而这种实现方式和ASP.NE…