Java 中数组中的倒序计数

2024 年 9 月 26 日 | 阅读 19 分钟

给定一个包含整数的数组。任务是找出给定数组中的总逆序对数。总逆序对数是一个数字,它表明给定数组距离排序有多近或多远。对于排序后的数组,逆序对数为 0。如果数组按相反的顺序排序,则逆序对数对于该数组来说是最大的。请看以下示例。

示例 1

输入

int inArr[] = {7, 4, 9, 1, 3, 5, 0, 6, 2, 8}

输出 24

解释:给定的数组有以下逆序对

(7, 4), (7, 1), (7, 3), (7, 5), (7, 0), (7, 6), (7, 2), (4, 1), (4, 3), (4, 0), (4, 2), (9, 1), (9, 3), (9, 5), (9, 0), (9, 6), (9, 2), (9, 8), (1, 0), (3, 0), (3, 2), (5, 0), (5, 2), (6, 2)。因此,总逆序对数为 24。

示例 2

输入

int inArr[] = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}

输出 45

解释:所有元素均按严格递减顺序排列。因此,这次我们将得到最大的逆序对数。逆序对是

(9, 8), (9, 7), (9, 6), (9, 5), (9, 4), (9, 3), (9, 2), (9, 1), (9, 0), (8, 7), (8, 6), (8, 5), (8, 4), (8, 3), (8, 2), (8, 1), (8, 0), (7, 6), (7, 5), (7, 4), (7, 3), (7, 2), (7, 1), (7, 0), (6, 5), (6, 4), (6, 3), (6, 2), (6, 1), (6, 0), (5, 4), (5, 3), (5, 2), (5, 1), (5, 0), (4, 3), (4, 2), (4, 1), (4, 0), (3, 2), (3, 1), (3, 0), (2, 1), (2, 0), (1, 0),它们的计数为 (9 * (9 + 1)) / 2 = 45。

示例 3

输入

int inArr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

输出 0

解释:元素按递增顺序排列。因此,右侧没有更小的元素,使得逆序对数为 0。

示例 4

输入

int inArr[] = {4, 4, 4, 4, 4, 4, 4, 4, 4, 4}

输出 0

解释:输入数组的所有元素都是常数,即相同的值。因此,输入数组的右侧不存在更小的元素。因此,逆序对数为 0。

方法:使用嵌套循环

在此方法中,我们将嵌套两个 for 循环。外层循环选择输入数组中的元素(将其视为当前元素)。内层循环从当前元素的下一个元素开始,一直遍历到末尾。在内层循环中,将内层循环变量指向的元素与当前元素进行比较。如果它更小,则将逆序对数增加 1;否则,不增加逆序对数。

文件名:CountInversion.java

输出

For the input array: 
7 4 9 1 3 5 0 6 2 8 
The total inversion count of the input array is: 24

For the input array: 
9 8 7 6 5 4 3 2 1 0 
The total inversion count of the input array is: 45

For the input array: 
0 1 2 3 4 5 6 7 8 9 
The total inversion count of the input array is: 0

For the input array: 
4 4 4 4 4 4 4 4 4 4 
The total inversion count of the input array is: 0

复杂度分析:由于程序使用了嵌套循环,因此程序的时间复杂度为 O(N2)。程序未使用任何额外空间。因此,程序空间复杂度为常数,即 O(1)。

方法:增强型归并排序

该方法是使用增强型归并排序。在此方法中,我们将首先将输入数组分成两个子数组,然后计算第一个和第二个子数组中的逆序对。之后,我们将计算合并子数组时的逆序对。总数就是答案。我们将递归地将输入数组分成子数组。找到解决方案的步骤如下。

步骤 1:在每个递归步骤中,将输入数组分成两个大致相等或相等的两半,直到达到基本情况。当给定的一半只有一个元素时,达到递归的基本情况。

步骤 2:创建一个 merge() 方法,该方法在合并输入数组的两个半部分时计算总逆序对数。创建两个索引 j 和 k,j 是第一个半部分的索引,k 是第二个半部分的专用索引。如果 inArr[j] 大于 inArr[k],则存在 (mid - j) 个逆序对。这是因为右子数组和左子数组已排序,因此在左子数组中,所有剩余的元素 inArr[j + 1], inArr[j + 2] … inArr[mid]) 都将大于 inArr[k]。

步骤 3:另外,创建一个递归方法来将输入数组分成两半,并通过添加第一个半部分的逆序对数、第二个半部分的逆序对数以及合并两个半部分的逆序对数来计算答案。

步骤 4:显示总计数。

实施

观察上述步骤的实现。

文件名:CountInversion1.java

输出

For the input array: 
7 4 9 1 3 5 0 6 2 8 
The total inversions count is: 24

For the input array: 
9 8 7 6 5 4 3 2 1 0 
The total inversions count is: 45

For the input array: 
0 1 2 3 4 5 6 7 8 9 
The total inversions count is: 0

For the input array: 
4 4 4 4 4 4 4 4 4 4 
The total inversions count is: 0

复杂度分析:归并排序需要 O(n x log(n)) 时间。因此,程序的时间复杂度为 O(n x log(n))。程序空间复杂度为 O(n),因为使用辅助数组来存储左子数组和右子数组,其中 n 是输入数组中的元素总数。

方法:使用自平衡树

算法

步骤 1:创建一个 AVL 树。此外,在树的每个节点中添加一个属性,说明每个节点将包含其子树大小的信息。

步骤 2:从最后一个到第一个遍历输入数组。

步骤 3:对于每个元素,将其放入自平衡树(AVL 树)中。

步骤 4:大于当前元素的节点数可以通过检查右子树的大小(右子节点的数量)轻松计算得出。因此,可以肯定的是,当前节点右子树中的元素数量小于当前元素(因为我们是从后向前遍历输入数组)。此外,右子节点的值大于当前元素(AVL 树的属性)。因此,满足逆序条件。因此,将计数增加到当前插入节点的右子树大小。

步骤 5:显示计数。

文件名:CountInversion2.java

输出

For the input array: 
7 4 9 1 3 5 0 6 2 8 
The total inversions count is: 24

For the input array: 
9 8 7 6 5 4 3 2 1 0 
The total inversions count is: 45

For the input array: 
0 1 2 3 4 5 6 7 8 9 
The total inversions count is: 0

For the input array: 
4 4 4 4 4 4 4 4 4 4 
The total inversions count is: 0

复杂度分析:程序的时空复杂度与上一个程序相同。

方法:使用二叉索引树

使用二叉索引树也可以达到期望的结果。观察以下算法。

算法

使用二叉索引树也可以达到期望的结果。观察以下算法。

步骤 1:创建一个二叉索引树来查找小于某个数的元素的数量。还创建一个变量 res = 0。

步骤 2:从最后一个到第一个遍历输入数组。

步骤 3:对于每个索引,找出小于当前元素的元素总数,并将结果与之相加。

步骤 4:创建一个 retrieveSum() 方法来获取更少元素的数量。

步骤 5:通过执行更新操作将当前元素添加到数组 BIT[] 中,该操作将当前元素计数从 0 更新为 1,从而更新二叉索引树中的当前元素祖先。

文件名:CountInversion3.java

输出

For the input array: 
7 4 9 1 3 5 0 6 2 8 
The total inversions count is: 24

For the input array: 
9 8 7 6 5 4 3 2 1 0 
The total inversions count is: 45

For the input array: 
0 1 2 3 4 5 6 7 8 9 
The total inversions count is: 0

For the input array: 
4 4 4 4 4 4 4 4 4 4 
The total inversions count is: 0

复杂度分析:程序的时空复杂度与上一个程序相同。