核心思想:將一個數組進行排序,可以先(遞歸)將他分成兩半分別排序,然后把結果合并起來.若將兩個有序表合并成一個有序表,稱為二路歸并
即:將數組分成兩部分,左邊部分,和右邊部分分別遞歸再次分成兩部分知道每個數組里面只有一個元素.然后調用合并函數,將兩個數組進行排好序.每次排序的時候會使用同一個臨時緩存,儲存排好序的元素.最后的排序結果是 依次把 左半邊的 數組 兩兩排序合并,遞歸再把合并的數組繼續兩兩合并,最后左邊的數組全部合并;接下來對右半部分的數組兩兩排序合并,重復左邊的類似動作,把右邊的數組排序成一個數組;最后進行左右兩個有序數組合并,組成最終有序數組.
歸并排序的時間復雜度是NlogN,缺點是所需要的額外輔助空間和N成正比(需要分配一個與N長度一致的臨時存儲空間).
2.2.1原地歸并的抽象方法:
因為考慮到直接歸并算法每次合并需使用一個臨時空間,所以考慮在原始數組上直接進行排序,先對前半部分排序,在對后半部分排序.但是相對二路歸并的復雜度高.
2.2.2二路歸并具體的算法思路:(是一種自頂向下的mergesort)
1.判斷需要進入遞歸操作的邊界值.(一般 left<right)
2.把數組分成兩半. Mid =(left+right)/2
3.進行左半邊數組遞歸調用.直到 left=right ,不在遞歸.此時提取出數組的第一個元素
4.進行右半邊數組遞歸調用.
5.調用merge方法,把兩個有序數組合并.合并的時候會使用一個臨時數組.合并的思路是從兩個有序的數組第一個元素開始,依次比較,把數據按從小到大的順序存放到臨時temp;遇到某一個數組完成了,剩下一個數組還沒比較完,則把剩下的數組復制到temp尾部.最后把temp數組讀取,替換實際數組中.
步驟如下圖:
要把a[]分成a[lo...hi]兩部分,進行分別對 a[lo...mid] ,a[mid+1...hi]進行遞歸調用,直到調用的數組lo和hi相同時跳出,繼而進行和右邊部分合并排序.
代碼如下:
public class Mergesort{
public static int [] temp;
public void sort(int[] arr){
if(arr==null||arr.lenth==0){
return;}
temp=new int[arr.lenth];
mergesort(arr,0,arr.lenth-1);
}
private void mergsort(int[] arr,int low,int high){
if(low>=high){return;}
int mid=(low+high)/2;
mergesort(arr,low,mid);
mergesort(mid+1,high);
merge(arr,low,mid,high);
}
private void merge(int[] arr,int low,int mid,int high){
int leftIndex=low;
int rightIndex=mid+1;
int tempIndex=0;
while(leftIndex<=mid&&rightIndex<=high){
if(arr[leftIndex]<arr[rightIndex]){
temp[tempIndex++]=arr[leftIndex++];
}else{
temp[tempIndex++]=arr[rightIndex++];
}
//已經比較完了,接下來如果發現左邊 或者右邊還有沒到最后的部分,則把那一部分直接添加到temp尾部
while(leftIndex<=mid){
temp[tempIndex++]=arr[leftIndex++];
}
while(rightIndex<=high){
temp[tempIndex++]=arr[rightIndex++];
}
//把temp 合并到arr中
int t=0;
while(low<=high){
arr[low++]=temp[t++];
}
}
}
}
時間復雜度計算:
因為遞歸算法的時間復雜度可以表示為T(n)=2T(n/2)+O(n).
這是一個每次分一半的遞歸,總共執行排序的步驟可以畫成一棵類似的樹.
因為每一層的子問題時間代價為cn,一共有log2N+1 層,所以所有的時間=cn(lgN+1)=cnlnN+cn 時間復雜度
O(NlgN).
命題F:
對于長度為N的任意數組,自頂向下歸并排序的需要 ?NlgN ≤O≤ NlgN次比較.
命題G:
對于長度為N的任意數組,自頂向下歸并排序最多要訪問數組6NlgN次.
證明:
每次歸并最多需要訪問數組6N次(2N 復制,2N用來將排好序的數組復制過去,2N次進行合并),根據命題F,可知需要有NlgN次比較,所以訪問數組為6NlgN.
2.2.3對自定向下的歸并排序的優化
1.對小規模的數組使用插入排序.(具體設置的閥值7) 一般插入排序處理小規模排序(例如15)可以將歸并縮短時間10%~15%
2.測試數組是否已經有序.在merge之前添加一個方法判斷arr[mid]≤arr[mid+1],如果是的話說明這個從lo ...high的數組已經是有序了
3.在merge的時候不進行多次復制.(不把數組復制到臨時數組,然后又進行把臨時數組的數據copy到src中,這里面至少要經過2次重復操作)優化對臨時數組的空間上面還是保持不變,主要在與減少時間.
代碼如下:
public class MergeSort{
private static final int OFF_CUT=7;
public void sort(int[] src){
int[] org=src.clone();
mergeSort(org,src,0,org.length-1);
}
private void mergeSort(int[] src,int[] dst,int lo,int hi){
//進行閥值判斷
if(hi<lo+OFF_CUT){
insertSort(dst,lo,hi);
return ;
}
int mid=(lo+hi)/2;
mergeSort(dst,src,lo,mid);
mergeSort(dst,src,mid+1,hi);
//如果src已經有序,那么直接復制到dst即可
if(src[mid+1]>=src[mid]){
System.arrayCopy(src,lo,dst,lo,hi-lo+1);
}
merge(src,dst,lo,mid,hi);
}
private void insertSort(int[] arr,int lo,int hi){
for(int i=lo+1;i<=hi;i++){
int temp=arr[i];
int j=i;
while(j>lo&&temp<arr[j-1];j--){
arr[j]=arr[j-1];
}
arr[j]=temp;
}
}
private void merge(int[] src,int[] dst,int low,int mid,int hi){
int lowIndex=low;
int hiIndex=mid+1;
for(int i=low;i<=hi;i++){
if(lowIndex>mid){
dst[i]=src[hiIndex++];
}else if(hiIndex>hi){
dst[i]=src[lowIndex++];
}else if (src[lowIndex]<src[hiIndex]) {
dst[i]=src[lowIndex++];
}else{
dst[i]=src[hiIndex++];
}
}
}
}
2.2.4自底向上的歸并排序
使用歸并排序的另一種方法是 先歸并那些微型數組,然后在歸并得到的子數組,反復;直到我們得到整個數組并歸并一起.這樣比標準遞歸方法需要的代碼更少.
[手搖算法,又稱三次反轉]
用于反轉字符串包括里面有不服順序改變,可以將空間復雜度降低至O(1).
具體做法.例子:
題目要求部分反轉數組。比如說1,2,3,4,5 翻轉后是3,4,5,1,2
class Reverse{
private void reverse(int[] a, int low, int hi) {
while (low < hi) {
swap(a, low, hi);
low++;
hi--;
}
}
//這里也用到內存反轉3次 進行交換兩個數 ,采用 ^操作
private void swap(int[] a, int left, int right) {
a[left] ^= a[right];
a[right] ^= a[left];
a[left] ^= a[right];
}
public static void main(String[] args) {
int[] test = {1, 2, 3, 4, 5};
Reversel rev=new Reverse();
rev.reverse(test, 0, 1);
rev.reverse(test, 2, test.length - 1);
rev.reverse(test, 0, test.length - 1);
for (int i = 0; i < test.length; i++) {
System.out.print(test[i]);
}
}
}
自底向上,的歸并排序的思路是
- 先分割成前半部分為1,后半部分的數組, 劃分成 N/2 段.
- 然后接著以分成前半部分為2,后半部分為2的數組.
- 然后接著以分成前半部分為3,后半部分為3的數組.
… - 直到每部分的步長接近N 或者超過N.
/**
* Created by leon on 18-1-24.
* 這里是采用自底向上的算法
*/
public class MergeSortBottom2Up {
public void sort(int[] arr) {
System.out.println("自底向上遞歸前:" + Arrays.toString(arr));
int num = arr.length;
//翻倍遞增
for (int i = 1; i < num; i = i + i) {
for (int j = 0; j < num; j++) {
int low = Math.min(j, num - 1);
j = j + i + i - 1;/*往后移動步長*/
int high = Math.min(j, num - 1);
int mid = (low + high) / 2;
merge(arr, low, mid, high);
}
}
System.out.println("自底向上遞歸后:" + Arrays.toString(arr));
}
/**
* 進行在一個數組里面for循環,發現lowIndex下標超過mid 或者 highIndex 超過high進行切換
* 比較的思路:
* 1.lowIndex=lo,midIndex,highIndex=mid+1
* 1.從lowIndex 開始 ++,比較src[lowIndex] 和 src[highIndex]的值,直到 src[lowIndex]>src[highIndex]停止
* 2.用midIndex 定位到highIndex
* 3.從highIndex++,直到src[highIdex]>src[lowIndex],停
* 此刻說明從 midIndex~highIndex-1的數比lowIndex的小,需要插入到lowIndex之前.
* 利用內存3次反轉(手搖技術)反轉內存,達到把midIndex~highIndex-1部分插入到lowIndex之前
* ____________________
* |1|3|4|5|9|10|2|6|7|
* `^``````````^`^``````
* l h =>初始 l,m,h=m+1
* ____________________
* |1|3|4|5|9|10|2|6|7|
* ```^````````^`^````
* l h =>移動l,arr[l]>arr[h]
* ____________________
* |1|3|4|5|9|10|2|6|7|
* ```l``````````m`h``` =>把m->h位置,h++,直到arr[h]>arr[l]
*此時說明 arr[m]~arr[h-1]的數小于 arr[l],需要進行3次內存反轉,
*達到把arr[m]~arr[h-1]排到arr[lowIndex]前面
* ____________________
* |1|3|4|5|9|10|2|6|7|
*
* ____________________
* |1|10|9|5|4|3|2|6|7|==> 反轉左邊
^ ^ ^
* ____________________
* |1|10|9|5|4|3|2|6|7|==>反轉右邊 只有一個 2
* ^ ^ ^
* ____________________
* |1|2|3|4|5|9|10|6|7|==>反轉兩部分
* ^ ^ ^
*完成一次排序,
* lowIndex=lowIndex+i+i-1;繼續下一次排序
*
* @param src
* @param lo
* @param mid
* @param hi
*/
private void merge(int[] src, int lo, int mid, int hi) {
int lowIndex = lo;
int highIndex = mid + 1;
//當 lowIndex 與highIndex 重疊時.或者highIndex超出了最外層結束
while (lowIndex < highIndex && highIndex <= hi) {
while (lowIndex < highIndex && src[lowIndex] <= src[highIndex]) {
lowIndex++;
}
int midIndex = highIndex;
//這里的邊界值很關鍵
while (highIndex <= hi && src[highIndex] < src[lowIndex]) {
highIndex++;
}
Convert(src, lowIndex, midIndex - 1, highIndex - 1);
//完成一次排序
lowIndex += highIndex - midIndex;
}
}
void Convert(int a[], int low, int M, int high) {
//反轉 low...middle
reverse(a, low, M);
//反轉middle+1 high
reverse(a, M + 1, high);
//反轉low...high
reverse(a, low, high);
}
private void reverse(int[] a, int low, int hi) {
while (low < hi) {
swap(a, low, hi);
low++;
hi--;
}
}
private void swap(int[] a, int left, int right) {
a[left] ^= a[right];
a[right] ^= a[left];
a[left] ^= a[right];
}
}
2.2.5 排序算法的復雜度
研究復雜度的第一步是建立模型.一般來說尋找與問題相關的最簡單的模型.排序算法,是基于主鍵的比較決定的.
命題I:
沒有任何基于比較的算法能夠保證使用少于lg(N!)~NlogN次比較將長度為N的數組排序.
這個結論告訴我們在設計排序時能達到的最佳效果.
命題J:
歸并排序是一種漸進最優的基于比較的排序算法.更準確的說.歸并排序在最壞情況下的比較次數和其他基于比較的任意算法的最少時間都是~NlogN.