笔者在上一篇博客中谈到了合并排序算法,其是分治思想的一种体现。在《算法导论》后的一道例题上,笔者看到了一道例题如下:
假设A[1…n]是一个由n个不同元素构成的数组。若i<j且A[i]>A[j],则对偶(i, j)称为数组A的一个逆序对。给出一个确定在n个元素的任何排列中逆序对数量的算法,最坏的情况是O(n * log(n))(提示,修改合并排序算法)。
首先,通过最简单方式,即双层for循环可以计算出给定数组A中的逆序对的数量,但是其时间复杂度为O(n^2),不满足要求,但是可以作为参考。
根据提示,排序算法从思想上而言,是分治算法的体现;形式上,采用二分策略,然后合并将结果作为子结果向上一层递归回去作为合并集之一。笔者之前没有系统学习过分治算法,在这里也只能按照类似的思路求解。
- 若要求得整个数组的逆序对的数量,分别求出数组前n-1个元素的逆序对数量并将结果相加即可。亦即将一个问题分解为n-1个子问题,对每个子问题分别进行求解,最后将计算结果合并下。
- 对于任意的m,满足1<=m<=n-1,可以将其后的n-m个数分为两组,采用二分的思路来查询其逆序对。
- 从形式上讲,第一步是将双层for循环中的最外层for循环采用递归(Recursion)来实现,当然也可以保留for循环。第二步则完全采用递归来实现。由于二分查找的时间复杂度为O(log(n)),而外层递归的时间复杂度为O(n),因此,这种方式在最坏的情况下的时间复杂度大致可以表示为O(n * log(n))。
代码如下:
import com.sun.istack.internal.NotNull;
import java.util.Arrays;
import java.util.Random;
/**
* A class to find how many inversions, where index i is less than index j
* and arr[i] > arr[j], exists in an array.
*
* @author Mr.K
*/
public class Inversion {
public static void main(String[] args) {
int N = 20;
int[] arr = new int[N];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(2 * N);
}
System.out.println(Arrays.toString(arr));
System.out.println("Using ordinary method: " + inversion(arr));
System.out.println("Using Divide-and-Conquer:" + getInversions(arr, 0));
}
/**
* Tries to find the number of inversions of specified array by using two
* <em>For-Loops</em>. The cost of time of this method is O(n^2) in any
* cases.
*
* @param arr specified array which may contain inversions
* @return number of inversions, which may exists in the specified array
*/
public static int inversion(@NotNull int[] arr) {
int ans = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
ans++;
}
}
}
return ans;
}
/**
* A override version of method above. This method accepts three more
* parameters, which are the index of current position to find the
* inversions, the range from start index to end index, respectively.
* <ul>
* <li>If <em>end - start</em> is greater than or equals 1, which
* means there exists more than one element in the range [start, end].
* Then divides the range into two part, one of which is in the range
* [start, mid] and another in the range [mid + 1, end] where
* <blockquote>
* mid = start + (end - start) / 2
* </blockquote></li>
* <li>If not, then returns 1 if and only if current number is greater
* than number in the range [start, end] where start equals end, which
* means there is only one element in the range; Otherwise, return 0.
* </li>
* </ul>
* This method uses {@code Recursion}, rather than <em>For-Loop</em> to
* iterate the process and also the think of {@code BinarySearch}. So
* the cost of time of this process is O(log(n)).
*
* @param arr specified array
* @param index current index to find the number of inversions
* @param start start index of the range
* @param end end index of the range
* @return numbers of inversions of current number, pointed by <em>index</em>
*/
public static int inversion(@NotNull int[] arr, @NotNull int index, @NotNull int start, @NotNull int end) {
if (end - start >= 1) {
int mid = start + (end - start) / 2;
return inversion(arr, index, start, mid) + inversion(arr, index, mid + 1, end);
} else {
return arr[index] > arr[start] ? 1 : 0;
}
}
/**
* Accepts an array and an index and returns the total number of inversions
* existing in the array by {@code Recursion} and invoking static method
* {@link org.vimist.pro.Algorithm.Sort.Inversion#inversion(int[], int, int, int)}.
* <ul>
* <li>If current index indicates that it's the last number to find
* inversions, then returns the number of inversions.</li>
* <li>Otherwise, return sum of number of inversions at current index and
* number of inversions at next cursor by invoking this method itself, which
* is so called {@code Recursion}.</li>
* </ul>
* This is an implementation of iteration without using <em>For-Loop</em>, explicitly.
* And the cost of time of this method is O(n) to iterate the whole number in the array.
* Thus, the total cost of time to find total number of inversions may be O(n * log(n))
* in some bad cases.
*
* @param arr specified array
* @param index index of to start accumulation of numbers of inversions
* @return total number of inversions in the specified array
*/
public static int getInversions(@NotNull int[] arr, @NotNull int index) {
return inversion(arr, index, index + 1, arr.length - 1)
+ (index == arr.length - 2 ? 0 : getInversions(arr, index + 1));
}
}
运行结果如下:
[28, 31, 9, 7, 9, 14, 14, 33, 2, 32, 11, 36, 32, 6, 39, 28, 37, 0, 18, 22]
Using ordinary method: 82
Using Divide-and-Conquer:82
虽然随机生成的数组不一定题设条件,但是不影响。两种方式计算得到的逆序对数量是一致的。
重要更新
今日笔者无意间在LC上刷题时碰到了逆序对这个问题,会想起笔者在去年就写过一篇关于逆序对的文章,故直接引用之,发现超时,说明笔者当时的解法是错误的。
关于逆序对这个问题,最直接的解法是双重 for 循环遍历,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。《算法导论》上有提到设计一种
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n)) 的算法,问题是紧随归并排序算法。因此很自然的想到了分治思想来处理。
之前笔者看似写出了运用分治思想来解决这个问题的题解,但是由于当时笔者的能力有限,无法深入理解分治思想以及熟练地运用递归,当时的写法实际上是双重 for 循环 的递归展开,惭愧ing
今日笔者有幸再次面临同样的问题,有网友指出这个问题实际上只需要对归并算法进行少量修改即可。
(并过程)设 L 和 R 分别表示左右两个子数组,pl 表示左数组指针,pr 表示右数组指针。若 pl 指向的元素小于 pr 指向的元素,那么 pl 指向的元素大于右数组 [ 0 ⋯ p r − 1 ] [0 \cdots pr-1] [0⋯pr−1] 的元素,每次贡献的逆序对的个数为 p r pr pr 个。
public int reversePairs(int[] nums) {
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int start, int end) {
if (start == end) return 0;
int mid = start + ((end - start) >> 1);
int cnt = mergeSort(nums, start, mid) + mergeSort(nums, mid + 1, end);
int[] temp = new int[end - start + 1];
int i = start, j = mid + 1, k = 0;
while (i <= mid && j <= end) {
if (nums[i] > nums[j]) temp[k++] = nums[j++];
else {
cnt += j - (mid + 1);
temp[k++] = nums[i++];
}
}
while (i <= mid) {
cnt += j - (mid + 1);
temp[k++] = nums[i++];
}
while (j <= end) temp[k++] = nums[j++];
System.arraycopy(temp, 0, nums, start, end - start + 1);
return cnt;
}
2020年4月24日15点29分