歸併排序:陣列和連結串列的多種實現
阿新 • • 發佈:2021-11-11
思想
將陣列進行分割,形成多個組合並繼續分割,一直到每一組只有一個元素時,此時可以看作每一組都是有序的
然後逐漸合併相鄰的有序組合(合併之後也是有序的),分組個數呈倍數減少,每一組的元素個數呈倍數增長
一直到只剩下一個組合包含所有元素,將代表著陣列排序完畢
歸併排序是一種類似二叉樹遍歷的實現,所以時間複雜度與二叉樹遍歷一樣:o(n*log2n)
本來畫了個圖,但是因為太醜了,所以放在最下面
陣列的歸併排序實現
自頂向下
使用遞迴不斷往下遍歷,一直到當前組合只有一個元素,開始回溯,將相鄰的組合進行合併,直到最後只剩下一個組合,陣列即有序。
比如
44,8,33,5,1,9,2這組數字使用歸併排序的流程通過執行結果可以看到
首先遞迴到只有44一個元素的組合,然後回溯,等待相鄰組合變成有序之後,與相鄰組合8進行合併
44 , 8 這個組合 又等待相鄰組合 33 , 5 變成有序之後,進行合併,一直到合併完畢
每次合併的相鄰組合擁有的元素個數,必須與當前組合相等或者不足
實現程式碼
1 import java.util.Arrays; 2 3 public class MergeSortDemo { 4 5 public static void main(String[] args) { 6 7 int[] arr = new int[]{44,8,33,5,1,9,2}; 8 9 System.out.println(Arrays.toString(arr)); 10 11 mergeSort(arr,0,arr.length-1); 12 13 System.out.println(Arrays.toString(arr));View Code14 15 } 16 17 18 public static void mergeSort(int[] arr,int left,int right){ 19 20 if(left >= right){ 21 return; 22 } 23 int mid = (right - left)/2 + left; 24 25 mergeSort(arr,left,mid); 26 27 mergeSort(arr,mid+1,right); 28 29 merge(arr,left,mid,right);30 } 31 public static void merge(int[] arr,int left,int mid,int right){ 32 33 int len = right - left + 1; 34 35 36 //分別記錄兩組元素的起始位置和結束位置 37 int s1 = left, e1 = mid,s2 = mid+1 , e2 = right; 38 39 int[] newArr = new int[len]; 40 int index = 0; 41 42 //將兩組元素進行合併 43 while (s1 <= e1 && s2 <= e2){ 44 45 if(arr[s1] > arr[s2]){ 46 newArr[index++] = arr[s2++]; 47 }else { 48 newArr[index++] = arr[s1++]; 49 } 50 } 51 52 while (s1 <= e1){ 53 newArr[index++] = arr[s1++]; 54 } 55 56 while (s2 <= e2){ 57 newArr[index++] = arr[s2++]; 58 } 59 //將排序好的結果複製回原陣列 60 //新陣列的下標[0...index] 對應 原陣列[left...right] 61 for (int i = left; i <= right; i++) { 62 arr[i] = newArr[i - left]; 63 } 64 } 65 }
自底向上
自底向上沒有使用遞迴的方式,而是使用迴圈修改陣列
首先設定一個分割值gap進行分組,每個組合的元素數量為gap,從1開始,呈倍數增長,一直到等於陣列長度n
在gap每次增長之前,都將相鄰的組合進行合併,由n個組合併為1個組
通過執行結果可以看出,執行中有兩層迴圈
第一層迴圈不斷增長gap的值
第二層迴圈,根據gap的值,沿著陣列每次找出兩個組合,進行合併,一直到找不到為止
合併過程與上面的做法類似
實現程式碼
1 public class MergeSortDemo { 2 3 public static void main(String[] args) { 4 5 int[] arr = new int[]{44,8,33,5,1,9,2}; 6 7 System.out.println(Arrays.toString(arr)); 8 9 merge1(arr); 10 11 System.out.println(Arrays.toString(arr)); 12 13 } 14 15 public static void merge1(int[] arr){ 16 17 18 int gap = 1 , len = arr.length; 19 20 while (gap < len){ 21 22 int start = 0; 23 24 while (start < len){ 25 //兩個組合的起點 26 int s1 = start , s2 = start + gap ; 27 //終點 28 int e1 = s2 -1,e2 = s2 + gap - 1; 29 30 //陣列長度不足,沒有第二個組合了 31 if(s2 >= len){ 32 break; 33 } 34 35 //第二個組合長度小於第一個組合 36 if(e2 >= len){ 37 e2 = len - 1; 38 } 39 40 int left = start,right = e2; 41 42 int index = 0; 43 44 int size = right - left + 1; 45 int[] tmpArr = new int[size]; 46 47 while (s1 <= e1 && s2 <= e2){ 48 if(arr[s1] < arr[s2]){ 49 tmpArr[index] = arr[s1++]; 50 }else { 51 tmpArr[index] = arr[s2++]; 52 } 53 54 index++; 55 } 56 57 while (s1 <= e1){ 58 tmpArr[index++] = arr[s1++]; 59 } 60 61 while (s2 <= e2){ 62 tmpArr[index++] = arr[s2++]; 63 } 64 65 for (int i = left; i <= right ; i++) { 66 arr[i] = tmpArr[i - left]; 67 } 68 69 start = e2 + 1; 70 } 71 72 gap *= 2; 73 } 74 } 75 }View Code
連結串列的歸併排序實現
連結串列的資料結構
public class ListNode { int val; ListNode next; ListNode() { } ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }
連結串列跟陣列相比,不能用下標訪問,不知道長度,不能立即分割。
連結串列的賦值使用一個不儲存資料的頭節點,將資料往後插入。
跟陣列不一樣的是,連結串列需要真正的把節點一個個分割,在合併時再將節點連線起來。
自頂向下
不斷使用快慢指標得出連結串列中間節點,將整個連結串列進行分割,一直分割到每個連結串列都只有一個節點時再合併
程式碼實現
1 public class ListNode { 2 int val; 3 ListNode next; 4 5 ListNode() { 6 } 7 8 ListNode(int val) { 9 this.val = val; 10 } 11 12 ListNode(int val, ListNode next) { 13 this.val = val; 14 this.next = next; 15 } 16 17 18 public static void main(String[] args) { 19 //頭節點不儲存資料 20 ListNode tou = new ListNode(); 21 ListNode tmp = tou; 22 int[] arr = new int[]{44,8,33,5,1,9,2}; 23 24 //藉助臨時節點擴充連結串列 25 for (int i = 0; i < 7; i++) { 26 tmp.next = new ListNode(); 27 tmp = tmp.next; 28 tmp.val = arr[i]; 29 } 30 31 ListNode sortedHead = sortList(tou.next); 32 33 while (sortedHead != null){ 34 System.out.print(sortedHead.val + ","); 35 sortedHead = sortedHead.next; 36 } 37 } 38 39 public static ListNode sortList(ListNode head) { 40 41 if(head == null || head.next == null){ 42 return head; 43 } 44 45 ListNode fast = head,slow = head; 46 47 //快指標走的步數 = 慢指標 * 2 48 /* 49 連結串列長度為偶數時,快指標到倒數第二個節點,慢指標到中間兩個節點中的前一個 50 奇數時,快指標到最後一個節點,慢指標到中間節點 51 */ 52 //當快指標沒法走下去時,說明慢指標已經到達了連結串列的中間節點 53 while (fast.next != null && fast.next.next != null){ 54 slow = slow.next; 55 fast = fast.next.next; 56 } 57 //第二部分的開始節點 58 ListNode mid = slow.next; 59 //分割出第一部分 60 slow.next = null; 61 62 //當連結串列的節點數量超過一個時,繼續分割 63 if(head.next != null){ 64 head = sortList(head); 65 } 66 67 if(mid.next != null){ 68 mid = sortList(mid); 69 } 70 71 return merge(head,mid); 72 } 73 74 public static ListNode merge(ListNode l1,ListNode l2){ 75 76 ListNode pre = new ListNode(); 77 ListNode tou = pre; 78 while (l1 != null && l2 != null){ 79 pre.next = new ListNode(); 80 pre = pre.next; 81 82 if(l1.val > l2.val){ 83 pre.val = l2.val; 84 l2 = l2.next; 85 86 }else { 87 pre.val = l1.val; 88 l1 = l1.next; 89 } 90 } 91 //將剩餘的節點在後面插入 92 pre.next = l1 == null ? l2 : l1; 93 94 return tou.next; 95 } 96 }View Code
自底向上
要使用gap對連結串列分組,需要先計算連結串列的長度
與陣列一樣是兩層迴圈,第一層gap不斷倍增
第二層迴圈使用h作為不斷遍歷原連結串列的輔助節點,h1,h2確定兩個要合併的連結串列,i1,i2確定連結串列的長度,然後合併
程式碼實現
1 package node; 2 3 public class ListNode { 4 int val; 5 ListNode next; 6 7 ListNode() { 8 } 9 10 ListNode(int val) { 11 this.val = val; 12 } 13 14 ListNode(int val, ListNode next) { 15 this.val = val; 16 this.next = next; 17 } 18 19 20 public static void main(String[] args) { 21 //頭節點不儲存資料 22 ListNode tou = new ListNode(); 23 ListNode tmp = tou; 24 int[] arr = new int[]{44,8,33,5,1,9,2}; 25 26 //藉助臨時節點擴充連結串列 27 for (int i = 0; i < 7; i++) { 28 tmp.next = new ListNode(); 29 tmp = tmp.next; 30 tmp.val = arr[i]; 31 } 32 33 34 ListNode sortedHead = merge1(tou.next); 35 36 while (sortedHead != null){ 37 System.out.print(sortedHead.val + ","); 38 sortedHead = sortedHead.next; 39 } 40 } 41 42 //自底向上的歸併排序 43 public static ListNode merge1(ListNode head){ 44 45 int len = 0 , gap = 1; 46 ListNode tmp = head; 47 while (tmp != null){ 48 tmp = tmp.next; 49 len++; 50 } 51 52 ListNode pre,h,h1,h2 ; 53 ListNode tou = new ListNode(); 54 tou.next = head; 55 56 /* 57 每次迴圈指定一個頭節點,從該節點開始根據gap去獲取要比較的兩部分的頭節點 58 將兩個部分的節點按順序加入該節點 59 該節點指向還沒排序的後續節點 60 */ 61 while (gap < len){ 62 pre = tou; 63 64 h = pre.next; 65 while (h != null){ 66 int i1 = gap; 67 h1 = h; 68 while (i1 > 0 && h != null){ 69 i1--; 70 h = h.next; 71 } 72 //第一部分已經到連結串列的終點 73 if(i1 > 0){ 74 break; 75 } 76 h2 = h; 77 int i2 = gap; 78 while (i2 > 0 && h != null){ 79 i2--; 80 h = h.next; 81 } 82 83 int l1 = gap; 84 //第二部分的長度 85 int l2 = gap - i2; 86 87 /* 88 不用新建立節點的方式 89 而是將現有節點插入到頭節點後面 90 */ 91 while (l1 > 0 && l2 > 0){ 92 if(h1.val > h2.val){ 93 pre.next = h2; 94 h2 = h2.next; 95 l2--; 96 }else { 97 pre.next = h1; 98 h1 = h1.next; 99 l1--; 100 } 101 pre = pre.next; 102 } 103 104 pre.next = l1 == 0 ? h2 : h1; 105 //需要遍歷完已經合併的兩個連結串列,才能合併的後續連結串列 106 while (l1 > 0 || l2 > 0){ 107 pre = pre.next; 108 l1--; 109 l2--; 110 } 111 //將未排序的連結串列接在後面 112 pre.next = h; 113 } 114 115 gap *= 2; 116 } 117 return tou.next; 118 } 119 }View Code
圖