本文章包括全部基本排序算法(和其中一些算法的改進算法):
直接插入排序、希爾排序、直接選擇排序、堆排序、冒泡排序、快速排序、歸併排序、基數排序。html
一個插入排序是另外一種簡單排序,它的思路是:每次從未排好的序列中選出第一個元素插入到已排好的序列中。
它的算法步驟能夠大體概括以下:git
所以,從上面的描述中咱們能夠發現,直接插入排序能夠用兩個循環完成:
第一層循環:遍歷待比較的全部數組元素
第二層循環:將本輪選擇的元素(selected)與已經排好序的元素(ordered)相比較。
若是:selected > ordered,那麼將兩者交換算法
#include <stdio.h> void InsertSort(int *a, int len) { int i, j, tmp; for(i=1; i<len; i++){ //i指向無序區第一個元素 tmp = a[i]; j = i - 1; //j指向有序去第一個元素 // j往前遍歷,找到比a[i]小的,插入到此處。比a[i]大的後移 while(j>=0 && tmp<a[j]){ //小於號換成大於號則是從大到小排序 a[j+1] = a[j]; j--; } a[j+1] = tmp; //插入到空出來的位置 } return; } int main() { int a[] = {1,3,63,5,78,9,12,52,8}; int n = sizeof(a)/sizeof(int),i; InsertionSort(a, n); for(i=0; i<n; i++) printf("%d ", a[i]); return 0; }
與直接插入算法的區別在於:在有序表中尋找待排序數據的正確位置時,使用了折半查找/二分查找。
減小了比較的次數,但沒有減小插入的次數。時間複雜度仍爲O(n^2),但比直接插入排序稍微快一點。shell
void BinaryInsertSort(int *a, int len) { int i, j, low, high, tmp; for(i=1; i<len; i++){ tmp = a[i]; j = i - 1; low = 0; high = i - 1; while(low<=high){ int mid = (low + high) / 2; if(tmp>a[mid]) low = mid + 1; else high = mid - 1 ; } while(j>=low && tmp<a[j]){ //小於號換成大於號則是從大到小排序 a[j+1] = a[j]; j--; } a[j+1] = tmp; } return; }
希爾排序又叫縮小增量排序數組
基於直接插入排序,基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄「基本有序」時,再對全體記錄進行依次直接插入排序。數據結構
希爾的思想也很簡單就是一個h-sort的插入算法——每相鄰h個元素進行插入排序
若是h比較大,那麼子數組會很小,用插入效率高
若是h很小,這時候數組基本有序,插入效率也很高ide
void ShellSort(int arr[], int len) { int step; int i, j; int tmp; for(step = len/2; step > 0; step = step/2) // 比直接插入排序多了一層循環 { for(i = step; i < len; i++) // 直接插入排序能夠當作step爲1的希爾排序。把這裏的Step都替換成1,就是直接插入排序 { j = i - step; tmp = arr[i]; while(j>=0 && tmp<arr[j]) { arr[j+step] = arr[j]; j = j - step; } arr[j+step] = tmp; } } return; }
排序思路是:每次從未排序的序列中選出一個最小值,並把它放在已排好序的序列的序尾。這樣就造成了一個有序序列(從小到大)。
簡單選擇排序的基本思想:找到最小值 + 交換。函數
第一趟,從n 個記錄中找出關鍵碼最小的記錄與第一個記錄交換;
第二趟,從第二個記錄開始的 n-1 個記錄中再選出關鍵碼最小的記錄與第二個記錄交換;
以此類推.....
第 i 趟,則從第 i 個記錄開始的 n-i+1 個記錄中選出關鍵碼最小的記錄與第 i 個記錄交換,
直到整個序列按關鍵碼有序。性能
void SelectSort(int *a, int len) { int i, j; int key, tmp; for(i=0; i<len; i++) { key = i; // 記錄最小值的下標,初始值爲i,即有序序列的最後一個元素 for(j=i+1; j<len; j++) // j從i的下一個元素開始遍歷,即無序序列的第一個元素 { if(a[j]<a[key]) // 若是找到比當前key上的值小的值,則替換key值 key = j; } if(key!=i) // key == i說明 無序序列中沒有比a[i]更小的數了,沒必要替換 { tmp = a[i]; // 替換a[i]和無序序列中最小的元素a[key] a[i] = a[key]; a[key] = tmp; } } return; }
堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。實際上也是一種選擇排序,只不過採用了堆這種數據結構,利用這種數據結構,使得在每次查找最大元素時,直接選取堆頂元素,從而時間大大縮短,相對簡單選擇排序算法來講,比較高效。優化
將初始待排序關鍵字序列(A0, A1, A2 .... An-1)構建成大頂堆(從最後一個非葉子結點 i = len/2 - 1 自下而上),此堆爲初始的無序區(構建堆)
將堆頂元素A[0]與最後一個元素A[n-1]交換,此時獲得新的無序區(A0, A1, A2,......An-2)和新的有序區(An-1) (交換首尾元素)
因爲交換後新的堆頂A[0]可能違反堆的性質,所以須要對當前無序區(A0,A1,......An-2)從新調整爲新的大頂堆(從根節點 i = 0,自上而下) (調整堆)
而後再次將A[0]與無序區最後一個元素交換,獲得新的無序區(A0,A1....Rn-3)和新的有序區(An-2,An-1)。(不斷循環)
不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成
// 下標從0開始,左孩子是2start+1; 若是從1開始,是2start
void HeapAdjust(int *A, int start, int end) { int k; int tmp = A[start]; for(k=2*start+1; k<=end; k=k*2+1) // k=k*2+1 是取左孩子 { if(k<end && A[k]<A[k+1]) // 比較左右孩子,選較大者 k++; if(tmp<A[k]) // 比較父節點和左右孩子中較大者,若是孩子較大,將孩子節點值賦值給父節點,start指向孩子節點的位置 { A[start] = A[k]; start = k; } else break; } A[start] = tmp; // 將一開始的start的值賦給此時start指向的節點,即將原來不和諧的元素放到合適的位置上 } void HeapSort(int *A, int len) { int i, j; for(i=len/2-1; i>=0; i--) //從最後一個非葉子結點i(len/2-1)從下至上,從右至左調整結構,構建初始堆 { HeapAdjust(A, i, len); } for(j=len-1; j>=0; j--) // 將堆頂元素和末尾元素互換,再將剩下的j-1個元素調整爲大頂堆 { int tmp = A[j]; A[j] = A[0]; A[0] = tmp; HeapAdjust(A, 0, j-1); // 重建堆是從頂至下,與初始堆相反 } }
http://www.javashuo.com/article/p-csojmyyk-gg.html 堆排序詳解
http://www.cnblogs.com/mengdd/archive/2012/11/30/2796845.html 堆排序 Heap Sort
冒泡排序是一種相對簡單的排序,它每次比較相鄰的兩個元素,若是前者大於後者,則交換< swap >這兩個元素(從小到大排序),這樣每一趟比較就把大的元素沉入最後,形象的稱之爲「冒泡」,每走一趟,實際上最尾的元素已經排好。
將序列當中的左右元素,依次比較,保證右邊的元素始終大於左邊的元素;( 第一輪結束後,序列最後一個元素必定是當前序列的最大值;)
對序列當中剩下的n-1個元素再次執行步驟1。
對於長度爲n的序列,一共須要執行n-1輪比較。(利用while循環能夠減小執行次數)
void BubbleSort(int *A, int len) { int i, j; int tmp; for(i=0; i<len-1; i++) // 長度爲len的數組,最多須要len-1次冒泡,便可有序(剩下一個自動變爲最大或最小) { for(j=0; j<len-i-1; j++) // 注意:每次循環比較len-1-i次,若是寫成len-1,多了一次數組會越界,產生不可預料的結果 { if(A[j]>A[j+1]) { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; } } } }
對冒泡排序常見的改進方法是加入一標誌性變量exchange,用於標誌某一趟排序過程當中是否有數據交換,若是進行某一趟排序時並無進行數據交換,則說明數據已經按要求排列好,可當即結束排序,避免沒必要要的比較過程。
// 改進一,加入標誌位 void BubbleSort2(int *A, int len) { int i, j; int tmp; int flag; // flag=1時表示已經排序完成 for(i=0; i<len-1; i++) { flag = 1; // 每次冒泡前把flag置1 for(j=0; j<len-i-1; j++) { if(A[j]>A[j+1]) { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; flag = 0; // 若是發生交換,置0,說明尚未排序完成 } } if(flag==1) // 若是一次冒泡循環中一次交換也沒發生,則flag仍是爲1,說明數組已經排好序 break; } }
在冒泡排序的每趟掃描中,記住最後一次交換髮生的位置lastexchange也能有所幫助。由於該位置以前的相鄰記錄已經有序,故下一趟排序開始的時候,0到lastexchange已是有序的了,lastexchange到n-1是無序區。因此一趟排序可能使當前有序區擴充多個記錄.即較大縮小無序區範圍,而非遞減1,以此減小排序趟數。這種算法以下:
// 改進二,記錄最後一次交換的位置 void BubbleSort3(int *A, int len) { int j; int tmp; int flag = len - 1; int pos; while(flag>0) { pos = 0; // 不要漏了該條賦值語句。不然會陷入無限循環。 for(j=0; j<flag; j++) { if(A[j]>A[j+1]) // 若是沒有交換,則不會進入該if分支,pos仍是=0,說明已經排好序,while循環纔會結束 { tmp = A[j]; A[j] = A[j+1]; A[j+1] = tmp; pos = j; // 記錄最後一次交換的位置 } } flag = pos; // 把位置賦給flag,用於判斷無序區是否還有元素 } }
傳統冒泡排序中每一趟排序操做只能找到一個最大值或最小值,咱們考慮利用在每趟排序中進行正向和反向兩遍冒泡的方法一次能夠獲得兩個最終值(最大者和最小者) , 從而使排序趟數幾乎減小了一半。
// 改進三,雙向冒泡 void BubbleSort4(int *A, int len) { int low=0, high=len-1; int i; int tmp; while(low<high) { for(i=low; i<high; i++) // 正向冒泡,找到最大者 { if(A[i]>A[i+1]) { tmp = A[i]; A[i] = A[i+1]; A[i+1] = tmp; } } high--; // 修改high值, 前移一位 for(i=high; i>low; i--) // 反向冒泡,找到最小者 { if(A[i]<A[i-1]) { tmp = A[i]; A[i] = A[i-1]; A[i-1] = tmp; } } low++; // 修改low值,後移一位 } }
快速排序(Quicksort)是對冒泡排序的一種改進。
關於快速排序,它的基本思想就是選取一個基準,一趟排序肯定兩個區間,一個區間所有比基準值小,另外一個區間所有比基準值大,接着再選取一個基準值來進行排序,以此類推,最後獲得一個有序的數列。
1.選取基準值,經過不一樣的方式挑選出基準值。
2.用分治的思想進行分割,經過該基準值在序列中的位置,將序列分紅兩個區間,在準值左邊的區間裏的數都比基準值小(默認以升序排序),在基準值右邊的區間裏的數都比基準值大。
3.遞歸調用快速排序的函數對兩個區間再進行上兩步操做,直到調用的區間爲空或是隻有一個數。
// 基本雙向快速排序 void QuickSort(int *A, int start, int end) { if(start<end){ // 調試時少了這一步,一直報錯 int i=start, j=end; int pivot = A[i]; // 第0個元素做爲基準數 while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基準數歸位,i左邊爲較小數,右邊爲較大數 QuickSort(A, start, i-1); // 遞歸調用,將剩下兩部分繼續進行快排 QuickSort(A, i+1, end); } }
上面版本的快排在選取主元的時候,每次都選取第一個元素。當序列爲有序時,會發現劃分出來的兩個子序列一個裏面沒有元素,而另外一個則只比原來少一個元素。爲了不這種狀況,引入一個隨機化量來破壞這種有序狀態。
// 隨機選取基準數,若是序列基本有序,能夠避免分治以後一個區間元素過少,一個區間元素過多的狀況 void QuickSort2(int *A, int start, int end) { if(start<end){ int i=start, j=end; int pivot_pos = rand() % (end - start) + start; // 從start~end間隨機選取一個元素做爲基準數 int pivot = A[pivot_pos]; // 開闢一塊內存存儲pivot值 SWAP(&A[pivot_pos], &A[start]); // 交換pivot_pos和start位置上的值,不會影響到上面pivot的值 while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基準數歸位,i左邊爲較小數,右邊爲較大數 QuickSort2(A, start, i-1); // 遞歸調用,將剩下兩部分繼續進行快排 QuickSort2(A, i+1, end); } }
可是隨機函數自己也要消耗必定的時間,並且隨機選取也有可能出現很差分割的概率,因此又提出了三數取中法,即取左端、右端和中間三個元素排序後取中間的數做爲關鍵元素。
// 三數取中選取基準數,選取序列頭尾還有中間的三個數,取三個中值在中間的元素做爲基準數 void QuickSort3(int *A, int start, int end) { if(start<end){ int mid = (start + end) / 2; if(A[mid]>A[end]) SWAP(&A[mid], &A[end]); if(A[start]>A[end]) SWAP(&A[start], &A[end]); if(A[start]<A[mid]) SWAP(&A[start], &A[mid]); //經過以上三步能夠找到中間值,存放在A[start]中 int i=start, j=end; int pivot = A[start]; while(i<j) { while(i<j && A[j]>pivot) j--; A[i] = A[j]; while(i<j && A[i]<pivot) i++; A[j] = A[i]; } A[i] = pivot; // 基準數歸位,i左邊爲較小數,右邊爲較大數 QuickSort3(A, start, i-1); // 遞歸調用,將剩下兩部分繼續進行快排 QuickSort3(A, i+1, end); return; } else return; }
緣由:對於很小和部分有序的數組,快排不如插排好。當待排序序列的長度分割到必定大小後,繼續分割的效率比插入排序要差,此時可使用插排而不是快排
截止範圍:待排序序列長度N = 10,雖然在5~20之間任一截止範圍都有可能產生相似的結果。這種作法也避免了一些有害的退化情形。
if (start - end + 1 < 10) { InsertSort(A, start, end); return; } //else時,正常執行快排
其實這種優化編譯器會本身優化,相比不使用優化的方法,時間幾乎沒有減小。
QuickSort函數在其尾部有兩次遞歸操做。若是待排序的序列劃分極端不平衡,遞歸的深度將趨近於n,而不是平衡時的logn。
// 尾遞歸,減小遞歸深度 int Partition(int *A, int start, int end) { int pivot = A[start]; while(start<end) { while(start<end && A[end]>pivot) end--; SWAP(&A[end], &A[start]); while(start<end && A[start]<pivot) start++; SWAP(&A[start], &A[end]); } return start; } void QuickSortTail(int *A, int start, int end) { /* 普通遞歸方式,start和end這兩個局部變量在下一次函數調用中還須要使用,因此須要繼續堆棧 if(start<end) { int pivot_pos = Partition(A, start, end); QuickSort(A, start, pivot_pos-1); QuickSort(A, pivot_pos+1, end); } */ while(start<end) { int pivot_pos = Partition(A, start, end); if(pivot_pos-start < end-pivot_pos) // 短的部分採用遞歸,能夠有效減小遞歸深度 { QuickSortTail(A, start, pivot_pos-1); // 左半部分繼續遞歸 start = pivot_pos + 1; } else { QuickSortTail(A, pivot_pos+1, end); // 右半部分繼續遞歸 end = pivot_pos - 1; } } }
歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。
分爲兩個函數實現:
一、分:能夠看到這種結構很像一棵徹底二叉樹,本文的歸併排序咱們採用遞歸去實現(也可採用迭代的方式去實現)。分階段能夠理解爲就是遞歸拆分子序列的過程,遞歸深度爲log2n。
二、並:而後考慮下如何將將二個有序數列合併。這個很是簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了後就在對應數列中刪除這個數。而後再進行比較,若是有數列爲空,那直接將另外一個數列的數據依次取出便可。
// 把兩個有序數組合併成一個有序數組 void MergeArray(int *A, int len_A, int *B, int len_B) { int i, j, k; i = j = k = 0; int temp[len_A + len_B]; while(i<len_A && j<len_B) //取兩個數組第一個元素比較,小的存入temp中 { if(A[i]<B[j]) temp[k++] = A[i++]; else temp[k++] = B[j++]; } while(i<len_A) //兩個數組中剩餘的元素必定是偏大的,直接存入temp中 { temp[k++] = A[i++]; } while(j<len_B) { temp[k++] = B[j++]; } for(i=0; i<(len_A+len_B); i++) A[i] = temp[i]; //temp再轉存入A數組中 } //遞歸實現歸併排序 void MergeSort1(int *A, int len) { if(len>1) { // 數組分紅兩半 int *list1 = A; int list1_len = len/2; int *list2 = A + len/2; int list2_len = len - list1_len; MergeSort1(list1, list1_len); //左邊部分遞歸 MergeSort1(list2, list2_len); //右邊部分遞歸 MergeArray(list1, list1_len, list2, list2_len); //合併兩個有序數組 } }
非遞歸的方法,避免了遞歸時深度爲log2N的棧空間,空間只是用到歸併臨時申請的跟原來數組同樣大小的空間,而且在時間性能上也有必定的提高,所以,使用歸併排序是,儘可能考慮用非遞歸的方法。
不管是基於遞歸仍是循環的歸併排序, 它們調用的核心方法都是相同的:完成一趟合併的算法,即兩個已經有序的數組序列合併成一個更大的有序數組序列 (前提是兩個原序列都是有序的)
//迭代實現歸併排序 void MergeSort2(int *A, int len) { int left_min, left_max, right_min, right_max; int i, next; int *temp = (int *)malloc(len*sizeof(int)); if(temp == -1) return; for(i=1; i<len; i*=2) //步長從1開始,以2的倍數遞增 { for(left_min=0; left_min<len-i; left_min=right_max) // 每次循環結束後,把letf_min指向right_max;right_max指向right_min+i; { right_min = left_max = left_min + i; right_max = right_min + i; if(right_max>len) // right_max 最大爲len,若是超出,則等於len { right_max = len; } next = 0; while(left_min<left_max && right_min<right_max) // 合併[left_min, left_max]和[right_min, right_max]兩個區間的數組 { if(A[left_min]<A[right_min]) { temp[next++] = A[left_min++]; } else { temp[next++] = A[right_min++]; } } while(left_min<left_max) // 若是是左邊的元素還有剩餘,則把它們移動到right數組的最右邊;若是是右邊的數組元素還有剩餘,則不用動 { A[--right_min] = A[--left_max]; } while(next>0) // 再把temp數組填充到left和right數組中 { A[--right_min] = temp[--next]; } } } }
前面全部的排序算法都存在比較,均可以稱爲」比較排序「。比較排序的下界爲o(nlogn)。那麼有沒有時間複雜度爲o(n)的線性時間排序算法呢?計數排序即是很基礎的一種線性時間排序,它是基數排序的基礎。基本思想是:對每個元素x,肯定小於x的元素個數,就能夠把x直接放到它在有序序列中的位置上。過程描述:假設待排序序列a中值的範圍[0,k],其中k表示待排序序列中的最大值。首先用一個輔助數組count記錄各個值在a中出現的次數,好比count[i]表示i在a中的個數。而後依次改變count中元素值,使count[i]表示a中不大於i的元素個數。而後從後往前掃描a數組,a中的元素根據count中的信息直接放到輔助數組b中。最後把有序序列b複製到a。
計數排序是穩定的排序算法;平均時間複雜度、最優時間複雜度和最差時間複雜度都爲O(n+k),空間複雜度爲O(n+k),其中,n爲待排元素個數,k爲待排元素的範圍(0~k)。
// 計數排序 void CountSort(int *A, int len) { int min = A[0]; int max = A[0]; int i = 0; //找出數組中的最大值和最小值,肯定哈希表的大小 for(i=0; i<len; i++) { if(A[i]<min) min = A[i]; if(A[i]>max) max = A[i]; } int count_size = max - min + 1; int *count_arr = (int *)malloc(sizeof(int) * count_size); // 建立一個用於計數的數組 int *temp = (int *)malloc(sizeof(int) * len); for(i=0; i<count_size; i++) count_arr[i] = 0; // 計數數組所有初始化爲0 for(i=0; i<len; i++) count_arr[A[i]-min]++; // 統計數組A中各元素出現的次數,並放在對應的位置上 for(i=1; i<count_size; i++) count_arr[i] += count_arr[i-1]; // 儲存本身數組下標數值在目標數組對應的位置,保證穩定性 for(i=0; i<count_size; i++){ //printf("%d, ", count_arr[i]); } //printf("\n"); for(i=len-1; i>=0; i--) { temp[count_arr[A[i]-min]-1] = A[i]; //將原數組按大小順序儲存到另外一個數組 count_arr[A[i]-min]--; //temp[--count_arr[A[i] - min]] = A[i]; } for(i=0; i<len; i++) A[i] = temp[i]; free(count_arr); free(temp); }
不過該計數排序不能對負數進行排序,若是須要對負數進行排序,須要進行改進(先一次遍歷待排數組,找出負數中的最大值和最小值,正數中的最大值和最小值,建立兩個計數數組,negativeCountArray用來統計待排數組中各個不一樣負數的出現個數,positiveCountArray用來統計待排數組中各個正數出現的個數,先統計總的負數的個數,而後再統計各個不一樣正數的個數,而後在往sortedArray中放元素的時候對正數和負數區別對待。)
基數排序的基本思想是:一共有10個"桶",表明各個數位爲0~9.在每一個桶上,組織一個優先隊列,對於輸入的一系列正整數和0,按照個位的大小關係分別進入10個"桶"中.而後遍歷每一個"桶",按照十位的大小關係進行調整,緊接着是百位,千位.......直到到達最大數的最大位數。
基數排序只是針對於數字,思想就是將咱們須要待排列的元素按照指定的進制將每一位排列,時間複雜度爲:P(N+B),注:其中P爲待排列數字的最大位數,N爲待排序列的長度,B爲進制數。
//基數排序 void RadixSort(int *A, int len) { int max_digit = 0; int i, j; int radix = 10; //求數組中的最大位數 for (i = 0; i < len; ++i) { while (A[i] > (pow(10, max_digit))) { max_digit++; } } int flag = 1; for(j=1; j<=max_digit; j++) { //創建數組 統計每一個位上不一樣數字出現的次數 int digit_count_arr[10] = { 0 }; for(i=0; i<len; i++) digit_count_arr[(A[i] / flag) % radix]++; //創建數組 統計A[i]在temp中的起始下標 int BeginIndex[10] = { 0 }; for(i=1; i<radix; i++) BeginIndex[i] = BeginIndex[i - 1] + digit_count_arr[i - 1]; //創建臨時數組 用於收集數據 int *tmp = (int *)malloc(sizeof(int) * len); //初始化 for(i=0; i<len; i++) tmp[i] = 0; //將數據寫入臨時數組 for(i=0; i<len; i++) { int index = (A[i] / flag) % 10; tmp[BeginIndex[index]++] = A[i]; // 寫入一個同時要把該位置上的計數加1,由於同一個位置可能有多個元素 } //將數據從新寫回原數組 for(i=0; i<len; i++) A[i] = tmp[i]; flag = flag * 10; // 用於下一位的排序 free(tmp); } }
資料
http://www.javashuo.com/article/p-urxrwdjw-hv.html
經常使用七大經典排序算法總結(C語言描述)
http://blog.csdn.net/gl486546/article/details/53053069
排序算法之希爾排序
https://www.jianshu.com/p/7d037c332a9d
數據結構常見的八大排序算法(詳細整理)
http://blog.csdn.net/zhangjikuan/article/details/49095533
九大排序算法-C語言實現及詳解
https://www.jianshu.com/p/6777a3297e36
快速排序的優化 和關於遞歸的問題,說說個人想法
http://www.cnblogs.com/Anker/archive/2013/03/04/2943498.html
遞歸與尾遞歸總結
http://blog.csdn.net/touch_2011/article/details/6785881 漫談經典排序算法:4、歸併排序(合併排序)(推薦)