浅谈排序算法之合并排序(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

虽然随机生成的数组不一定题设条件,但是不影响。两种方式计算得到的逆序对数量是一致的。


重要更新

今日笔者无意间在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] [0pr1] 的元素,每次贡献的逆序对的个数为 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分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值