分治
- 快速排序
-
- 1.颜⾊分类(medium)
- 2.快速排序(medium)
- 3.数组中的第K个最⼤元素(medium)
- 归并排序
-
- 1.排序数组(medium)
- 2.数组中的逆序对(hard)
- 3.计算右侧⼩于当前元素的个数(hard)
- 4.翻转对(hard)
快速排序
1.颜⾊分类(medium)
题⽬链接:75. 颜⾊分类 题⽬描述: 给定⼀个包含红⾊、⽩⾊和蓝⾊、共 n 个元素的数组 nums ,原地对它们进⾏排序,使得相同颜⾊的元素相邻,并按照红⾊、⽩⾊、蓝⾊顺序排列。 我们使⽤整数 0、 1 和 2 分别表⽰红⾊、⽩⾊和蓝⾊。 必须在不使⽤库的 sort 函数的情况下解决这个问题。 ⽰例 1: 输⼊:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
解法(快排思想 – 三指针法使数组分三块): 算法思路: 类⽐数组分两块的算法思想,这⾥是将数组分成三块,那么我们可以再添加⼀个指针,实现数组分三块。 设数组⼤⼩为 n ,定义三个指针 left, cur, right : ◦ left :⽤来标记 0 序列的末尾,因此初始化为 -1 ; ◦ cur :⽤来扫描数组,初始化为 0 ; ◦ right :⽤来标记 2 序列的起始位置,因此初始化为 n 。 在 cur 往后扫描的过程中,保证: ◦ [0, left] 内的元素都是 0 ; ◦ [left + 1, cur – 1] 内的元素都是 1 ; ◦ [cur, right – 1] 内的元素是待定元素; ◦ [right, n] 内的元素都是 2 。 算法流程: a. 初始化 cur = 0,left = -1, right = numsSize ; b. 当 cur < right 的时候(因为 right 表⽰的是 2 序列的左边界,因此当 cur 碰到right 的时候,说明已经将所有数据扫描完毕了),⼀直进⾏下⾯循环: 根据 nums[cur] 的值,可以分为下⾯三种情况:
- i. nums[cur] = = 0 ;说明此时这个位置的元素需要在 left + 1 的位置上,因此交换 left + 1 与 cur 位置的元素,并且让 left++ (指向 0 序列的右边界),cur++ (为什么可以 ++ 呢,是因为 left + 1 位置要么是 0 ,要么是 cur ,交换完毕之后,这个位置的值已经符合我们的要求,因此 cur++ );
- ii. nums[cur] == 1 ;说明这个位置应该在 left 和 cur 之间,此时⽆需交换,直接让 cur++ ,判断下⼀个元素即可;
- iii. nums[cur] = = 2 ;说明这个位置的元素应该在 right – 1 的位置,因此交换right – 1 与 cur 位置的元素,并且让 right– (指向 2 序列的左边界),cur 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)
c. 当循环结束之后: [0, left] 表⽰ 0 序列; [left + 1, right – 1] 表⽰ 1 序列; [right, numsSize – 1] 表⽰ 2 序列。
算法代码:
class Solution {
public void swap(int[] nums, int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
public void sortColors(int[] nums) {
int left = –1, right = nums.length, i = 0;
while(i < right){
if(nums[i] == 0) swap(nums, ++left, i++);
else if(nums[i] == 1) i++;
else swap(nums, —right, i);
}
}
}
2.快速排序(medium)
题⽬链接:912. 排序数组 由于⼒扣的测试⽤例在不断加强,所以这⾥的数组划分三块的思想搭配随机选择基准元素的⽅法是⽐较优秀的。 顺便说个有趣的事:官⽅题解的快排代码提交后会超时~~~ 2022/12/07 题⽬描述:
给你⼀个整数数组 nums,请你将该数组升序排列。 ⽰例 1: 输⼊:nums = [5,2,3,1] 输出:[1,2,3,5] ⽰例 2: 输⼊:nums = [5,1,1,2,0,0] 输出:[0,0,1,1,2,5]
解法(数组分三块思想 + 随机选择基准元素的快速排序):
算法思路: 我们在数据结构阶段学习的快速排序的思想可以知道,快排最核⼼的⼀步就是 Partition (分割数据):将数据按照⼀个标准,分成左右两部分。 如果我们使⽤荷兰国旗问题的思想,将数组划分为 左 中 右 三部分:左边是⽐基准元素⼩的数据,中间是与基准元素相同的数据,右边是⽐基准元素⼤的数据。然后再去递归的排序左边部分和右边部分即可(可以舍去⼤量的中间部分)。 在处理数据量有很多重复的情况下,效率会⼤⼤提升。 算法流程: 随机选择基准算法流程: 函数设计:int randomKey(vector& nums, int left, int right) a. 在主函数那⾥种⼀颗随机数种⼦; b. 在随机选择基准函数这⾥⽣成⼀个随机数; c. 由于我们要随机产⽣⼀个基准,因此可以将随机数转换成随机下标:让随机数 % 上区间⼤⼩,然后加上区间的左边界即可。 快速排序算法主要流程: a. 定义递归出⼝; b. 利⽤随机选择基准函数⽣成⼀个基准元素; c. 利⽤荷兰国旗思想将数组划分成三个区域; d. 递归处理左边区域和右边区域。 算法代码:
class Solution{
public int[] sortArray(int[] nums) {
qsort(nums, 0, nums.length – 1);
return nums;
}
public void qsort(int[] nums, int l, int r){
if(l >= r) return;
// 数组分三块
int key = nums[new Random().nextInt(r – l + 1) + l];
int left = l – 1, right = r + 1, i = l;
while(i < right){
if(nums[i] < key) swap(nums, ++left, i++);
else if(nums[i] == key) i++;
else swap(nums, —right, i);
}
// [l, left] [left + 1, right – 1] [rigth, r]
qsort(nums, l, left);
qsort(nums, right, r);
}
public void swap(int[] nums, int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
3.数组中的第K个最⼤元素(medium)
题⽬链接:215. 数组中的第K个最⼤元素 题⽬描述:
给定整数数组 nums 和整数 k,请返回数组中第 k 个最⼤的元素。 请注意,你需要找的是数组排序后的第 k 个最⼤的元素,⽽不是第 k 个不同的元素。 你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。 ⽰例 1: 输⼊: [3,2,1,5,6,4], k = 2 输出: 5 ⽰例 2: 输⼊: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4 提⽰: 1 <= k <= nums.length <= 10^5 -10^4 <= nums[i] <= 10^4
解法(快速选择算法):
算法思路: 在快排中,当我们把数组「分成三块」之后: [l, left] [left + 1, right – 1] [right, r] ,我们可以通过计算每⼀个区间内元素的「个数」,进⽽推断出我们要找的元素是在「哪⼀个区间」⾥⾯。 那么我们可以直接去「相应的区间」去寻找最终结果就好了。 算法代码:
class Solution{
public int findKthLargest(int[] nums, int k) {
return qsort(nums, 0, nums.length – 1, k);
}
public int qsort(int[] nums, int l, int r, int k) {
if(l == r) {
return nums[l];
}
// 1. 按照随机选择的基准元素,将数组分三块
int key = nums[new Random().nextInt(r – l + 1) + l];
int left = l – 1, right = r + 1, i = l;
while(i < right) {
if(nums[i] < key) swap(nums, ++left, i++);
else if(nums[i] == key) i++;
else swap(nums, —right, i);
}
// 2. 分情况讨论
int c = r – right + 1, b = right – left – 1;
if(c >= k) return qsort(nums, right, r, k);
else if(c + b >= k) return key;
else return qsort(nums, l, left, k – b – c);
}
public void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
归并排序
1.排序数组(medium)
题⽬链接:912. 排序数组 题⽬描述:
给你⼀个整数数组 nums,请你将该数组升序排列。 ⽰例 1: 输⼊:nums = [5,2,3,1] 输出:[1,2,3,5] ⽰例 2: 输⼊:nums = [5,1,1,2,0,0] 输出:[0,0,1,1,2,5]
解法(归并排序): 算法思路: 归并排序的流程充分的体现了「分⽽治之」的思想,⼤体过程分为两步: ◦ 分:将数组⼀分为⼆为两部分,⼀直分解到数组的⻓度为 1 ,使整个数组的排序过程被分为「左半部分排序」 + 「右半部分排序」; ◦ 治:将两个较短的「有序数组合并成⼀个⻓的有序数组」,⼀直合并到最初的⻓度。 算法代码:
class Solution{
int[] tmp;
public int[] sortArray(int[] nums) {
tmp = new int[nums.length];
mergeSort(nums, 0, nums.length – 1);
return nums;
}
public void mergeSort(int[] nums, int left, int right){
if(left >= right) return;
// 1. 根据中间点划分区间
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 先把左右区间排个序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 3. 合并两个有序数组
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right)
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
// 处理没有遍历完的数组
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
// 4. 还原
for(int j = left; j <= right; j++)
nums[j] = tmp[j – left];
}
}
2.数组中的逆序对(hard)
题⽬链接:剑指 Offer 51. 数组中的逆序对 题⽬描述:
在数组中的两个数字,如果前⾯⼀个数字⼤于后⾯的数字,则这两个数字组成⼀个逆序对。输⼊⼀个数组,求出这个数组中的逆序对的总数。 ⽰例 1: 输⼊: [7,5,6,4] 输出: 5··
解法(利⽤归并排序的过程 — 分治): 算法思路: ⽤归并排序求逆序数是很经典的⽅法,主要就是在归并排序的合并过程中统计出逆序对的数量,也就是在合并两个有序序列的过程中,能够快速求出逆序对的数量。 我们将这个问题分解成⼏个⼩问题,逐⼀破解这道题。 注意:默认都是升序,如果掌握升序的话,降序的归并过程也是可以解决问题的。
-
先解决第⼀个问题,为什么可以利⽤归并排序? 如果我们将数组从中间划分成两个部分,那么我们可以将逆序对产⽣的⽅式划分成三组: • 逆序对中两个元素:全部从左数组中选择 • 逆序对中两个元素:全部从右数组中选择 • 逆序对中两个元素:⼀个选左数组另⼀个选右数组 根据排列组合的分类相加原理,三种种情况下产⽣的逆序对的总和,正好等于总的逆序对数量。⽽这个思路正好匹配归并排序的过程: • 先排序左数组; • 再排序右数组; • 左数组和右数组合⼆为⼀。 因此,我们可以利⽤归并排序的过程,先求出左半数组中逆序对的数量,再求出右半数组中逆序对的数量,最后求出⼀个选择左边,另⼀个选择右边情况下逆序对的数量,三者相加即可。
-
解决第⼆个问题,为什么要这么做? 在归并排序合并的过程中,我们得到的是两个有序的数组。我们是可以利⽤数组的有序性,快速统计出逆序对的数量,⽽不是将所有情况都枚举出来。
-
最核⼼的问题,如何在合并两个有序数组的过程中,统计出逆序对的数量? 合并两个有序序列时求逆序对的⽅法有两种: 1.快速统计出某个数前⾯有多少个数⽐它⼤; 2.快速统计出某个数后⾯有多少个数⽐它⼩;
⽅法⼀:快速统计出某个数前⾯有多少个数⽐它⼤ 通过⼀个⽰例来演⽰⽅法⼀: 假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [ ],通过合并两个有序数组的过程,来求得逆序对的数量:规定如下定义来叙述过程: cur1 遍历 left 数组,cur2 遍历 right 数组,ret 记录逆序对的数量 第⼀轮循环: left[cur1] > right[cur2],由于两个数组都是升序的,那么我们可以断定,此刻 left 数组中[cur1, 2] 区间内的 3 个元素均可与 right[cur2] 的元素构成逆序对,因此可以累加逆序对的数量 ret+= 3,并且将 right[cur2] 加⼊到辅助数组中,cur2++ 遍历下⼀个元素。第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 3 cur1 = 0 cur2 = 1 第⼆轮循环: left[cur1] == right[cur2],因为 right[cur2] 可能与 left 数组中往后的元素构成逆序对,因此我们需要将 left[cur1] 加⼊到辅助数组中去,此时没有产⽣逆序对,不更新 ret。第⼆轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 3 cur1 = 1 cur2 = 1 第三轮循环: left[cur1] > right[cur2],与第⼀轮循环相同,此刻 left 数组中[cur1, 2] 区间内的 2 个元素均可与 right[cur2] 的元素构成逆序对,更新 ret 的值为 ret += 2,并且将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。 第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 5 cur1 = 1 cur2 = 2 第四轮循环: left[cur1] < right[cur2],由于两个数组都是升序的,因此我们可以确定 left[cur1] ⽐ right 数组中的所有元素都要⼩。left[cur1] 这个元素是不可能与 right 数组中的元素构成逆序对。因此,⼤胆的将 left[cur1] 这个元素加⼊到辅助数组中去,不更细 ret 的值。第四轮循环结束后:left = [x, x, 9] right = [x, x, 8] help = [4, 5, 5, 7] ret = 5 cur1 = 2 cur2 = 2 第五轮循环: left[cur1] > right[cur2],与第⼀、第三轮循环相同。此时 left 数组内的 1 个元素能与right[cur2] 构成逆序对,更新 ret 的值,并且将 right[cur2] 加⼊到辅助数组中去。第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 6 cur1 = 2 cur2 = 2 处理剩余元素: • 如果是左边出现剩余,说明左边剩下的所有元素都是⽐右边元素⼤的,但是它们都是已经被计算过的(我们以右边的元素为基准的),因此不会产⽣逆序对,仅需归并排序即可。 • 如果是右边出现剩余,说明右边剩下的元素都是⽐左边⼤的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可。整个过程只需将两个数组遍历⼀遍即可,时间复杂度为 O(N)。 由上述过程我们可以得出⽅法⼀统计逆序对的关键点: 在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,我们可以通过计算左数组中剩余元素的⻓度,就可快速求出右数组当前元素前⾯有多少个数⽐它⼤,对⽐解法⼀中⼀个⼀个枚举逆序对效率快了许多。
⽅法⼆:快速统计出某个数后⾯有多少个数⽐它⼩ 依旧通过⼀个⽰例来演⽰⽅法⼆: 假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [ ],通过合并两个有序数组的过程,来求得逆序对的数量:规定如下定义来叙述过程: cur1 遍历 left 数组,cur2 遍历 right 数组,ret 记录逆序对的数量 第⼀轮循环: left[cur1] > right[cur2],先不要着急统计,因为我们要找的是当前元素后⾯有多少⽐它⼩的,这⾥虽然出现了⼀个,但是 right 数组中依旧还可能有其余⽐它⼩的。因此此时仅将 right[cur2] 加⼊到辅助数组中去,并且将 cur2++。第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 0 cur1 = 0 cur2 = 1 第⼆轮循环: left[cur1] = = right[cur2],由于两个数组都是升序,这个时候对于元素 left[cur1] 来说,我们已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是⽐它⼩的。因此此时可以统计逆序对的数量 ret += cur2 – 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。第⼆轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 1 cur1 = 1 cur2 = 1 第三轮循环: left[cur1] > right[cur2],与第⼀轮循环相同,直接将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 1 cur1 = 1 cur2 = 2 第四轮循环: left[cur1] < right[cur2],由于两个数组都是升序的,这个时候对于元素 left[cur1] 来说,我们依旧已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是⽐它⼩的。因此此时可以统计逆序对的数量 ret += cur2 – 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。第四轮循环结束后:left = [9] right = [8] help = [4, 5, 5, 7] ret = 3 cur1 = 2 cur2 = 2 第五轮循环: left[cur1] > right[cur2],与第⼀、第三轮循环相同。直接将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 3 cur1 = 2 cur2 = 2 处理剩余元素: • 如果是左边出现剩余,说明左边剩下的所有元素都是⽐右边元素⼤的,但是相⽐较于⽅法⼀,逆序对的数量是没有统计过的。因此,我们需要统计 ret 的值: ◦ 设左边数组剩余元素的个数为 leave ◦ ret += leave * (cur2 – 0) 对于本题来说,处理剩余元素的时候, left 数组剩余 1 个元素,cur2 – 0 = 3,因此 ret 需要类加上 3,结果为 6。与⽅法⼀求得的结果相同。 • 如果是右边出现剩余,说明右边剩下的元素都是⽐左边⼤的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可。 整个过程只需将两个数组遍历⼀遍即可,时间复杂度依旧为 O(N)。 由上述过程我们可以得出⽅法⼆统计逆序对的关键点: 在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,我们可以通过计算右数组已经遍历过的元素的⻓度,快速求出左数组当前元素后⾯有多少个数⽐它⼤。但是需要注意的是,在处理剩余元素的时候,⽅法⼆还需要统计逆序对的数量。
升序的版本算法代码:
class Solution{
int[] tmp;
public int reversePairs(int[] nums) {
int n = nums.length;
tmp = new int[n];
return mergeSort(nums, 0, n – 1);
}
public int mergeSort(int[] nums, int left, int right){
if(left >= right) return 0;
int ret = 0;
// 1. 选择⼀个中间点,将数组划分成两部分
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 左半部分的个数 + 排序 + 右半部分的个数 + 排序
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. ⼀左⼀右的个数
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) { // 升序版本
if(nums[cur1] <= nums[cur2]){
tmp[i++] = nums[cur1++];
}
else{
ret += mid – cur1 + 1;
tmp[i++] = nums[cur2++];
}
}
// 4. 处理⼀下排序
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j – left];
return ret;
}
}
降序的版本算法代码:
class Solution{
int[] tmp;
public int reversePairs(int[] nums) {
int n = nums.length;
tmp = new int[n];
return mergeSort(nums, 0, n – 1);
}
public int mergeSort(int[] nums, int left, int right){
if(left >= right) return 0;
int ret = 0;
// 1. 选择⼀个中间点,将数组划分成两部分
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 左半部分的个数 + 排序 + 右半部分的个数 + 排序
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. ⼀左⼀右的个数
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) { // 降序的版本
if(nums[cur1] <= nums[cur2]){
tmp[i++] = nums[cur2++];
}else{
ret += right – cur2 + 1;
tmp[i++] = nums[cur1++];
}
}
// 4. 处理⼀下排序
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j – left];
return ret;
}
}
3.计算右侧⼩于当前元素的个数(hard)
题⽬链接:315. 计算右侧⼩于当前元素的个数 题⽬描述:
给你⼀个整数数组 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 个更⼩的元素
解法(归并排序): 算法思路: 这⼀道题的解法与 求数组中的逆序对 的解法是类似的,但是这⼀道题要求的不是求总的个数,⽽是要返回⼀个数组,记录每⼀个元素的右边有多少个元素⽐⾃⼰⼩。但是在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要⼀个辅助数组,来将数组元素和对应的下标绑定在⼀起归并,也就是再归并元素的时候,顺势将下标也转移到对应的位置上。 由于我们要快速统计出某⼀个元素后⾯有多少个⽐它⼩的,因此我们可以利⽤求逆序对的第⼆种⽅法。 算法流程:
- 创建两个全局的数组: vector index:记录下标 vector ret:记录结果 index ⽤来与原数组中对应位置的元素绑定,ret ⽤来记录每个位置统计出来的逆序对的个数。
- countSmaller() 主函数: a. 计算 nums 数组的⼤⼩为 n; b. 初始化定义的两个全局的数组; i. 为两个数组开辟⼤⼩为 n 的空间 ii. index 初始化为数组下标; iii. ret 初始化为 0 c. 调⽤ mergeSort() 函数,并且返回 ret 结果数组。
- void mergeSort( vector& nums, int left, int right ) 函数: 函数设计:通过修改全局的数组 ret, 统计出每⼀个位置对应的逆序对的数量,并且排序; ⽆需返回值,因为直接对全局变量修改,当函数结束的时候,全局变量已经被修改成最后的结果。
- mergeSort() 函数流程: a. 定义递归出⼝:left >= right 时,直接返回; b. 划分区间:根据中点 mid,将区间划分为 [left, mid] 和 [mid + 1, right]; c. 统计左右两个区间逆序对的数量: (1)i. 统计左边区间 [left, mid] 中每个元素对应的逆序对的数量到 ret 数组中,并排序; (2)ii. 统计右边区间 [mid + 1, right] 中每个元素对应的逆序对的数量到 ret 数组中,并排序。 d. 合并左右两个有序区间,并且统计出逆序对的数量: (1)i. 创建两个⼤⼩为 right – left + 1 ⼤⼩的辅助数组: • numsTmp: 排序⽤的辅助数组; • indexTmp:处理下标⽤的辅助数组。 (2)ii. 初始化遍历数组的指针:cur1 = left(遍历左半部分数组)cur2 = mid + 1(遍历右半边数组)dest = 0(遍历辅助数组)curRet(记录合并时产⽣的逆序对的数量); (3)iii. 循环合并区间: • 当 nums[cur1] <= nums[cur2] 时: ◦ 说明此时 [mid + 1, cur2) 之间的元素都是⼩于 nums[cur1] 的,需要累加到 ret 数组的 indext[cur1] 位置上(因为 index 存储的是元素对应位置在原数组中的下标) ◦ 归并排序:不仅要将数据放在对应的位置上,也要将数据对应的坐标也放在对应的位置上,使数据与原始的下标绑定在⼀起移动。 • 当 nums[cur1] > nums[cur2] 时,⽆需统计,直接归并,注意 index 也要跟着归并。 (4)iv. 处理归并排序中剩余的元素; • 当左边有剩余的时候,还需要统计逆序对的数量; • 当右边还有剩余的时候,⽆需统计,直接归并。 (5)v. 将辅助数组的内容替换到原数组中去;
Java 算法代码:
class Solution{
int[] ret;
int[] index; // 标记 nums 中当前元素的原始下标
int[] tmpIndex;
int[] tmpNums;
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
ret = new int[n];
index = new int[n];
tmpIndex = new int[n];
tmpNums = new int[n];
// 初始化 index 数组
for(int i = 0; i < n; i++)
index[i] = i;
mergeSort(nums, 0, n – 1);
List<Integer> l = new ArrayList<Integer>();
for(int x : ret)
l.add(x);
return l;
}
public void mergeSort(int[] nums, int left, int right){
if(left >= right) return;
// 1. 根据中间元素划分区间
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 处理左右两个区间
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 3. 处理⼀左⼀右的情况
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) { // 降序排序
if(nums[cur1] <= nums[cur2]){
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}else{
ret[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++];
}
for(int j = left; j <= right; j++){
nums[j] = tmpNums[j – left];
index[j] = tmpIndex[j – left];
}
}
}
4.翻转对(hard)
题⽬链接:493. 翻转对 题⽬描述:
给定⼀个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作⼀个重要翻转对。你需要 返回给定数组中的重要翻转对的数量。 ⽰例 1: 输⼊: [1,3,2,3,1] 输出: 2
题⽬解析: 翻转对和逆序对的定义⼤同⼩异,逆序对是前⾯的数要⼤于后⾯的数。⽽翻转对是前⾯的⼀个数要 ⼤于后⾯某个数的两倍。因此,我们依旧可以⽤归并排序的思想来解决这个问题。
解法(归并排序): 算法思路: ⼤思路与求逆序对的思路⼀样,就是利⽤归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:左半区间翻转对的数量,右半区间翻转对的数量,⼀左⼀右选择时翻转对的数量。重点就是在合并区间过程中,如何计算出翻转对的数量。 与上个问题不同的是,上⼀道题我们可以⼀边合并⼀遍计算,但是这道题要求的是左边元素⼤于右边元素的两倍,如果我们直接合并的话,是⽆法快速计算出翻转对的数量的。 例如 left = [4, 5, 6] right = [3, 4, 5] 时,如果是归并排序的话,我们需要计算 left 数组中有多少个能与 3 组成翻转对。但是我们要遍历到最后⼀个元素 6 才能确定,时间复杂度较⾼。 因此我们需要在归并排序之前完成翻转对的统计。 下⾯依旧以⼀个⽰例来模仿两个有序序列如何快速求出翻转对的过程: 假定已经有两个已经有序的序列 left = [4, 5, 6] right = [1, 2, 3] 。 ⽤两个指针 cur1 cur2 遍历两个数组。 ◦ 对于任意给定的 left[cur1] ⽽⾔,我们不断地向右移动 cur2,直到 left[cur1] <= 2 *right[cur2]。此时对于 right 数组⽽⾔,cur2 之前的元素全部都可以与 left[cur1] 构成翻转对。 ◦ 随后,我们再将 cur1 向右移动⼀个单位,此时 cur2 指针并不需要回退(因为 left 数组是升序的)依旧往右移动直到 left[cur1] <= 2 * right[cur2]。不断重复这样的过程,就能够求出所有左右端点分别位于两个⼦数组的翻转对数⽬。
由于两个指针最后都是不回退的的扫描到数组的结尾,因此两个有序序列求出翻转对的时间复杂度是 O(N)。 综上所述,我们可以利⽤归并排序的过程,将求⼀个数组的翻转对转换成求 左数组的翻转对数量 +右数组中翻转对的数量 + 左右数组合并时翻转对的数量。
算法代码: 降序版本
class Solution{
int[] tmp;
public int reversePairs(int[] nums) {
int n = nums.length;
tmp = new int[n];
return mergeSort(nums, 0, n – 1);
}
public int mergeSort(int[] nums, int left, int right){
if(left >= right) return 0;
int ret = 0;
// 1. 根据中间元素,将区间分成两部分
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 求出左右两个区间的翻转对
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. 处理⼀左⼀右 – 先计算翻转对
int cur1 = left, cur2 = mid + 1, i = left;
// 降序版本
while(cur1 <= mid){
while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0) cur2++;
if(cur2 > right)
break;
ret += right – cur2 + 1;
cur1++;
}
// 4. 合并两个有序数组
cur1 = left; cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right)
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j];
return ret;
}
}
升序版本
class Solution{
int[] tmp;
public int reversePairs(int[] nums) {
int n = nums.length;
tmp = new int[n];
return mergeSort(nums, 0, n – 1);
}
public int mergeSort(int[] nums, int left, int right){
if(left >= right) return 0;
int ret = 0;
// 1. 根据中间元素,将区间分成两部分
int mid = (left + right) / 2;
// [left, mid] [mid + 1, right]
// 2. 求出左右两个区间的翻转对
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. 处理⼀左⼀右 – 先计算翻转对
int cur1 = left, cur2 = mid + 1, i = left;
// 升序版本
while(cur2 <= right){
while(cur1 <= mid && nums[cur2] >= nums[cur1] / 2.0) cur1++;
if(cur1 > mid)
break;
ret += mid – cur1 + 1;
cur2++;
}
// 4. 合并两个有序数组 – 升序
cur1 = left; cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right)
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j];
return ret;
}
}
评论前必须登录!
注册