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


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

    作者:Grey

    原文地址:

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

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

    归并排序的递归解法

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

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

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

    但是merge过程需要辅助数组,所以额外空间复杂度为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)
    

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

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

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

    a为子过程的调用次数

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

    如果log(b,a) < d,则递归函数复杂度为O(N^d)

    如果log(b,a) > d,则递归函数复杂度为O(N^log(b,a))

    如果log(b,a) == d,则递归函数复杂度为O(N^d * logN)

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

    所以,根据 master 公式,归并排序的算法时间复杂度为O(N*logN)

    归并排序的迭代版本实现

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

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

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

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

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

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

    [0……1],[2……3],[4……5],[6……7],[8……8]

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

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

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

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

    [0……3],[4……7],[8……8]

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

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

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

    [0……7],[8……8]

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

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

    最后设置步长为 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 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;
            }
        }
    }
    

    合并有序数组

    题目描述见LeetCode 88. Merge Sorted Array

    本题思路就是归并排序的merge过程,不赘述,代码如下

    class Solution {
        public void merge(int[] nums1, int m, int[] nums2, int n) {
            int len = m + n;
            while (m > 0 && n > 0) {
                if (nums1[m - 1] > nums2[n - 1]) {
                    nums1[--len] = nums1[--m];
                } else {
                    nums1[--len] = nums2[--n];
                }
            }
            while (n > 0) {
                nums1[--len] = nums2[--n];
            }
        }
    }
    

    注:本题在 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]

    示例图如下

    image

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

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

    完整代码见:

    class Solution {
       public static class Node {
            public int value;
            public int index;
    
            public Node(int index, int value) {
                this.value = value;
                this.index = index;
            }
        }
    
        // 思路转换为:一个数的右边有多少个数比它小!
        // 改归并排序(从大到小)
        public static List<Integer> countSmaller(int[] nums) {
            List<Integer> result = new ArrayList<>(nums.length);
            Node[] nodes = new Node[nums.length];
            for (int i = 0; i < nums.length; i++) {
                result.add(0);
                nodes[i] = new Node(i, nums[i]);
            }
            process(nodes, 0, nums.length - 1, result);
            return result;
        }
    
        private static void process(Node[] nodes, int l, int r, List<Integer> result) {
            if (l == r) {
                return;
            }
            int m = l + ((r - l) >> 1);
            process(nodes, l, m, result);
            process(nodes, m + 1, r, result);
            merge(nodes, l, m, r, result);
        }
    
        private static void merge(Node[] nodes, int l, int m, int r, List<Integer> result) {
            Node[] help = new Node[r - l + 1];
            int s1 = l;
            int s2 = m + 1;
            int index = 0;
            while (s1 <= m && s2 <= r) {
                if (nodes[s1].value > nodes[s2].value) {
                    result.set(nodes[s1].index, result.get(nodes[s1].index) + r - s2 + 1);
                    help[index++] = nodes[s1++];
                } else if (nodes[s1].value < nodes[s2].value) {
                    help[index++] = nodes[s2++];
                } else {
                    help[index++] = nodes[s2++];
                }
            }
            while (s1 <= m) {
                help[index++] = nodes[s1++];
            }
            while (s2 <= r) {
                help[index++] = nodes[s2++];
            }
            for (int i = 0; i < help.length; i++) {
                nodes[l + i] = help[i];
            }
        }
    }
    

    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;
        }
    }
    

    更多

    算法和数据结构笔记

    参考资料

    算法和数据结构体系班-左程云

  • 相关阅读:
    Comparison of Performance Testing Tools
    软件测试两年总结
    利用FSO生成QTP测试报告
    Web 2.0 再思考(三)「搭讪」是需要理由的
    脑子里想的是A,嘴巴上说的是B,实际上做的是C,幻想前景远大的是D,可真正赚钱的恰恰却是E
    东莞镇区实力排名榜
    狂籌資計畫
    转载:日剧和韩剧的区别
    Web 2.0 再思考(一)「关系」才是重点
    炒股
  • 原文地址:https://www.cnblogs.com/greyzeng/p/16653063.html
Copyright © 2020-2023  润新知