筆者在上一篇博客中談到了合併排序算法,其是分治思想的一種體現。在《算法導論》後的一道例題上,筆者看到了一道例題如下:
假設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
雖然隨機生成的數組不一定題設條件,但是不影響。兩種方式計算得到的逆序對數量是一致的。