数组中的逆序对(逐字稿,旧)

327 阅读11分钟

大家好,这里是「力扣」视频题解。今天要和大家分享的是「剑指 Offer」第 51 题:数组中的逆序数。

这道题要求我们计算一个数组中「逆序对」的个数。

什么是「逆序对」呢?

从一个数组里抽出的两个数字,如果前面的数值严格大于后面的数值,就称为「构成一个逆序对」。

我们从示例数组里抽出两个不同下标的数字, 5 和 4,5 排在 4 的前面,5 大于 4 ,因此它们构成了一个逆序对。

「逆序对」的个数反映了一个数组的有序程度,这里有两个很特殊的例子,它们分别是「顺序数组」和「逆序数组」:

1、对于「顺序数组」来说,任意抽出的两个数字都不存在逆序关系。

例子:[1, 2, 3, 4, 5]

2、而对于「倒序排列的数组」来说,任意的抽出的两个数都构成了逆序关系。

例子:[5, 4, 3, 2, 1]

(这里隐含了优化算法的思想:)这个数组的逆序的个数就是 5 后面所有元素的个数(= 4) + 4 后面所有元素的个数(= 3) + 3 后面所有元素的个数(= 2) + 2 后面所有元素的个数(= 1) = 10。

一个非常容易想到的方法是,使用一个双重循环,枚举所有的两个不同下标的元素,只要发现一个逆序关系,就给计数器加 1。

这个算法我们称之为暴力解法或者是依据定义的解法,这个算法的时间复杂度是 O(N^2)。空间复杂度是 O(1)

这个算法的缺点是显而易见的,我们在每一次的比较过程中,没有记住一些信息。

而优化的解法是基于以下两个事实:

1、暴力解法的缺点:在比较的过程中没有记录下元素的大小关系,不管是构成顺序对还是逆序对,都没有有被记录下来,也就是:之前比较的结果不能为以后的比较提供有用的信息;

2、其实隐含在我们刚刚介绍完全逆序的数组的例子里,正是因为我们知道了这个数组里的元素关系,我们就可以一下子计算出与 5 构成逆序的元素个数,与 4 构成构成逆序的元素个数而不用一个一个地去比较。

这两点事实告诉我们,在计算逆序的过程中,掌握元素的大小关系是加快计算的思路。而掌握元素的大小关系其实就是在计算逆序的过程中,对已经看到的数字进行一次排序。

而在高级排序算法(归并排序、快速排序)里,能够看到非常明显的阶段排序结果的算法就是归并排序。

有兴趣的朋友不妨在这里暂停视频,想一想如何利用归并排序给数组排序的过程,计算出逆序对的个数。

使用归并排序计算逆序数,关键在于「合并两个有序数组」的过程,我们在归并的过程中,每一次考察两个紧挨着的有序子区间,考虑将它们合并成为一个更长的有序子区间。

(要合并两个紧挨着的有序子区间,我们先把这两部分的元素拷贝到一个新的区间,然后再通过比较逐个赋值回去)

比如在这个例子里,

[2, 3, 5, 7, 1, 4, 6, 8] 

我们是先看 2 和 1 的大小,1 比 2 小,1 就应该先归并回去,由于两个子区间都是有序的,我们就知道了,1 比它之前的所有元素都小,也就是 1 与它之前的所有元素都构成了逆序对,我们就可以一下子给计数器加上 4;

1 出列以后,考察 2 和 4。

2 比 4 小,我们将 2 归并回去,在 2 归并回去的同时,我们可以观察到 2 点:第 1 : 2 与之前的 1 构成了逆序对,不过我们刚刚在 1 归并回去的时候已经计算进去了;第 2 :2 与 1 之后的所有元素不构成逆序对,因此在 2 归并回去这一步我们什么都不用做;

接下来比较 3 和 4 的大小,3 比 4 小,3 归并回去,依然是什么都不做;

接下来比较 5 和 4 的大小,4 比 5 小,4 归并回去,与此同时我们就知道了,4 与第 1 个数组里还没有归并回去的所有元素 5 和 7 构成了逆序对,把 4 归并回去以后,我们就可以给计数器加上 2

(就按照这样的顺序,其实我们只需要在第 2 个有序数组归并回去的过程中,计算归并回去的那个元素与第 1 个有序数组的构成逆序的个数,也就是,依次给计数器加上相应的第 1 个有序数组还没有归并回去的元素的个数)。

接下来比较 5 和 6 的大小,5 比 6 小,5 归并回去,依然是什么都不做;

接下来比较 7 和 6 的大小,6 比 7 小,这个时候第 1 个有序数组里只有 1 个 7,我们把 6 归并回去以后,给计数器加 1。

接下来比较 7 和 8 的大小,7 归并回去,什么都不做,最后我们把 8 归并回去,由于第 1 个有序数组都归并完了,也就是 8 在这个区间里不与任何元素构成逆序关系,我们可以认为是计数器加 0。

这样我们就完成了一个区间里,逆序对的计算。依然要和大家强调的前提是:可以这样计算逆序对个数的前提,两个子区间分别有序。

而使得两个子区间有序,是基于上一轮计算逆序对的结果,正是由于我们在计算逆序数的同时,给数组排了序,这样结果就可以应用在下一轮计算逆序对的过程中,直至整个数组有序,我们就计算出了整个数组逆序对的个数。

这个过程可以用下面这张图来表示:

34fa3058ea84693164d4d5c6b28cf2e3.png

