并归排序(数组+链表)

  

  并归排序与快速排序相似,靠分治思想突破了排序算法 O(n2) 的瓶颈。

  我们看回顾一下几大排序算法的时间、空间复杂度:

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 是否稳定
冒泡排序 O(n2)O(n2) O(n2)O(n2) O(1)O(1)
选择排序 O(n2)O(n2) O(n2)O(n2) O(1)O(1) 不是
直接插入排序 O(n2)O(n2) O(n2)O(n2) O(1)O(1)
归并排序 O(nlogn)O(nlogn) O(nlogn)O(nlogn) O(n)O(n)
快速排序 O(nlogn)O(nlogn) O(n2)O(n2) O(logn)O(logn) 不是
堆排序 O(nlogn)O(nlogn) O(nlogn)O(nlogn) O(1)O(1) 不是
希尔排序 O(nlogn)O(nlogn) O(ns)O(ns) O(1)O(1) 不是
计数排序 O(n+k)O(n+k) O(n+k)O(n+k) O(n+k)O(n+k)
基数排序 O(N∗M)O(N∗M) O(N∗M)O(N∗M) O(M)O(M)

  早期的排序算法总是免不了元素间的一一比较,因此时间复杂度很难突破 O(n2) 。而并归排序采用分治的思想将问题的规模缩小,使用小问题的解来解决大问题,并由此突破了 n的诅咒。

  以冒泡排序为例,我们需要n次遍历,每次遍历将数组中最大或者最小的元素冒到顶端,而这样的遍历需要 n-1 次。本质上每次遍历等于从所有元素中找到最大或者最小的元素,这就要求我们需要遍历和比较到数组中未排序的每一个元素。

  所以冒泡排序的计算次数为 n-1 + n-2 + n-3 +...+1 = n(n+1)/2 ,时间复杂度表示为 O(n2)。

  那么我们想一下,如果我们不是对一个杂乱的序列进行排序,而是对两个有序的子序列进行排序的话情况会是怎样的:

  我们可以维护两个指针分别指向两个子序列的顶端,选择较小的元素放入新的序列,并向后移动指向拿走的元素的指针。这样我们从未排序的元素中选出一个最小或最大的数只要比较一次。

  我们可以不断的缩小排序序列的范围来构建有序的子序列,从下向上一层一层逐步完成对整个序列的排序。

  缩小的排序范围的过程是这样的,不断的将序列分解为俩个子序列,直到序列无法分解。比如一个序列长度为8:

  两个长度为4的子序列--->四个长度为2的子序列---->八个长度为1的子序列。

  分解过程就像一颗 B树 向下分裂(不同的是分裂时父节点不变),第 n 层的拥有 2 个节点,也就是说直到每个节点中只包含一个元素时共分裂 log2n 次。

  而每一层总的元素数不变,使该层所有序列变为有序数列需要 n 次比较。

  整个过程下来,我们需要比较 nlog2n 次。也就是并归排序的时间复杂度为 O( nlog2n ) 。

  (也不知道为什么,用小问题推导大问题总是比直接解决大问题来的快,可能是程序员的命吧。其实个人觉着不管什么问题,如果有办法用子问题来推导原问题,那么时间复杂度中一定包含log分解出的子问题数量问题规模,一旦觉着自己当前尝试的解法比该解法时间复杂度高,不妨尝试一下分治。)

  所以我们有两个关键步骤:分解为子序列、合并子序列为一个有序序列

  下面上代码,注释比较全,以下两种解法都已在leetcode提交通过:

    /**
     * @Author Nxy
     * @Date 2019/12/4
     * @Param
     * @Return
     * @Exception
     * @Description 数组并归排序
     *  将begin、end间的数组分解为两个子序列并回归排序
     */
    public static void mergeSort(int[] nums, int begin, int end) {
        int length = nums.length;
        //回归条件,子序列长度为一时返回
        if (begin == end) {
            return;
        }
        //序列中点
        int mid = (begin + end) / 2;
        //排序左边子序列
        mergeSort(nums, begin, mid);
        //排序右边子序列
        mergeSort(nums, mid + 1, end);
        //并归已排序的左右子序列
        merge(nums, begin, mid, end);

    }

    /**
     * @Author Nxy
     * @Date 2019/12/4 10:59
     * @Param
     * @Return
     * @Exception
     * @Description 并归  begin--mid  与  mid+1--end  两个子序列
     */
    public static void merge(int[] nums, int begin, int mid, int end) {
        //临时数组大小
        int length = end - begin + 1;
        int[] temp = new int[length];
        //临时数组将要填充的位置指针
        int i = 0;
        //左子序列将要拿出的位置指针
        int left = begin;
        //右子序列将要拿出的位置指针
        int right = mid + 1;
        while (i < length) {
            //一个子序列为空,将另一个子序列余下的元素放入临时数组
            if (left == mid + 1) {
                System.arraycopy(nums, right, temp, i, end - right + 1);
                break;
            }
            if (right == end + 1) {
                System.arraycopy(nums, left, temp, i, mid - left + 1);
                break;
            }
            //选择较小的元素放入临时数组
            if (nums[left] >= nums[right]) {
                temp[i] = nums[right];
                right++;
                i++;
            } else {
                temp[i] = nums[left];
                left++;
                i++;
            }
        }
        System.arraycopy(temp, 0, nums, begin, length);
        //手动为临时数组去掉引用,方便连续的内存空间被及时回收
        temp=null;
    }    

  链表的并归排序与数组一个思路:

  /**
     * @Author Nxy
     * @Date 2019/12/4
     * @Param
     * @Return
     * @Exception
     * @Description 链表并归排序
* 递归分解序列为两个子序列,并向上并归排序,返回排序后的总链表 * 使用快慢指针法,快指针到终点时慢指针指向中点
*/ public static ListNode mergeSort(ListNode head) { //回归条件 if (head.getNext() == null) { return head; } //快指针,考虑到链表为2时的情况,fast比slow早一格 ListNode fast = head.getNext(); //慢指针 ListNode slow = head; //快慢指针开跑 while (fast != null && fast.getNext() != null) { fast = fast.getNext().getNext(); slow = slow.getNext(); } //找到右子链表头元素,复用fast引用 fast = slow.getNext(); //将中点后续置空,切割为两个子链表 slow.setNext(null); //递归分解左子链表,得到新链表起点 head = mergeSort(head); //递归分解右子链表,得到新链表起点 fast = mergeSort(fast); // System.out.println(head.getValue()+" "+fast.getValue()); //并归两个子链表 ListNode newHead = merge(head, fast); // ListNode.print(newHead); return newHead; } /** * @Author Nxy * @Date 2019/12/4 14:48 * @Param * @Return * @Exception * @Description 以left节点为起点的左子序列 及 以right为起点的右子序列 并归为一个有序序列并返回头元素; * 传入的 left 及 right 都不可为 null */ public static ListNode merge(ListNode left, ListNode right) { //维护临时序列的头元素 ListNode head; if (left.getValue() <= right.getValue()) { head = left; left = left.getNext(); } else { head = right; right = right.getNext(); } //两个子链表均存在剩余元素 ListNode temp = head; while (left != null && right != null) { //将较小的元素加入临时序列 if (left.getValue() <= right.getValue()) { temp.setNext(left); left = left.getNext(); temp = temp.getNext(); } else { temp.setNext(right); right = right.getNext(); temp = temp.getNext(); } } //左子序列用完将右子序列余下元素加入临时序列 if (left == null) { temp.setNext(right); } //右子序列用完将左子序列余下元素加入临时序列 if (right == null) { temp.setNext(left); } ListNode.print(head); return head; }

  

猜你喜欢

转载自www.cnblogs.com/niuyourou/p/11986221.html