算法设计之分治思想(求数组的逆序对)

Pamela ·
更新时间:2024-09-21
· 607 次阅读

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 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 nums[j]) { res++; } } } return res; } python def reversePairs(self,nums:list[int])->int: size=len(nums) if sizenums[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>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]mid: nums[k]=temp[j] j+=1 elif j>right: nums[k]=temp[i] i+=1 elif 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 treeSet = new TreeSet(); for (int i = 0; i < len; i++) { treeSet.add(nums[i]); } // 2、把排名存在哈希表里方便查询 Map 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 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 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 不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!
作者:云澈丿



逆序对 算法 数组

需要 登录 后方可回复, 如果你还没有账号请 注册新账号