与归并排序相关的一些问题

与归并排序相关的一些问题

作者:Grey

原文地址:

博客园:与归并排序相关的一些问题

CSDN:与归并排序相关的一些问题

归并排序的递归解法

插入,选择,冒泡排序时间复杂度是 O ( N 2 ) O(N^2) O(N2),归并排序可以做到时间复杂度 O ( N ∗ l o g N ) O(N*logN) O(NlogN)

归并排序的整体思路是利用递归,先让左边排好序,再让右边排好序,然后通过merge操作让整体有序。

merge操作类似合并两个及以上有序链表问题中提到的算法,

两个有序数组,分别用两个指针指向数组的第一个元素,哪个值小就拷贝哪个值到最终的数组中,并移动对应的指针,如果其中的一个数组已经遍历完毕,则剩下那个数组的其余元素依次拷贝到最终数组的剩余位置中即可。

merge过程需要辅助数组,所以归并排序的额外空间复杂度为 O ( N ) O(N) O(N)

完整代码和注释见:

public class Code_MergeSort {

    // 递归方法实现
    public static void mergeSort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }
    // 递归过程,让l……r变有序
    public static void process(int[] arr, int l, int r) {
        if (l == r) {
            return;
        }
        // 求中点
        int mid = l + ((r - l) >> 1);
        // 左边部分有序
        process(arr, l, mid);
        // 右边部分有序
        process(arr, mid + 1, r);
        // 整体变有序
        merge(arr, l, mid, r);
    }
    // arr[l……mid]已经有序
    // arr[mid+1……r]也已经有序
    // 将arr[l……r]整体变有序
    public static void merge(int[] arr, int l, int mid, int r) {
        // 辅助数组
        int[] help = new int[r - l + 1];
        int ls = l;
        int rs = mid + 1;
        int i = 0;
        while (ls <= mid && rs <= r) {
            // 谁小拷贝谁到辅助数组中。
            if (arr[ls] < arr[rs]) {
                help[i++] = arr[ls++];
            } else {
                help[i++] = arr[rs++];
            }
        }
        // 左边和右边剩余部分直接拷贝到辅助数组中
        while (ls <= mid) {
            help[i++] = arr[ls++];
        }
        while (rs <= r) {
            help[i++] = arr[rs++];
        }
        i = 0;
        for (int n : help) {
            arr[l + (i++)] = n;
        }
    }
}

复杂度估计

这个递归过程时间复杂度可以利用 master 公式来计算。

master 公式适用于子问题规模等量的情况下, 用来计算递归函数的复杂度

T ( N ) = a ∗ T ( N / b ) + O ( N d ) T(N) = a * T(N/b) + O(N^d) T(N)=aT(N/b)+O(Nd)

其中的 a、b、d 都是常数

T ( N ) T(N) T(N)为父过程的数据规模

T ( N / b ) T(N/b) T(N/b)为子过程的数据规模

a a a为子过程的调用次数

O ( N d ) O(N ^ d) O(Nd)为除了递归过程之外其他调用的时间复杂度

如果 l o g b a < d logb^a < d logba<d,则递归函数复杂度为 O ( N d ) O(N^d) O(Nd)

如果 l o g b a > d logb^a > d logba>d,则递归函数复杂度为 O ( N ( l o g b a ) ) O(N^(logb^a)) O(N(logba))

如果 l o g b a = = d logb^a == d logba==d,则递归函数复杂度为 O ( N d ∗ l o g N ) O(N^d * logN) O(NdlogN)

注:该公式只适用子过程的调用都是数据规模相同的情况,如果一个递归过程有多个子过程数据规模不一样,那么它不能用该公式进行时间复杂度的计算。

所以,根据 master 公式,针对归并排序,就是看上述递归方法process的时间复杂度,
子过程调用次数是两次(process中调用了两次process方法),所以 a = 2 a = 2 a=2process中分了两部分(左半部分,右半部分)来做递归,所以 b = 2 b=2 b=2,最后剩余部分,即merge方法,就是 O ( N ) O(N) O(N)的复杂度,所以 d = 1 d=1 d=1,所以归并排序满足

T ( N ) = 2 ∗ T ( N / 2 ) + O ( N 1 ) T(N) = 2 * T(N/2) + O(N^1) T(N)=2T(N/2)+O(N1)

l o g 2 2 = = 1 log2^2 == 1 log22==1

满足:

如果 l o g b a = = d logb^a == d logba==d,则递归函数复杂度为 O ( N d ∗ l o g N ) O(N^d * logN) O(NdlogN)

所以归并排序的算法时间复杂度为 ( N ∗ l o g N ) (N*logN) (NlogN)

归并排序的迭代版本实现

