淺談排序算法之合併排序(4)續

筆者在上一篇博客中談到了合併排序算法,其是分治思想的一種體現。在《算法導論》後的一道例題上,筆者看到了一道例題如下:

假設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

雖然隨機生成的數組不一定題設條件,但是不影響。兩種方式計算得到的逆序對數量是一致的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章