一. 基本思想
歸併排序的思想是將數組分成兩部分,分別進行排序,然後歸併起來。把長度爲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;
}
}