排序(五):归并排序法

一. 基本思想
    归并排序的思想是将数组分成两部分,分别进行排序,然后归并起来。把长度为n的输入序列分成两个长度为n/2的子序列;对这两个子序列分别采用归并排序;将两个排序好的子序列合并成一个最终的排序序列。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二. 代码实现

  • 版本一
package com.hong.sort;

import java.util.Arrays;

/**
 * 归并排序 O(Nlog(N))
 * 需要将当前的数组拷贝一份出来,因此相比之前的排序,需要更多的存储空间
 */
public class MergeSort {

    /**
     * 将arr[l...mid]和arr[mid+1...r]两部分进行归并
     *
     * @param arr
     * @param l   左半部元素起始点
     * @param mid 左半部元素终点
     * @param r   右半部元素终点
     */
    private static void merge(int[] arr, int l, int mid, int r) {

        int[] aux = Arrays.copyOfRange(arr, l, r + 1);

        // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {

            if (i > mid) {  // 如果左半部分元素已经全部处理完毕
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {   // 如果右半部分元素已经全部处理完毕
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l] < aux[j - l]) {  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i - l];
                i++;
            } else {  // 左半部分所指元素 >= 右半部分所指元素
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

    /**
     * 递归使用归并排序,对arr[l...r]的范围进行排序
     *
     * @param arr
     * @param l
     * @param r
     */
    public static void sort(int[] arr, int l, int r) {
        if (l >= r) {
            return;
        }

       // int mid = (l + r) / 2;
        int mid = l + (r-l)/2;
        sort(arr, l, mid);
        sort(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }

    public static void sort(int[] arr) {
        int n = arr.length;
        // [0,n-1],这里注意区间的开闭
        sort(arr, 0, n - 1);
    } 
}
  • 版本二
package com.hong.sort;

import java.util.Arrays;

/**
 * 归并排序优化
 */
public class MergeSort2 {

    public static void sort(int[] arr) {
        sort(arr, 0, arr.length - 1);
    }

    public static void sort(int[] arr, int left, int right) {
        /**
         * 优化2:
         *对于小规模数组, 使用插入排序
         */
        if (right - left <= 15){
            InsertionSort.sort(arr,left,right);
            return;
        }

       // int mid = (left + right) / 2;
        int mid = left + (right - left)/2;
        sort(arr, left, mid);
        sort(arr, mid + 1, right);
        /**
         * 优化1
         *   对于arr[mid] <= arr[mid+1]的情况,不进行merge
         *   对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
         */
        if (arr[mid] > arr[mid+1]){
            merge(arr, left, mid, right);
        }
    }

    public static void merge(int[] arr, int l, int mid, int r) {
        int[] aux = Arrays.copyOfRange(arr, l, r + 1);
        int i = l;
        int j = mid + 1;
        for (int k = l; k <= r; k++) {
            if (i > mid) {
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l] < aux[j - l]) {
                arr[k] = aux[i - l];
                i++;
            } else {
                arr[k] = aux[j - l];
                j++;
            }
        }
    }
}
public class InsertionSort {
    /**
     * 对arr[l...r]的区间使用InsertionSort排序
     * @param arr
     * @param l
     * @param r
     */
    public static void sort(int[] arr, int l, int r) {
        for (int i = l + 1; i <= r; i++) {
            int e = arr[i];
            int j = i;
            for (; j > l && arr[j - 1] > e; j--) {
                arr[j] = arr[j - 1];
            }
            arr[j] = e;
        }
    }
}

上面的两个版本都是使用递归实现自顶向下的归并排序。

  • 版本三
package com.hong.sort;

import java.util.Arrays;

/**
 * <br>自底向上的归并排序</br>
 * 之前的归并排序中是自顶向下,扩散式逐步拆分成小的组,排好序后,再逐步向上收敛整合成一个最终有序的序列
 *
 */
public class MergeSort3 {