因为任何递归函数都可以用非递归函数来实现,所以,归并排序有对应的迭代方法,思路如下

  1. 设置一个步长,从 1 开始, 1 , 2 , 4 , 8 , 16... 2 n 1,2,4,8,16...2^n 124816...2n 方式递增

  2. 每次处理对应步长的数组区间范围内的排序。

  3. 步长超过或者等于数组长度,则整个数组排序完成。

比如 [ 1 , 3 , 4 , 2 , 5 , 6 , 4 , 6 , 8 ] [1,3,4,2,5,6,4,6,8] [1,3,4,2,5,6,4,6,8]

先设置步长为 1,数组分成如下区间

[ 0 … 1 ] , [ 2 … 3 ] , [ 4 … 5 ] , [ 6 … 7 ] , [ 8 … 8 ] [0…1],[2…3],[4…5],[6…7],[8…8] [01],[23],[45],[67],[88]

注:最后一组不够分,则单独作为一组处理。

将如上区间内部排好序,得到的数组为

[ 1 , 3 , 2 , 4 , 5 , 6 , 4 , 6 , 8 ] [1,3,2,4,5,6,4,6,8] [1,3,2,4,5,6,4,6,8]

然后设置步长为 2,数组分成如下区间

[ 0 … 3 ] , [ 4 … 7 ] , [ 8 … 8 ] [0…3],[4…7],[8…8] [03],[47],[88]

然后将上述区间内部先排好序,得到数组为

[ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 6 , 8 ] [1,2,3,4,4,5,6,6,8] [1,2,3,4,4,5,6,6,8]

然后设置步长为 4,数组分成如下区间

[ 0 … 7 ] , [ 8 … 8 ] [0…7],[8…8] [07],[88]

然后将上述区间内部先排好序,得到数组为

[ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 6 , 8 ] [1,2,3,4,4,5,6,6,8] [1,2,3,4,4,5,6,6,8]

最后设置步长为 8,数组只有一个区间,直接排序,得到最后结果

[ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 6 , 8 ] [1,2,3,4,4,5,6,6,8] [1,2,3,4,4,5,6,6,8]

完整代码见


public class Code_MergeSort {

    // 归并排序的迭代版
    public static void mergeSort2(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int len = arr.length;
        // 步长,1,2,4,8…….
        int step = 1;
        while (step < len) {
            // 左组的第一个位置
            int lStart = 0;
            while (lStart < len) {
                if (lStart + step >= len) {
                    // 没有右组
                    break;
                }
                int mid = lStart + step - 1;
                // rEnd不能越界
                int rEnd = mid + Math.min(step, len - mid - 1);
                // 右组中第一个位置
                // 中点位置
                merge(arr, lStart, mid, rEnd);
                lStart = rEnd + 1;
            }
            // 防止溢出
            if (step > (len / 2)) {
                break;
            }
            step <<= 1;
        }
    }
    // arr[l……mid]已经有序
    // arr[mid+1……r]也已经有序
    // 将arr[l……r]整体变有序
    public static void merge(int[] arr, int l, int mid, int r) {
        int[] help = new int[r - l + 1];
        int lStart = l;
        int index = 0;
        int rStart = mid + 1;
        while (lStart <= mid && rStart <= r) {
            help[index++] = arr[lStart] > arr[rStart] ? arr[rStart++] : arr[lStart++];
        }
        while (lStart <= mid) {
            help[index++] = arr[lStart++];
        }
        while (rStart <= r) {
            help[index++] = arr[rStart++];
        }
        System.arraycopy(help, 0, arr, l, help.length);
    }
}

合并有序数组

题目描述见LeetCode 88. Merge Sorted Array

本题思路就是归并排序的merge过程,本题有一些不太一样的地方是,由于排序后的数据全部要存在 num1 中,所以 num1在题目中已经说明,大小是 m + n m + n m+n, num1 的 前 m 个数是 num1 初始的有效范围,其余 n 个位置由 0 填充,所以我们在merge的过程中,使用逆向遍历两个数组方式,即先填充 num1 的 第 ( m + n − 1 ) (m + n - 1) (m+n1)号位置,谁大就第一个进这个位置,代码如下

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1;
        int j = n - 1;
        int index = m + n - 1;
        while (i >= 0 && j >= 0) {
            nums1[index--] = nums1[i] < nums2[j] ? nums2[j--] : nums1[i--];
        }
        while (j >= 0) {
            // 只需要继续判断 nums2 了
            // 因为 nums1 自然拍好了
            nums1[index--] = nums2[j--];
        }
    }
}

注:本题在 LintCode 中也有,见LintCode 6 · Merge Two Sorted Arrays

在 LintCode 中,对本题有个扩展要求:

