歸併排序:陣列和連結串列的多種實現

划水的魚dm發表於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));
14 
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 }
View Code

 

 

 自底向上

自底向上沒有使用遞迴的方式,而是使用迴圈修改陣列

首先設定一個分割值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

 

 

相關文章