我们对有序数组的拆分,直到区间里只剩下一个元素的时候。1 个元素的数组,肯定是有序数组。然后我们再依次归并两个有序数组,直到整个数组有序。

这样的过程我们通过编写递归函数实现,这是因为我们在计算的逆序数的过程中符合:先拆分的问题后计算,后拆分得到的问题先计算的规律。

我们借助了递归函数的方法栈,在上一个方法出栈以后,归并两个有序数组,就这样一直执行下去。完成了逆序数的计算。

下面我们来看一下代码:

Java 代码:


(看算导总结分治)


而优化的解法的思路是:在比较的过程中,记录下元素的大小关系,而记录元素的大小关系的过程中,就是给数组

(下面这段话说明逆序对的定义)

最后一个元素,它没有后面的元素。

方法一:暴力解法(依据定义)

我们可以根据逆序对的定义,得到第 1 个非常朴素的解法:暴力解法。

我们直接来看代码。

Java 代码:

public class Solution {

    public int reversePairs(int[] nums) {

        int len = nums.length;
        int res = 0;
        for (int i = 0; i < len - 1; i++) {
            for (int j = i + 1; j < len; j++) {
                if (nums[i] > nums[j]) {
                    res++;
                }
            }
        }
        return res;
    }
}

这个代码的时间复杂度是 O(N^2),直接提交给「力扣」测评是不能得到通过的。

空间复杂度是 O(1)

在这里插入图片描述

优化的思路:

暴力解法的缺点其实是显而易见的。我们在使用定义求解逆序数的过程,没有使用到已经比较的结果。

1、优化的解法来自于一个经典的算法问题「合并两个有序数组」。我们简单和大家回忆一下。 2、和合并的过程中,边合并,边计算逆序数,由于合并的过程中,使得数组有序,在更大规模的合并过程中,就可以减少一些比较的次数,从而降低时间复杂度;

3、而合并两个有序数组的过程,是一直出现在「归并排序」这个过程中的。

在合并的过程中,计算逆序数是方便的。

  • 暴力解法的缺点:每一次在比较两个数值的大小关系是,都没有利用到之前比较的结果,例如 [4, 5, 1, 2, 3]45 都在 [1, 2, 3] 的前面,我们在得出 4123 分别构成了 3 个逆序对。由于 54 还大,5 也在 [1, 2, 3] 的前面,那么它也一定与 123 构成逆序对;
  • 因此我们希望优化的解法能够记住一些信息,并且记住的信息能够体现元素之间的大小关系;
  • 我们再看一下刚刚给出的这个例子,这个数组是分段有序的,前一段是有序的,后一段也是有序的,在计算逆序数的时候,能够带来一些方便。
  • 为此,一个可行的思路是:我们一边排序,一边计算逆序数。这个思路看起来比较神奇,但是这个想法的来源还是来自于逆序数的定义:
    • 顺序数组的逆序对为 0,我们在对一个数组排序的过程,就相当于记住了一些信息;

这里很特别的一点是:

已经计算了逆序的区间,我们就让它有序,这样下一轮再计算逆序的时候,就不会重复。

在排序算法里,有一种算法,在排序的过程中是能体现出区间分段有序性的,那就是「归并排序」。说道这里,感兴趣的朋友不妨暂停一下视频,想一想如何在归并的过程中,一边排序,一边记录下逆序对的个数。对归并排序还不熟悉的朋友,也可以顺便复习一下归并排序。

下面我们就具体来看一下这个思路最关键的一个过程,那就是合并两个有序数组:

[2, 3, 5, 7, 1, 4, 6, 8]

我们把这个数组逻辑上分成两个区间,分界线就在 7 和 1 的中间。

一开始我们看 2 和 1 ,由于 1 比 2 小,1 应该先出列,1 与它前面的 4 个元素分别构成逆序对,因此计数器一下子可以加上 4;

1 出列以后,指向 1 的指针向后移动一位来到 4 这个位置;

由于 2 比 4 小,这一次我们将 2 出列,与 2 构成逆序的只有 1 ,1 出列的时候这个逆序关系我们已经计算过了,因此将 2 出列以后,我们什么都不做,然后将原来指向 2 的指针向后移动一位;

现在两个指针指向的是 3 和 4,3 比 4 小, 3 出列,同样是,与 3 构成逆序的 1 在 1 出列的时候就已经计算过了,因此计数器不需要增加。 原来指向 3 的指针指向 5。

这个时候 4 比 5 小,4 出列,4 的前面比 4 还大的元素就是第 1 个数组里还没有出列的元素的个数,我们看到有 5 和 7 两个元素,此时计数器 + 2,然后 指针向右移动一位。

接下来比较 5 和 6 ,5 比 6 小,5 出列,5 的后面比 5 小的元素 1、4 构成的逆序个数在它们出列的时候已经被计算过了。同样的,什么都不做,5 的指针向后移动 1 位。

接下来比较 7 和 6,6 的前面比 6 大的元素依然是第 1 个数组里还没有出列的元素的个数,我们把 6 放在合适的位置以后,计数器 + 2,指针向后移动一位。

现在比较 7 和 8 ,7 出列,什么都不做,第 1 个数组里的元素我们都看完了,然后 8 出列,第 2 个数组里的元素我们也都看完了。

完成了两个有序数组的合并以后,数组有序,原来它们构成的逆序的对数我们也已经计算完了。

在这里要和大家强调的一点是:

这道题其实是从「归并排序」引申出来的问题。