如果一个数组很大,另一个数组很小,你将如何优化算法?

对于扩展要求,我们可以用如下方式来优化

直接查小数组中的元素在大数组中的位置(可以用二分),然后依次填入具体位置

完整代码见

public class Solution {

    public static int[] mergeSortedArray(int[] A, int[] B) {
        int m = A.length;
        int n = B.length;
        int[] bigger = m >= n ? A : B;
        int[] smaller = bigger == A ? B : A;
        int[] helper = new int[m + n];
        int from = 0;
        int to;
        int index = 0;
        for (int i = 0; i < smaller.length; i++) {
            int position = position(smaller[i], bigger, i);
            helper[position] = smaller[i];
            to = position - 1;
            while (from <= to) {
                helper[from++] = bigger[index++];
            }
            from = position + 1;
        }
        while (from < (m + n)) {
            helper[from++] = bigger[index++];
        }
        return helper;
    }

    // value在bigger的位置是多少
    public static int position(int value, int[] bigger, int offset) {
        int smallerThanMe = 0;
        int L = 0;
        int R = bigger.length - 1;
        while (L <= R) {
            int mid = L + ((R - L) >> 1);
            if (bigger[mid] > value) {
                R = mid - 1;
            } else if (bigger[mid] < value) {
                smallerThanMe = (mid + 1);
                L = mid + 1;
            } else {
                smallerThanMe = mid;
                R = mid - 1;
            }
        }
        return smallerThanMe + offset;
    }
}

计算右侧小于当前元素的个数问题

题目描述见:LeetCode 315. Count of Smaller Numbers After Self

本题也是利用了归并排序的merge过程,由于归并排序是从小到大排序,而我们需要得到某个元素右侧有多少比它小,所以我们还需要将归并排序改成从大到小排序。

以某一次merge过程为例,比如

左侧区间(已排好序): [5,3,2,1]

右侧区间(已排好序):[6,4,3,3]

示例图如下

img

当左侧指针来到s1的时候,右侧指针移动到s2的时候,开始比左侧的值要小,此时可以结算s1位置右侧有几个比它小的元素。

左侧组中比 s1 更小的元素个数 + (r - s2 + 1)

完整代码见:

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        List<Integer> ans = new ArrayList<>(nums.length);
        Node[] nodes = new Node[nums.length];
        for (int i = 0; i < nums.length; i++) {
            nodes[i] = new Node(i, nums[i]);
            ans.add(0);
        }
        count(nodes, 0, nums.length - 1, ans);

        return ans;
    }

    public void count(Node[] nums, int l, int r, List<Integer> result) {
        if (l != r) {
            int m = ((r - l) >> 1) + l;
            count(nums, l, m, result);
            count(nums, m + 1, r, result);
            merge(nums, l, m, r, result);
        }
    }

    // 54 21 20 19 18 17
    public void merge(Node[] nums, int l, int m, int r, List<Integer> result) {
        Node[] help = new Node[r - l + 1];
        int i = 0;
        int ls = l;
        int rs = m + 1;
        while (ls <= m && rs <= r) {
            if (nums[ls].value > nums[rs].value) {
                result.set(nums[ls].index, r - rs + 1 + result.get(nums[ls].index));
                help[i++] = nums[ls++];
            } else {
                help[i++] = nums[rs++];
            }
        }
        while (ls <= m) {
            help[i++] = nums[ls++];
        }
        while (rs <= r) {
            help[i++] = nums[rs++];
        }
        for (i = 0; i < help.length; i++) {
            nums[l + i] = help[i];
        }
    }


    public class Node {
        public int value;
        public int index;

        public Node(int index, int value) {
            this.value = value;
            this.index = index;
        }
    }
}

LintCode上有一个类似的题目,题目描述见:LintCode 532. Reverse Pairs

本题的思路和上一题一致,都是先将归并排序改成从大到小排序,然后在merge过程中,求一个数右侧有几个数比它小,不赘述,代码见:

public class Solution {
   public static long reversePairs(int[] A) {
        if (null == A || A.length < 2) {
            return 0;
        }
        return process(A, 0, A.length - 1);
    }

    private static long process(int[] a, int l, int r) {
        if (l == r) {
            return 0L;
        }
        int m = l + ((r - l) >> 1);
        return process(a, l, m) + process(a, m + 1, r) + merge(a, l, m, r);
    }

