「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
归并排序的基本思路就是,把一个大问题分解为最小问题单元, 然后每个微元都能轻松解决,
最重的的一步就是,把两个已解决的成果成一个大成果。
归并排序是外部排序,需要额外的空间,也就是空间复杂度略高。但是实际场景,往往就是要借助外部空间。比如,我们的电脑,假如只有两个G的内存,要如何复制一个100G的文件,当然是借助外部存储,一次复制一部分,然后把这些部分连起来。
好了,进入主题,逆序数和归并有什么关系?主要是最近刷题的时候,碰到逆序数相关的题目,基本解法都是归并的思想,但是在一些细节上又有不同。
逆序数
逆序数就是, 一个数列,期望是升序排列, 违反这个规则的两个数构成一个逆序对。
例如 12398727636 中9后面的一堆数字都比9小,那么后面任意一个数都是9的逆序数。那么9在这个数列中一共就有7个逆序数。 逆序数有什么用呢? 咱也不知道,依稀记得在求行列式的时候好像用到过。
那么归并排序,如何能算出逆序数。
归并排序
归并排序的重点,就在合并有序数组上。统计逆序数,也是在这个时候进行的。 不过要想证明,归并排序可以统计一个数列的逆序数,我们需要一点数学归纳法。
我们在用归并排序,求一个数列的逆序数时,进行的是如下三步:
1求出左边区间的逆序数
2求出右边区间的逆序数
3求出右边相对左边的逆序数。 注意,归并排序的过程中,区间在合并前面,它们的相对位置是没变的, 所以,我们只需要关心, 左边的元素和右边的元素能组成多少逆序数。 比如说, 左边第一个元素是0 , 我们就只需要关心右边有多少元素是小于0的。
最后加起来就是整个数列的逆序数。
解决了合并两个序列的逆序数, 那么以此类推,合并多个数列的逆序数。
至于左右两边的区间的逆序数怎么求? 多个元素的逆序数,我不知道,但是单个元素的逆序数,我知道啊,那就是0. 知道单个,就能知道两个的,以此类推。
重点来了
在合并两个数列时,怎么统计左边和右边组成的逆序数呢?
-
如果只是要统计全部的逆序数的个数,我们采用升序排序。
如此一来,当我们把右边区间的元素,放入排序后的数组时,这个元素是当前剩余元素的最小值,也就是说这个元素,比左边剩余未排序的元素都要小。如此一来,右边小于左边,构成逆序,这个元素就和左边区间未排序的每个元素都构成一对逆序。 主要逆序要严格不等,因此照这种解法, 合并时,两边元素相等时,先放左边的元素, 这样就能保证放入右边元素时, 剩余左边元素一定是严格大于这个元素。
完整代码见题解 剑指 Offer 51. 数组中的逆序对
这里就只放关键的合并代码了。l,r是数组区间索引, count是统计的逆序数。 mid 是中位,说到mid这里就要注意一个小点。
mid = (l + r)>> 1,这样写mid永远都小于r,取不到r。因此,把mid放在左边区间。
let mid = (l + r)>> 1 ;
merge( nums, l, mid, inds)
merge( nums, mid+1, r, inds)
let i1 = l, i2 = mid+1;
while( i1 < mid +1 || i2 <= r){
/* 这次 等于号得放这 */
if(i1 < mid+1 && nums[i1]<= nums[i2] || i2> r ){
temp.push(nums[i1++])
}else {
/* 此时这个元素绝对大于 已经放入的左边的元素 */
temp.push( inds[i2++])
count += ( mid - i1 +1 )
}
}
- 如果要统计每个元素的逆序数,也就是每个元素后面有多少小于它的元素。
我们按照上面的方法不行, 因为上面统计的是每个元素前面有多少大于当前元素的。 要统计这个元素后面有多少元素小于它,合并的时候,这个元素必然是左边的。
当我们升序排序,合并时放入左边元素,此时剩余右边元素全部都大于它,而已排序的右边元素才是小于它的。 所以当前元素的逆序数就是右边已经排序的元素。 因为要严格小于,那么相等时应先放入左边元素。
下面代码也是经过提交验证的。
值得注意的是, 我们要求每个元素的逆序数,但是排序之后,元素的位置就发生了变化。 因此,下面直接对索引排序, 存储逆序数时就可以方便的拿到索引了。
/* 试试升序排序 */
while( i1 < mid +1 || i2 <= r){
/* 这次 等于号得放这 */
if(i1 < mid+1 && nums[inds[i1]]<= nums[inds[i2]] || i2> r ){
/* 此时 右边已经排序的元素全部都小于当前元素 是它的逆序数, 剩余未排序的右边元素都大于等于它,不可能是逆序数*/
res[inds[i1]]+= (i2- mid -1)
temp.push(inds[i1++])
}else {
temp.push( inds[i2++])
}
}
完整代码见题解 315. 计算右侧小于当前元素的个数
题解里面用的降序排序, 如此一来,放入左边元素时, 剩余未排序的右边区间元素都比它小。
这题具体要注意的就是 ,要保留原始索引。