归并排序和逆序数

280 阅读5分钟

「这是我参与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. 计算右侧小于当前元素的个数

题解里面用的降序排序, 如此一来,放入左边元素时, 剩余未排序的右边区间元素都比它小。

这题具体要注意的就是 ,要保留原始索引。