    private static long merge(int[] a, int l, int m, int r) {
        int[] help = new int[r - l + 1];
        int index = 0;
        int s1 = l;
        int s2 = m + 1;
        long ans = 0L;
        while (s1 <= m && s2 <= r) {
            if (a[s1] < a[s2]) {
                help[index++] = a[s2++];
            } else if (a[s1] > a[s2]) {
                ans += (r - s2 + 1);
                help[index++] = a[s1++];
            } else {
                help[index++] = a[s2++];
            }
        }
        while (s1 <= m) {
            help[index++] = a[s1++];
        }
        while (s2 <= r) {
            help[index++] = a[s2++];
        }
        index = 0;
        for (int n : help) {
            a[l + (index++)] = n;
        }
        return ans;
    }
}

翻转对问题

题目描述见:LeetCode 493. Reverse Pairs

本题也是利用merge过程,不同于上述两个问题,本题在merge两个区间之前,就要先统计一下num[i] > 2 * num[j]的数量。

完整代码见:

class Solution {
   public static int reversePairs(int[] A) {
        if (null == A || A.length < 2) {
            return 0;
        }
        int size = A.length;
        return process(A, 0, size - 1);
    }

    public static int process(int[] a, int l, int r) {
        if (l == r) {
            return 0;
        }
        int m = l + ((r - l) >> 1);
        return process(a, l, m) + process(a, m + 1, r) + merge(a, l, m, r);
    }

    public static int merge(int[] a, int l, int m, int r) {
        // 先执行统计
        int ans = 0;
        int s1 = l;
        int s2 = m + 1;
        while (s1 <= m && s2 <= r) {
            if ((long) a[s1] - (long) a[s2] > (long) a[s2]) {
                ans += (r - s2 + 1);
                s1++;
            } else {
                s2++;
            }
        }
        // 以下是经典mergesort排序
        int[] help = new int[r - l + 1];
        s1 = l;
        s2 = m + 1;
        int index = 0;

        while (s1 <= m && s2 <= r) {
            if (a[s1] < a[s2]) {
                help[index++] = a[s2++];
            } else if (a[s1] > a[s2]) {
                help[index++] = a[s1++];
            } else {
                help[index++] = a[s2++];
            }
        }
        while (s1 <= m) {
            help[index++] = a[s1++];
        }
        while (s2 <= r) {
            help[index++] = a[s2++];
        }
        index = 0;
        for (int n : help) {
            a[l + (index++)] = n;
        }
        return ans;
    }
}

区间和的个数问题

题目描述见:LeetCode 327. Count of Range Sum

本题有几个优化点:

  1. 由于需要快速得到区间和,所以,可以通过前缀和数组来加速区间和的求法。

  2. merge过程中,由于存在单调性,所以可以通过滑动窗口的方式,定位到区间和的上下界,整个过程不回退,所以不会增加归并排序的整体时间复杂度。

完整代码和注释见

class Solution {
    public static int countRangeSum(int[] nums, int lower, int upper) {
        int size = nums.length;
        // 前缀和数组加速求区间的和!!
        long[] preSum = new long[size];
        preSum[0] = nums[0];
        for (int i = 1; i < size; i++) {
            preSum[i] = nums[i] + preSum[i - 1];
        }
        return p(preSum, 0, size - 1, lower, upper);
    }

    public static int p(long[] preSum, int i, int j, int lower, int upper) {
        if (i == j) {
            if (preSum[i] >= lower && preSum[j] <= upper) {
                return 1;
            }
            return 0;
        }
        int mid = i + ((j - i) >> 1);
        return p(preSum, i, mid, lower, upper) + p(preSum, mid + 1, j, lower, upper) + merge(preSum, i, mid, j, lower, upper);
    }

    private static int merge(long[] preSum, int i, int mid, int j, int lower, int upper) {
        // 单调性->滑动窗口
        int pair = 0;
        int L = i;
        int R = i;
        int S = mid + 1;
        // 区间和存在单调性,使用滑动窗口定位上下界,不回退,所以O(logN)
        while (S <= j) {
            long max = preSum[S] - lower;
            long min = preSum[S] - upper;
            while (L <= mid && preSum[L] < min) {
                L++;
            }
            while (R <= mid && preSum[R] <= max) {
                R++;
            }
            pair += (R - L);
            S++;
        }

        // mergeSort经典代码
        long[] helper = new long[j - i + 1];
        int l = i;
        int r = mid + 1;
        int index = 0;
        while (l <= mid && r <= j) {
            if (preSum[l] > preSum[r]) {
                helper[index++] = preSum[r++];
            } else {
                helper[index++] = preSum[l++];
            }
        }
        while (l <= mid) {
            helper[index++] = preSum[l++];
        }
        while (r <= j) {
            helper[index++] = preSum[r++];
        }
        int k = 0;
        for (long num : helper) {
            preSum[i + (k++)] = num;
        }
        return pair;
    }
}

更多

算法和数据结构学习笔记

算法和数据结构学习代码

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GreyZeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值