歸併排序——史上最詳細圖解教程!!!
題目大意:把n個數,分成若干份,然後每一份暴力排序一下,然後遞迴地合起來。
為什麼要這樣做?這樣有個球用?
核心問題就在於,每兩份之間你是怎麼合起來的。
我們舉個例子。
一個比較呆萌的思路就是,2二分插入,形成新的序列,再繼續用4插入。。。這樣的話,確實沒什麼球用。
正確思路:2在1,3中插入以後,記下插入的這個位置的index,然後下一次,就是從3開始找了,然後4插入,接下來就是從5開始找了。。。請注意哦,這個情況已經是最複雜的情況了哦!另外兩種極端情況,找遍前面的陣列都沒找到,直接在最後一股腦加進去。或者說後面的數都沒前面的某個數大,一股腦加進去。這兩種極端情況的時間複雜度是O(N)。中間會略高一點點,最差情況O(n^2),注意,是所有數都插進去的時間複雜度,可以說是很快很快很快了。
弄明白上面歸併排序的核心思想,就可以開始動手了!
接下來我們需要分析總體的思路。
1.拆分n個數為根號n組(後面會分析具體怎麼分成這個奇怪的份數的)
2.給每個組進行排序
3.把當前所有的組,兩兩合併,如果發現合併以後,剩下的組數並沒有達到1,那就繼續合併,直到合併到1組為止。
4.合併的過程又要細化,要單獨寫一個兩兩合併的方法。
5.兩兩合併的過程又要細化,要單獨寫一個方法:單個值併到陣列中,返回一個標記,下次從這個標記開始找。
所以我們用虛擬碼串聯起這個程式
// TODO: 2017/11/29 把一個數組分成根號n份 // TODO: 2017/11/29 把每一份都排好序,存入List<List<Integer>>中(二維陣列也可以) // TODO: 2017/11/29 寫一個遞迴方法,功能是把List<List<Integer>>中的List合併成一個 // TODO: 2017/11/29 寫一個兩個List<Integer>之間合併的方法,後一個List<Integer>賦給前一個 // TODO: 2017/11/29 寫一個一個值在List<Integer>插入的方法,返回index記錄下次開始的位置
這樣才是正確的做法,上來就想啃掉這整個流程肯定不現實,把他拆分成一個個模組,不僅容易成功,出現bug的時候還便於debug。
拆分這個陣列成根號n段
其思維是這樣的,假設是5,根號5向下取整就是2,那就要分成兩份,一份2,一份3。
再舉一個11的例子,根號11向下取整就是3,那就要分成3份,3,3,5。
int len = a.length;//陣列長度 int k = (int) Math.sqrt(len);//根號後向下取整的值,即份數 int num = a.length / k;//把最後一份區別對待,前面每份的個數
把每一份都排好序賦值(挺複雜的,需要好好理解下的 )
for (int row = 0; row < k - 1; row ++) {//0 1 List<Integer> list = new ArrayList<>(); for (int col = 0; col < num; col ++) { //index 是你開始點,其實就是col的變形 int index = row * num + col;//0 1 2//3 4 5 int min = Integer.MAX_VALUE; int q = -1; for (int i = index; i < row * num + num; i ++) { if (a[i] < min) { min = a[i]; q = i; } } if (q != -1) { int t = a[q]; a[q] = a[index]; a[index] = t; } list.add(a[index]); } this.list.add(list); }
遞迴,奇陣列跟偶陣列區別對待。每兩組一起處理,後一組加到前一組中,刪除後一組。直到合併到只有一組。
void circle(List<List<Integer>> list) { int size = list.size();//取得你有幾組數 if (size == 1) { return; } if (size % 2 == 0) {//如果你有2 4 6組數,假設4組 for (int i = 0; i < size; i += 2) {//2組併到1組中,4組併到3組中 gather(list.get(i), list.get(i + 1)); } int f = 0; for (int i = 1; i < size; i += 2) {//把1 3 兩組刪除 list.remove(i - f); f ++; } } else {//如果你有5組數 int newSize = size - 1;//只處理前4組數 for (int i = 0; i < newSize; i += 2) { gather(list.get(i), list.get(i + 1));//2組併到1組中,4組併到3組中 } int f = 0; for (int i = 1; i < newSize; i += 2) {//把1 3 兩組刪除 list.remove(i); f ++; } } circle(list); }把後面的併到前面的集合中,把後面的集合每個點都插入進去,更新搜尋區間
//把b併到a中 void gather(List<Integer> a, List<Integer> b) { int aSize = a.size(); int bSize = b.size(); int start = 0;//取得a的第一個 int end = aSize - 1;//取得a的最後一個 for (int i = 0; i < bSize; i ++) { int num = b.get(i);//把每個b取一遍 start = insert(a, start, end, num);//把這個數在a的可插區間內插入,把插入點返回 end = a.size() - 1;//插入成功,結束點也要變化了 //如果迴圈完了也沒找到大於等於n的數,上面已經是插入在最後了 if (start == 999) { if (i + 1 < bSize) {//看看下一個數是否還在區間內 for (int j = i + 1; j < bSize; j ++) {//如果在 a.add(b.get(j));//全加進去 } } break; } } }如果找得到那就返回index,找不到就一股腦全加在後面
int insert(List<Integer> k, int start, int end, int n) { for (int i = start; i <= end; i ++) {//給定搜尋區間 int num = k.get(i);//取得a中的值 if (n <= num) { k.add(i, n); return i; } } k.add(n); return 999; }
全部程式碼
import java.util.ArrayList; import java.util.List; public class Test { List<List<Integer>> list = new ArrayList<>(); Test(int[] a) { int len = a.length;//11 int k = (int) Math.sqrt(len);//3 int num = a.length / k;//3 for (int row = 0; row < k - 1; row ++) {//0 1 List<Integer> list = new ArrayList<>(); for (int col = 0; col < num; col ++) { //index 是你開始點,其實就是col的變形 int index = row * num + col;//0 1 2//3 4 5 int min = Integer.MAX_VALUE; int q = -1; for (int i = index; i < row * num + num; i ++) { if (a[i] < min) { min = a[i]; q = i; } } if (q != -1) { int t = a[q]; a[q] = a[index]; a[index] = t; } list.add(a[index]); } this.list.add(list); } List<Integer> list = new ArrayList<>(); for (int i = (k - 1) * num; i < len; i ++) { int min = Integer.MAX_VALUE; int index = -1; for (int j = i; j < len; j ++) { if (a[j] < min) { min = a[j]; index = j; } } if (index != -1) { int t = a[i]; a[i] = a[index]; a[index] = t; } list.add(a[i]); } this.list.add(list); circle(this.list); for (int i = 0; i < this.list.size(); i ++) { List<Integer> mList = this.list.get(i); for (int j = 0; j < mList.size(); j ++) { System.out.print(mList.get(j) + " "); } System.out.println(); } } // TODO: 2017/11/29 心得 可以寫的蠢一點 效能差一點 後期再優化 void circle(List<List<Integer>> list) { int size = list.size();//取得你有幾組數 if (size == 1) { return; } if (size % 2 == 0) {//如果你有2 4 6組數,假設4組 for (int i = 0; i < size; i += 2) {//2組併到1組中,4組併到3組中 gather(list.get(i), list.get(i + 1)); } int f = 0; for (int i = 1; i < size; i += 2) {//把1 3 兩組刪除 list.remove(i - f); f ++; } } else {//如果你有5組數 int newSize = size - 1;//只處理前4組數 for (int i = 0; i < newSize; i += 2) { gather(list.get(i), list.get(i + 1));//2組併到1組中,4組併到3組中 } int f = 0; for (int i = 1; i < newSize; i += 2) {//把1 3 兩組刪除 list.remove(i); f ++; } } circle(list); } //把b併到a中 void gather(List<Integer> a, List<Integer> b) { int aSize = a.size(); int bSize = b.size(); int start = 0;//取得a的第一個 int end = aSize - 1;//取得a的最後一個 for (int i = 0; i < bSize; i ++) { int num = b.get(i);//把每個b取一遍 start = insert(a, start, end, num);//把這個數在a的可插區間內插入,把插入點返回 end = a.size() - 1;//插入成功,結束點也要變化了 //如果迴圈完了也沒找到大於等於n的數,上面已經是插入在最後了 if (start == 999) { if (i + 1 < bSize) {//看看下一個數是否還在區間內 for (int j = i + 1; j < bSize; j ++) {//如果在 a.add(b.get(j));//全加進去 } } break; } } } int insert(List<Integer> k, int start, int end, int n) { for (int i = start; i <= end; i ++) {//給定搜尋區間 int num = k.get(i);//取得a中的值 if (n <= num) { k.add(i, n); return i; } } k.add(n); return 999; } public static void main(String[] args) throws Exception { int[] a = new int[17]; a[0] = 7; a[1] = 10; a[2] = 8; a[3] = 11; a[4] = 50; a[5] = 9; a[6] = 6; a[7] = 70; a[8] = 1; a[9] = 4; a[10] = 34; a[11] = 34; a[12] = 4; a[13] = 4; a[14] = 4; a[15] = 4; a[16] = 4; Test test = new Test(a); } } // TODO: 2017/11/29 把一個數組分成根號n份 // TODO: 2017/11/29 把每一份都排好序,存入List<List<Integer>>中(二維陣列也可以) // TODO: 2017/11/29 寫一個遞迴方法,功能是把List<List<Integer>>中的List合併成一個 // TODO: 2017/11/29 寫一個兩個List<Integer>之間合併的方法,後一個List<Integer>賦給前一個 // TODO: 2017/11/29 寫一個一個值在List<Integer>插入的方法,返回index記錄下次開始的位置