在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
首先最容易想到的是暴力解法。
方法一:暴力解法(超时)
使用两层 for 循环枚举所有的数对,逐一判断是否构成逆序关系。
参考代码 1:
java
private static int reversePairs(int[] nums) {
// TODO Auto-generated method stub
int res = 0;
int len = nums.length;
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (nums[i] > nums[j]) {
res++;
}
}
}
return res;
}
python
def reversePairs(self,nums:list[int])->int:
size=len(nums)
if size<2:
return 0
res=0
for i in range(0,size-1):
for j in range(i+1,size):
if nums[i]>nums[j]:
res+=1
return res
方法二:分治思想(借助归并排序统计逆序数)
说明:理解这个算法需要对归并排序比较熟悉。掌握如果编写递归函数,每一次都一分为二拆分数组的子区间,然后在方法栈弹出的时候,一步一步合并两个有序数组,最后完成排序工作。
而计算逆序数就发生在排序的过程中,利用了「排序」以后数组的有序性。
利用「归并排序」计算逆序对,是非常经典的做法;
关键在于「合并两个有序数组」的步骤,利用数组的部分有序性,一下子计算出一个数之前或者之后元素的逆序的个数;
前面「分」的时候什么都不做,「合」的过程中计算「逆序对」的个数;
「排序」的工作是必要的,正是因为「排序」才能在下一轮利用顺序关系加快逆序数的计算,也能避免重复计算;
在代码实现上,只需要在「归并排序」代码的基础上,加上「逆序对」个数的计算,计算公式需要自己在草稿纸上推导。
思想是「分治算法」,所有的「逆序对」来源于 3 个部分:
左边区间的逆序对;
右边区间的逆序对;
横跨两个区间的逆序对。
下面提供两种写法:
1、在第 2 个子区间元素归并回去的时候,计算逆序对的个数(参考代码 2);
2、在第 1 个子区间元素归并回去的时候,计算逆序对的个数(参考代码 3)。
注意:两者不能同时计算,否则会计算重复。
参考代码 2:在第 2 个子区间元素归并回去的时候,计算逆序对的个数。
即在 j 指向的元素赋值回去的时候,给计数器加上 mid - i + 1。
java
private static int reversePairs2(int[] nums) {
int len = nums.length;
if (len < 2) {
return 0;
}
int[] copy = new int[len];
for (int i = 0; i < len; i++) {
copy[i] = nums[i];
}
int[] temp = new int[len];
return reversePair(copy, 0, len - 1, temp);
}
// nums[left...right]计算逆序对个数并且排序
private static int reversePair(int[] nums, int left, int right, int[] temp) {
// TODO Auto-generated method stub
if (left == right) {
return 0;
}
int mid = left + (right - left) / 2;
int leftPairs = reversePair(nums, left, mid, temp);
int rightPairs = reversePair(nums, mid + 1, right, temp);
// 如果整个数组已经有序,则无需合并,注意这里使用小于等于
if (nums[mid] <= nums[mid + 1]) {
return leftPairs + rightPairs;
}
int crossPairs = mergeAndCount(nums, left, mid, right, temp);
return leftPairs + rightPairs + crossPairs;
}
// nums[left..mid]有序,nums[mid+1..right]有序
private static int mergeAndCount(int[] nums, int left, int mid, int right, int[] temp) {
// TODO Auto-generated method stub
for (int i = left; i <= right; i++) {
temp[i] = nums[i];
}
int i = left;
int j = mid + 1;
int count = 0;
for (int k = left; k <= right; k++) {
// 有下标访问,的先判断是否越界
if (i == mid + 1) {
nums[k] = temp[j];
j++;
} else if (j == right + 1) {
nums[k] = temp[i];
i++;
} else if (temp[i] < temp[j]) {
// 注意。这里是<=,写成<就不对,可以想想原因
nums[k] = temp[i];
i++;
} else {
nums[k] = temp[j];
j++;
// 在j指向的元素归并回去的时候,计算逆序对的个数,只多了这一行代码
count += (mid - i + 1);
}
}
return count;
}
python
def reversePairs1(self,nums:list[int])->int:
size = len(nums)
if size<2:
return 0
# 用于归并的辅助数组
temp=[0 for _ in range(size)]
return self.count_reverse_pairs(nums,0,size-1,temp)
def count_reverse_pairs(self,nums,left,right,temp):
# 在数组nums的区间[left..right]统计逆序对
if left==right:
return 0
mid=(left+right)>>1
left_pairs=self.count_reverse_pairs(nums, left, mid, temp)
right_pairs=self.count_reverse_pairs(nums, mid+1, right, temp)
reverse_pairs=left_pairs+right_pairs
# 代码走到这里。[left,right]和[mid+1,right]已经完成了排序并且计算好逆序对
if nums[mid]<=nums[mid+1]:
# 此时不用计算机横跨两个区间的逆序对,直接返回reverse_pairs
return reverse_pairs
reverse_cross_pairs=self.merge_and_count(nums,left,mid,right,temp)
return reverse_cross_pairs+reverse_pairs
def merge_and_count(self,nums,left,mid,right,temp):
"""
[left, mid] 有序,[mid + 1, right] 有序
前:[2, 3, 5, 8],后:[4, 6, 7, 12]
只在后面数组元素出列的时候,数一数前面这个数组还剩下多少个数字,
由于"前"数组和"后"数组都有序,
此时"前"数组剩下的元素个数 mid - i + 1 就是与"后"数组元素出列的这个元素构成的逆序对个数
"""
for i in range(left,right+1):
temp[i]=nums[i]
i=left
j=mid+1
res=0
for k in range(left,right+1):
if i>mid:
nums[k]=temp[j]
j+=1
elif j>right:
nums[k]=temp[i]
i+=1
elif temp[i]<=temp[j]:
#此时数组元素出列,不统计逆序对
nums[k]=temp[i]
i+=1
else:
# assert temp[i]>temp[k]
# 此时后数组元素出列,统计逆序对,这里很快,一次可以统计一个区间的逆序对
nums[k]=temp[j]
j+=1
# 例:[7, 8, 9][4, 6, 9],4 与 7 以及 7 后面所有的数都构成逆序对
res+=(mid-i+1)
return res
参考代码 3:在第 1 个子区间元素归并回去的时候,计算逆序对的个数。
即在 i 指向的元素赋值回去的时候,给计数器加上 j - mid - 1。
public int reversePairs(int[] nums) {
int len = nums.length;
if (len < 2) {
return 0;
}
int[] copy = new int[len];
for (int i = 0; i < len; i++) {
copy[i] = nums[i];
}
int[] temp = new int[len];
return reversePairs(copy, 0, len - 1, temp);
}
private int reversePairs(int[] nums, int left, int right, int[] temp) {
if (left == right) {
return 0;
}
int mid = left + (right - left) / 2;
int leftPairs = reversePairs(nums, left, mid, temp);
int rightPairs = reversePairs(nums, mid + 1, right, temp);
if (nums[mid] <= nums[mid + 1]) {
return leftPairs + rightPairs;
}
int crossPairs = mergeAndCount(nums, left, mid, right, temp);
return leftPairs + rightPairs + crossPairs;
}
private int mergeAndCount(int[] nums, int left, int mid, int right, int[] temp) {
for (int i = left; i <= right; i++) {
temp[i] = nums[i];
}
int i = left;
int j = mid + 1;
int count = 0;
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
nums[k] = temp[j];
j++;
} else if (j == right + 1) {
nums[k] = temp[i];
i++;
count += (right - mid);
} else if (temp[i] <= temp[j]) {
nums[k] = temp[i];
i++;
count += (j - mid - 1);
} else {
nums[k] = temp[j];
j++;
}
}
return count;
}
python 同理,就不贴了
复杂度分析:
时间复杂度:O(NlogN),这里 NN是数组的长度。复杂度是归并排序的时间复杂度,直接看递归树的结点个数或者使用主定理分析,归并的回收每一步计算逆序对的个数是 O(1) 的;
空间复杂度:O(N)。
主定理分析:数据列均分为两部分,分别排序,之后以 O(N) 的复杂度进行合并。根据主定理(递归函数的时间复杂度分析工具):
至于为什么是这样,不在我能解释的范围内,大家可以在互联网上搜索「主定理」、「归并排序」获得相关的知识。
方法三:树状数组
这部分内容如果是准备普通公司的算法面试,不建议花时间掌握。写在这里是为了知识的完整性和知识点的科普。
用树状数组解决逆序数问题,也是一个经典的做法。
树状数组是一种实现了高效查询「前缀和」与「单点更新」操作的数据结构,在「力扣」第 315 题:计算右侧小于当前元素的个数 的 题解 里有介绍,这两题的解法可以说是一模一样。
具体的做法是:
先离散化,将所有的数组元素映射到 0、1、2、3… ,这是为了节约树状数组的空间;
从后向前扫描,边统计边往树状数组里面添加元素,这个过程是「动态的」,需要动手计算才能明白思想。
参考代码 4:
java
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class Solution {
public int reversePairs(int[] nums) {
int len = nums.length;
if (len < 2) {
return 0;
}
// 离散化:使得数字更紧凑,节约树状数组的空间
// 1、使用二分搜索树是为了去掉重复元素
Set<Integer> treeSet = new TreeSet<>();
for (int i = 0; i < len; i++) {
treeSet.add(nums[i]);
}
// 2、把排名存在哈希表里方便查询
Map<Integer, Integer> rankMap = new HashMap<>();
int rankIndex = 1;
for (Integer num : treeSet) {
rankMap.put(num, rankIndex);
rankIndex++;
}
int count = 0;
// 在树状数组内部完成前缀和的计算
// 规则是:从后向前,先给对应的排名 + 1,再查询前缀和
FenwickTree fenwickTree = new FenwickTree(rankMap.size());
for (int i = len - 1; i >= 0; i--) {
int rank = rankMap.get(nums[i]);
fenwickTree.update(rank, 1);
count += fenwickTree.query(rank - 1);
}
return count;
}
private class FenwickTree {
private int[] tree;
private int len;
public FenwickTree(int n) {
this.len = n;
tree = new int[n + 1];
}
public void update(int i, int delta) {
// 从下到上,最多到 size,可以等于 size
while (i <= this.len) {
tree[i] += delta;
i += lowbit(i);
}
}
// 区间查询:查询小于等于 tree[index] 的元素个数
// 查询的语义是「前缀和」
public int query(int i) {
// 从右到左查询
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i);
}
return sum;
}
public int lowbit(int x) {
return x & (-x);
}
}
}
python
from typing import List
class Solution:
def reversePairs(self, nums: List[int]) -> int:
class FenwickTree:
def __init__(self, n):
self.size = n
self.tree = [0 for _ in range(n + 1)]
def __lowbit(self, index):
return index & (-index)
# 单点更新:从下到上,最多到 len,可以取等
def update(self, index, delta):
while index <= self.size:
self.tree[index] += delta
index += self.__lowbit(index)
# 区间查询:从上到下,最少到 1,可以取等
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= self.__lowbit(index)
return res
# 特判
size = len(nums)
if size < 2:
return 0
# 原始数组去除重复以后从小到大排序,这一步叫做离散化
s = list(set(nums))
# 构建最小堆,因为从小到大一个一个拿出来,用堆比较合适
import heapq
heapq.heapify(s)
# 由数字查排名
rank_map = dict()
rank = 1
# 不重复数字的个数
rank_map_size = len(s)
for _ in range(rank_map_size):
num = heapq.heappop(s)
rank_map[num] = rank
rank += 1
res = 0
# 树状数组只要不重复数字个数这么多空间就够了
ft = FenwickTree(rank_map_size)
# 从后向前看,拿出一个数字来,就更新一下,然后向前查询比它小的个数
for i in range(size - 1, -1, -1):
rank = rank_map[nums[i]]
ft.update(rank, 1)
res += ft.query(rank - 1)
return res
不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!
转载:https://blog.csdn.net/weixin_45333934/article/details/105725803