資料結構與演算法——歸併排序: 陣列&連結串列&遞迴&非遞迴解法全家桶
原文連結:https://jiang-hao.com/articles/2020/algorithms-algorithms-merge-sort.html
文章目錄
演算法介紹
歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
作為一種典型的分而治之思想的演算法應用,歸併排序的實現由兩種方法:
-
自上而下的遞迴:它從樹的頂端開始,然後向下操作,每次操作都問同樣的問題(我需要做什麼來排序這個陣列?)並回答它(分成兩個子陣列,進行遞迴呼叫,合併結果),直到我們到達樹的底部。
-
自下而上的迭代:不需要遞迴。它直接從樹的底部開始,然後通過遍歷這些片段再將它們合併起來。
在《資料結構與演算法 JavaScript 描述》中,作者給出了自下而上的迭代方法。但是對於遞迴法,作者卻認為:
However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.
然而,在 JavaScript 中這種方式不太可行,因為這個演算法的遞迴深度對它來講太深了。
和選擇排序一樣,歸併排序的效能不受輸入資料的影響,但表現比選擇排序好的多,因為始終都是 O(nlogn) 的時間複雜度。代價是需要額外的記憶體空間。
歸併排序分為三個過程:
- 將數列劃分為兩部分(在均勻劃分時時間複雜度為 );
- 遞迴地分別對兩個子序列進行歸併排序;
- 合併兩個子序列。
不難發現,歸併排序的核心是如何合併兩個子序列,前兩步都很好實現。
其實合併的時候也不難操作。注意到兩個子序列在第二步中已經保證了都是有序的了,第三步中實際上是想要把兩個 有序 的序列合併起來。
演算法步驟
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列;
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置;
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置;
- 重複步驟 3 直到某一指標達到序列尾;
- 將另一序列剩下的所有元素直接複製到合併序列尾。
程式碼實現
陣列實現時間複雜度O(NlogN),空間複雜度O(N)
遞迴實現一:每次歸併時都建立一個輔助陣列
public static int[] sort(int[] nums) {
// 對陣列進行拷貝,不改變引數內容
int[] arr = Arrays.copyOf(nums, nums.length);
if (arr.length<2) return arr;
int middle = (int) Math.floor(arr.length >> 1);
int[] left = Arrays.copyOfRange(arr, 0, middle);
int[] right = Arrays.copyOfRange(arr, middle, arr.length);
return merge(sort(left), sort(right));
}
public static int[] merge(int[] left, int[] right) {
// 建立一個輔助陣列儲存歸併結果
int[] result = new int[left.length+right.length];
int i=0, j=0;
while (i+j < result.length) {
// 右側陣列全都轉存完時,直接將左側陣列剩餘的元素轉存到結果陣列
if (j==right.length) {
result[i+j] = left[i++];
}
// 左側陣列全都轉存完時,直接將右側陣列剩餘的元素轉存到結果陣列
else if (i==left.length) {
result[i+j] = right[j++];
}
// 否則,將兩個子陣列當前元素中較小的那個轉存到結果陣列中
else result[i+j] = left[i]<=right[j]? left[i++]: right[j++];
}
return result;
}
力扣執行結果:
執行用時:10 ms, 在所有 Java 提交中擊敗了30.97%的使用者
記憶體消耗:44.2 MB, 在所有 Java 提交中擊敗了99.55%的使用者
提交時間 | 提交結果 | 執行時間 | 記憶體消耗 | 語言 |
---|---|---|---|---|
幾秒前 | 通過 | 10 ms | 43.8 MB | Java |
幾秒前 | 通過 | 10 ms | 43.9 MB | Java |
幾秒前 | 通過 | 10 ms | 44.1 MB | Java |
遞迴實現二:僅建立一次一個等長的輔助陣列,交替歸併
/**
* 遞迴交替合併
* @param src 待合併的陣列
* @param dst 合併結果陣列
* @param start 陣列 src 的 start 下標
* @param end 陣列 src 的 end 下標
*/
public static void sort(int[] src, int[] dst, int start, int end) {
// 當待排序段[start,end)僅包含小於等於一個元素時,自然有序
if (end-start<2) {
dst[start]=src[start];
return;
}
// 將左右兩段子陣列分別排好序
int i = start + (end-start)/4;
int ii = start + (end-start)/2;
int iii = start + 3*(end-start)/4;
sort(src, dst, start, i);
sort(src, dst, i, ii);
sort(src, dst, ii, iii);
sort(src, dst, iii, end);
merge(dst, src, start, i, ii);
merge(dst, src, ii, iii, end);
// 最後歸併
merge(src, dst, start, ii, end);
}
/**
* 歸併方法:合併左右兩段已分別排好序的 src[start:middle) 和 src[middle:end) 到 dst[start:end)
* 陣列 src 的 [start:middle) 部分以及 [middle:end) 部分都已經各自排好序
* @param src 待合併的陣列
* @param dst 合併結果陣列
* @param start 陣列 src 的 start 下標
* @param middle 陣列 src 的 middle 下標
* @param end 陣列 src 的 end 下標
*/
private static void merge(int [] src, int [] dst, int start, int middle, int end){
int i = start;
int j = middle;
int k = start;
while (k<end) {
// 右側陣列全都轉存完時,直接將左側陣列剩餘的元素轉存到結果陣列
if (j==end) {
dst[k++] = src[i++];
}
// 左側陣列全都轉存完時,直接將右側陣列剩餘的元素轉存到結果陣列
else if (i==middle) {
dst[k++] = src[j++];
}
// 否則,將兩個子陣列當前元素中較小的那個轉存到結果陣列中
else dst[k++] = src[i]<=src[j]? src[i++]: src[j++];
}
}
力扣執行結果:
執行用時:6 ms, 在所有 Java 提交中擊敗了64.16%的使用者
記憶體消耗:45.7 MB, 在所有 Java 提交中擊敗了86.98%的使用者
提交時間 | 提交結果 | 執行時間 | 記憶體消耗 | 語言 |
---|---|---|---|---|
幾秒前 | 通過 | 6 ms | 46.3 MB | Java |
幾秒前 | 通過 | 6 ms | 45.9 MB | Java |
幾秒前 | 通過 | 6 ms | 46 MB | Java |
非遞迴實現
/**
* 歸併方法:合併左右兩段已分別排好序的 src[start:middle) 和 src[middle:end) 到 dst[start:end)
* 陣列 src 的 [start:middle) 部分以及 [middle:end) 部分都已經各自排好序
* @param src 待合併的陣列
* @param dst 合併結果陣列
* @param start 陣列 src 的 start 下標
* @param middle 陣列 src 的 middle 下標
* @param end 陣列 src 的 end 下標
*/
private static void merge(int [] src, int [] dst, int start, int middle, int end){
int i = start;
int j = middle;
int k = start;
while (k < end) {
if (i==middle) dst[k++] = src[j++];
else if (j==end) dst[k++] = src[i++];
else dst[k++] = src[i] <= src[j] ? src[i++] : src[j++];
}
}
/**
* 用於合併排好序的相鄰陣列段
* 將 x 合併到 y
* @param x
* @param y
* @param s 合併大小
*/
private static void mergePass(int [] x,int [] y,int s){
//從第一個元素開始
int i = 0;
//i+2*s 要小於等於陣列長度,也就是說未合併的元素個數要大於2*s
while (i + 2*s <= x.length) {
//合併大小為s的相鄰2段子陣列
merge(x, y, i, i+s, i+2*s);
i += 2*s;
}
//此迴圈執行的次數為: x.length/(2*s) 次 9/(2*1)=4
//若未合併的元素個數大於 1*s,則合併最後兩個序列
if (i+s < x.length) merge(x, y, i, i+s, x.length);
//否則直接複製到y
else {
while (i < x.length) {
y[i] = x[i++];
}
}
}
/**
* 消去遞迴後的歸併排序演算法
* @param a
*/
public static void mergeSort(int []a ){
//申請個大小和a相等的陣列b
int[] tmp = new int[a.length];
int s = 1;
//這裡不能為<=
while (s < a.length) {
//交替合併
mergePass(a, tmp, s);
s *= 2;
mergePass(tmp, a, s);
s *= 2;
}
}
力扣執行結果:
執行用時:7 ms, 在所有 Java 提交中擊敗了55.19%的使用者
記憶體消耗:45.8 MB, 在所有 Java 提交中擊敗了75.00%的使用者
提交時間 | 提交結果 | 執行時間 | 記憶體消耗 | 語言 |
---|---|---|---|---|
幾秒前 | 通過 | 7 ms | 45.8 MB | Java |
幾秒前 | 通過 | 8 ms | 45.7 MB | Java |
幾秒前 | 通過 | 7 ms | 45.7 MB | Java |
連結串列實現時間複雜度O(NlogN),空間複雜度O(1)
遞迴實現
static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public static ListNode sortList(ListNode head) {
if (head==null || head.next==null) return head;
//連結串列的快慢指標二分法
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//找到中間節點
ListNode tmp = slow.next;
//二分切割連結串列
slow.next = null;
//遞迴呼叫歸併
return merge(sortList(head), sortList(tmp));
}
public static ListNode merge(ListNode left, ListNode right) {
//建立哨兵節點,存放歸併結果
ListNode sentinel = new ListNode(-1);
//建立指標,維護尾節點
ListNode tail = sentinel;
//兩個子鏈都還存在節點時,進入迴圈體
while (left!=null && right!=null) {
//較小的節點放入結果連結串列,對應子鏈去頭
if (left.val <= right.val) {
tail.next = left;
left = left.next;
} else {
tail.next = right;
right = right.next;
}
//更新尾節點
tail = tail.next;
}
//將還有剩餘節點的子鏈直接尾接到結果鏈
tail.next = left==null? right: left;
//返回歸併結果
return sentinel.next;
}
力扣執行結果:
執行用時:6 ms, 在所有 Java 提交中擊敗了53.76%的使用者
記憶體消耗:46.5 MB, 在所有 Java 提交中擊敗了23.27%的使用者
提交時間 | 提交結果 | 執行時間 | 記憶體消耗 | 語言 |
---|---|---|---|---|
幾秒前 | 通過 | 6 ms | 46.5 MB | Java |
2 分鐘前 | 通過 | 6 ms | 47 MB | Java |
2 分鐘前 | 通過 | 6 ms | 46.6 MB | Java |
非遞迴實現(從底至頂直接合並)
public static ListNode sortList(ListNode head) {
// 求連結串列長度
ListNode h = head;
int length = 0;
while (h != null) {
h = h.next;
length+=1;
}
// 初始化準備
int blockSize = 1;
// 結果連結串列哨兵
ListNode res = new ListNode(-1);
res.next = head;
// 從1到length迭代單元塊大小
while (blockSize<length) {
// 結果連結串列遊標
ListNode pre = res;
h = res.next;
// 兩兩遍歷所有單元塊
while (h != null) {
// 求第一個子鏈
ListNode h1 = h;
int i = blockSize;
while (i>0 && h!=null) {
h = h.next;
i-=1;
}
if (i>0) break;
// 求第二個子鏈
ListNode h2 = h;
i = blockSize;
while (i>0 && h!=null) {
h = h.next;
i-=1;
}
// 合併兩個子鏈
int c1 = blockSize;
int c2 = blockSize-i;
while (c1>0 && c2>0) {
if(h1.val <= h2.val) {
pre.next = h1;
h1 = h1.next;
c1-=1;
} else {
pre.next = h2;
h2 = h2.next;
c2-=1;
}
pre = pre.next;
}
// 將比較完後其中剩下的那個子鏈所有節點直接尾接到結果連結串列
pre.next = c1>0 ? h1 : h2;
// 歸正遊標
while (c1>0 || c2>0) {
pre = pre.next;
c1-=1;
c2-=1;
}
// 歸正歸併後的連結串列尾節點回原連結串列
pre.next = h;
}
blockSize *= 2;
}
return res.next;
}
力扣執行結果:
執行用時:8 ms, 在所有 Java 提交中擊敗了41.64%的使用者
記憶體消耗:43.1 MB, 在所有 Java 提交中擊敗了66.86%的使用者
提交時間 | 提交結果 | 執行時間 | 記憶體消耗 | 語言 |
---|---|---|---|---|
幾秒前 | 通過 | 8 ms | 43.1 MB | Java |
3 分鐘前 | 通過 | 9 ms | 43.1 MB | Java |
7 分鐘前 | 通過 | 8 ms | 42.8 MB | Java |
演算法複雜度
最優時間複雜度:O(n*log(n))
最壞時間複雜度:O(n*log(n))
平均時間複雜度:O(n*log(n))
最壞空間複雜度:總共O(n),輔助O(n);當使用linked list,輔助空間為O(1).
相關文章
- 資料結構:歸併排序(非遞迴)資料結構排序遞迴
- 利用遞迴實現連結串列的排序(歸併排序)遞迴排序
- 【資料結構】遞迴實現連結串列逆序資料結構遞迴
- 歸併排序的非遞迴實現排序遞迴
- 資料結構與演算法:遞迴資料結構演算法遞迴
- Golang從合併連結串列聊遞迴Golang遞迴
- 【資料結構】二叉樹遍歷(遞迴+非遞迴)資料結構二叉樹遞迴
- 快速排序【遞迴】【非遞迴】排序遞迴
- 資料結構-遞迴資料結構遞迴
- 單連結串列逆置遞迴演算法遞迴演算法
- 資料結構與演算法--迴圈連結串列資料結構演算法
- 資料結構和演算法:遞迴資料結構演算法遞迴
- 資料結構與演算法學習總結--遞迴資料結構演算法遞迴
- 快速排序(遞迴及非遞迴演算法原始碼)排序遞迴演算法原始碼
- 【Java資料結構與演算法筆記(二)】樹的四種遍歷方式(遞迴&非遞迴)Java資料結構演算法筆記遞迴
- 歸併排序(C++_分治遞迴)排序C++遞迴
- 連結串列反轉非遞迴演算法!看不懂打死我!遞迴演算法
- 資料結構與演算法(十一)——演算法-遞迴資料結構演算法遞迴
- Java資料結構與演算法--遞迴和回溯Java資料結構演算法遞迴
- 資料結構5_遞迴資料結構遞迴
- 每天刷個演算法題20160525:快速排序的遞迴轉非遞迴解法演算法排序遞迴
- 資料結構與演算法 | 迴文連結串列檢測資料結構演算法
- 演算法:排序連結串列:歸併排序演算法排序
- 二十一、氣泡排序演算法——JAVA實現(遞迴與非遞迴)排序演算法Java遞迴
- 氣泡排序、快速排序(遞迴&非遞迴)、堆排序演算法比較淺析排序遞迴演算法
- 關於樹型結構資料遞迴查詢,轉非遞迴查詢的實現遞迴
- 連結串列歸併排序排序
- 基礎資料結構之遞迴資料結構遞迴
- 資料結構與演算法整理總結---陣列,連結串列資料結構演算法陣列
- 資料結構之迴圈連結串列資料結構
- 揹包問題的遞迴與非遞迴演算法遞迴演算法
- 淺談歸併排序:合併 K 個升序連結串列的歸併解法排序
- 【資料結構】——搜尋二叉樹的插入,查詢和刪除(遞迴&非遞迴)資料結構二叉樹遞迴
- C++單連結串列遞迴遍歷操作C++遞迴
- 反轉連結串列系列題練習遞迴遞迴
- python 遞迴樹狀結構 和 排序Python遞迴排序
- 資料結構之連結串列與陣列(1):陣列和連結串列的簡介資料結構陣列
- 【資料結構與演算法】歸併排序資料結構演算法排序