    public static void merge(int[] arr, int l, int mid, int r) {
        int[] aux = Arrays.copyOfRange(arr, l, r + 1);
        int i = l;
        int j = mid + 1;
        for (int k = l; k <= r; k++) {
            if (i > mid) {
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l] < aux[j - l]) {
                arr[k] = aux[i - l];
                i++;
            } else {
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

    /**
     * Merge Sort Bottom Up 无优化版本
     *
     * @param arr
     */
    public static void sort0(int[] arr) {
        int n = arr.length;

        // sz 表示对需要进行merge的元素个数进行遍历
        // 第一轮 看 1个元素。之后看2个元素。。。
      for (int sz = 1; sz < n; sz *= 2) {
            // 每一轮在归并的过程中,起始元素位置
            for (int i = 0; i < n - sz; i += sz + sz) {
                // 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并,注意越界问题
                merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
            }
        }
    }

    /**
     * Merge Sort Button Up优化
     *
     * @param arr
     */
    public static void sort(int[] arr) {
        int n = arr.length;

        // 对于小数组, 使用插入排序优化
        for (int i = 0; i < n; i += 16) {
            InsertionSort.sort(arr, i, Math.min(i + 15, n - 1));
        }

        for (int sz = 16; sz < n; sz += sz) {
            for (int i = 0; i < n - sz; i += sz + sz) {
                // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
                if (arr[i + sz - 1] > arr[i + sz]) {
                    merge(arr, i, i + sz - 1, Math.min(i + sz + sz - 1, n - 1));
                }
            }
        }
    }
}

三. 归并排序在链表中的应用
LeetCode 148. 排序链表。
Merge Sort Bottom Up :使用 nlog(n) 的复杂度为链表排序

package com.hong.leetcode;

import com.hong.linked.ListNode;

/**
 * Created by wanghong
 * Date 2019-05-10 19:18
 * Description:148. 排序链表
 */
public class Solution148 {

    /**
     * 自顶向下
     * 参考:Sort List——经典(链表中的归并排序) https://www.cnblogs.com/qiaozhoulin/p/4585401.html
     *
     * 归并排序法:在动手之前一直觉得空间复杂度为常量不太可能,因为原来使用归并时,都是 O(N)的,
     * 需要复制出相等的空间来进行赋值归并。对于链表,实际上是可以实现常数空间占用的(链表的归并
     * 排序不需要额外的空间)。利用归并的思想,递归地将当前链表分为两段,然后merge,分两段的方
     * 法是使用 fast-slow 法,用两个指针,一个每次走两步,一个走一步,知道快的走到了末尾,然后
     * 慢的所在位置就是中间位置,这样就分成了两段。merge时,把两段头部节点值比较,用一个 p 指向
     * 较小的,且记录第一个节点,然后 两段的头一步一步向后走,p也一直向后走,总是指向较小节点,
     * 直至其中一个头为NULL,处理剩下的元素。最后返回记录的头即可。
     *
     * 主要考察3个知识点,
     * 知识点1:归并排序的整体思想
     * 知识点2:找到一个链表的中间节点的方法
     * 知识点3:合并两个已排好序的链表为一个新的有序链表
     *
     * 一切的前提就是搞清为啥归并排序可以排序链表,原因如下
     * 包括选择、插入、堆排序、快排等等都需要随机读取进行元素的交换,
     * 这正是链表所缺乏的,归并排序是为数不多不需要随机读取的排序算法
     */
    public ListNode sortList(ListNode head) {
        return head == null ? null : mergeSort(head);
    }

    private ListNode mergeSort(ListNode head) {
        if (head.next == null) {
            return head;
        }
        // p 最后表示中间节点,pre 最后表示 中间节点的前一个节点
        ListNode p = head, q = head, pre = null;
        while (q != null && q.next != null) {
            pre = p;
            p = p.next;
            q = q.next.next;
        }
        // 将整个链表一分为二
        pre.next = null;
        ListNode l = mergeSort(head);
        ListNode r = mergeSort(p);
        return merge(l, r);
    }

    ListNode merge(ListNode l, ListNode r) {
        // 哑链表头。临时创建的一个链表头,把边界情况和普通情况做统一处理
        ListNode dummyHead = new ListNode(0);
        ListNode cur = dummyHead;
        while (l != null && r != null) {
            if (l.val <= r.val) {
                cur.next = l;
                cur = cur.next;
                l = l.next;
            } else {
                cur.next = r;
                cur = cur.next;
                r = r.next;
            }
        }
        if (l != null) {
            cur.next = l;
        }
        if (r != null) {
            cur.next = r;
        }
        return dummyHead.next;
    }

    /****************************************************/
    /**
     *要求空间复杂度是 O(1),因此不能使用递归。因此这里使用 bottom-to-up 的算法来解决
     * bottom-to-up 的归并思路是这样的:先两个两个的 merge,完成一趟后,再 4 个4个的 merge,直到结束。举个简单的例子:[4,3,1,7,8,9,2,11,5,6].
     *
     * step=1: (3->4)->(1->7)->(8->9)->(2->11)->(5->6)
     * step=2: (1->3->4->7)->(2->8->9->11)->(5->6)
     * step=4: (1->2->3->4->7->8->9->11)->5->6
     * step=8: (1->2->3->4->5->6->7->8->9->11)
     * 链表里操作最难掌握的应该就是各种断链啊,然后再挂接啊。在这里,我们主要用到链表操作的两个技术:
     *
     * merge(l1, l2),双路归并,我相信这个操作大家已经非常熟练的,就不做介绍了。
     * cut(l, n),可能有些同学没有听说过,它其实就是一种 split 操作,即断链操作。不过我感觉使用 cut 更准确一些,它表示,将链表 l 切掉前 n 个节点,并返回后半部分的链表头。
     * 额外再补充一个 dummyHead 大法,已经讲过无数次了,仔细体会吧。
     * @param head
     * @return
     */
    public ListNode sortList2(ListNode head) {

        return null;
    }

    public static void main(String[] args) {
        Solution148 s = new Solution148();
        int[] nums = {4,2,1,3};
        ListNode head = new ListNode(nums);
        System.out.println(head);
        head = s.sortList(head);
        System.out.println(head);
    }

}
package com.hong.linked;

/**
 * @author wanghong
 * @date 2019/04/08 22:57
 **/
public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int x) {
        val = x;
    }

    // 链表节点的构造函数
    // 使用arr为参数,创建一个链表,当前的ListNode为链表头结点
    public ListNode(int[] arr){

        if(arr == null || arr.length == 0)
            throw new IllegalArgumentException("arr can not be empty");

        this.val = arr[0];
        ListNode cur = this;
        for(int i = 1 ; i < arr.length ; i ++){
            cur.next = new ListNode(arr[i]);
            cur = cur.next;
        }
    }

    // 以当前节点为头结点的链表信息字符串
    @Override
    public String toString(){

        StringBuilder s = new StringBuilder();
        ListNode cur = this;
        while(cur != null){
            s.append(cur.val + "->");
            cur = cur.next;
        }
        s.append("NULL");
        return s.toString();
    }

    public int getVal() {
        return val;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章