數據結構與簡單算法-排序算法

排序算法

時間複雜度O(n^2)的排序

1.冒泡排序

冒泡排序經過兩兩比較相鄰記錄的關鍵字,反序則交換,從而達到排序的效果java

for(int i=0; i<o.length; i++){
    for(int j=0; j<o.length-i-1; j++){
        if(o[j]>o[j+1]){
            swap(o,j,j+1);
        }
    }
}

上面的代碼有個小問題,設想存在待排序序列{1,3,5,7,11,9},通過第一遍遍歷交換11和9以後已經達到排序效果,接下來的遍歷已無心義。考慮增長一個標誌位,若上一輪遍歷沒有引發序列任何變化,則退出整個遍歷,再也不作無心義的比較。算法

boolean flag = true;
for(int i=0; i<o.length&&flag; i++){
    flag = false;
    for(int j=0; j<o.length-i-1; j++){
        if(o[j]>o[j+1]){
            swap(o,j,j+1);
            flag = true;
        }
    }
}

冒泡排序還有幾種變形,或是向上冒泡、向下冒泡,但都差異不大。分析時間複雜度:最好狀況下無需交換,只要進行n-1次比較,時間複雜度O(n);最壞狀況下,即待排序序列爲逆序,須要比較n*(n-1)/2次,並做等數量級的交換,時間複雜度O(n^2)。數組

2.簡單選擇排序

與冒泡排序區別的是,冒泡排序只要發現反序就會交換相鄰的兩個數,而簡單選擇排序是先找出最小或最大的數,與特定位置交換,能夠看出其優勢是交換移動次數至關少。數據結構

int min;
for(int i=0; i<o.length-1; i++){
    min = i;
    for(int j=i+1; j<o.length; j++){
        if(o[min]>o[j]){
            min = j;
        }
    }
    if(min != i){
        swap(o,min,i);
    }
}

分析簡單選擇排序的複雜度,不管最好最壞狀況下,比較次數都是n*(n-1)/2,但最好狀況下交換0次,最壞狀況也只有n-1次,總的複雜度還是O(n^2)。測試

3.直接插入排序

直接插入排序的思想是向一個有序序列逐步插入記錄,獲得記錄數不斷增長的序列,若初始爲亂序序列,則選取第一個元素做爲一個有序序列,其餘元素依次插入這個有序序列,從而達到排序效果。優化

int j,temp;
for(int i=1; i<o.length; i++){
    if(o[i] < o[i-1]){
        temp = o[i];
        for(j=i-1; j>=0&&o[j]>temp; j--){
            o[j+1] = o[j];
        }
        o[j+1] = temp;
    }
}

直接插入與理撲克牌有點類似,最好狀況下須要比較n-1次,無需交換;最壞狀況下比較(n+2)(n-1)/2次,移動(n+4)(n-1)/2次。設計

時間複雜度O(n^3/2)的排序

1.希爾排序

希爾排序是基於直接插入排序的,直接插入排序在元素較少和元素基本有序時效率是不錯的,但隨着元素增多和有序性破壞,效率會降低的很明顯。希爾排序經過分組實現跳躍式移動,保證待排序序列基本有序後再排序,就能提升效率。code

int j,temp;
int increment = o.length;
do{
    increment = increment/3+1;
    for(int i=increment; i<o.length; i++){
        temp = o[i];
        for(j=i-increment; j>=0&&o[j]>temp; j-=increment){
            o[j+increment] = o[j];
        }
        o[j+increment] = temp;
    }
    
}while(increment>1);

希爾排序的關鍵在於增量increment的選擇,最壞狀況下,能夠取得時間複雜度O(n^3/2)的算法。排序

時間複雜度O(nlgn)的排序

1.堆排序

堆是特殊的徹底二叉樹,每一個節點的值都大於等於(小於等於)其左右孩子節點的值。堆排序其實相似簡單選擇排序,每次找出最大最小元素,移到特定位置完成排序。堆排序相對於簡單選擇排序的優勢是,以前排序的結果得以保存,能被後面的排序利用。rem

//堆排序
for(int i=o.length/2-1; i>=0; i--){
    //從最後一個有子節點的節點開始依次往前調整對應節點來生成大頂堆
    heapAdjust(o,i,o.length-1);
}

for(int i=0; i<o.length-1; i++){
    //交換堆頂元素與未排序堆最後一個元素
    swap(o,0,o.length-i-1);
    //根據調整節點從新生成大頂堆
    heapAdjust(o,0,o.length-2-i);
}
//構建大頂堆
void heapAdjust(int[] o,int i,int j){
    for(int m=i*2+1; m<=j; m=2*i+1){
        if(m<j&&o[m]<o[m+1]){
            m ++;
        }
        if(o[m]>o[i]){
            swap(o,m,i);
        }
        i = m;
    }
}

堆排序的時間複雜度主要取決於構建堆和重建堆兩部分,構建堆的時間複雜度O(n^2),重建堆的時間複雜度爲O(nlgn),總的時間複雜度爲O(nlgn)。

2.歸併排序

歸併排序的思想是逐層將序列子元素兩兩歸併成有序序列,每一個子序列長度經歷一、二、四、...、n,最後獲得完整有序序列。

int[] sort(int[] o,int m,int n){
    int mid;
    int[] result = new int[o.length];
    if(o.length == 1|| m==n){
        result[0] = o[0];
    }else{
        mid = (m+n)/2;
        int[] temp1 = new int[mid-m+1];
        int[] temp2 = new int[o.length-mid+m-1];
        System.arraycopy(o,0,temp1,0,mid-m+1);
        System.arraycopy(o,mid-m+1,temp2,0,o.length-mid+m-1);
        int[] result1 = sort(temp1,m,mid);
        int[] result2 = sort(temp2,mid+1,n);
        result = Merge(result1,result2);
    }
    return result;
}

int[] Merge(int[] i,int[] j){
    int m=0,n=0,k=0;
    int[] result = new int[i.length+j.length];
    
    for(; m<i.length&&n<j.length; k++){
        if(i[m]<j[n]){
            result[k] = i[m++];
        }else{
            result[k] = j[n++];
        }
    }
    
    if(m<i.length){
        while(m<i.length){
            result[k++] = i[m++];
        }
    }
    
    if(n<j.length){
        while(n<j.length){
            result[k++] = j[n++];
        }
    }
    return result;
}

上面的算法用了大量臨時數組,是一種很差的設計,其實只須要一個與原數組大小相同的臨時數組(必須有,若是隻有數組自己,那麼排序時可能覆蓋原有元素),就能完成歸併排序。

void sort(int[] o, int lo, int high){
    if(hi <= lo) return;
    int mid = lo+(high-lo)/2;
    sort(o,lo,mid);
    sort(o,mid+1,hi);
    merge(o,lo,mid,hi);
}

void merge(int[] o,int lo,int mid,int high){
    int i = lo,j = mid+1;
    int[] aux = new int[hi-lo+1];
    for(int k=lo; k<=hi, k++)
        if(i>mid) a[k] = aux[j++];
        else if(j>hi) a[k] = aux[i++];
        else if(aux[j]<aux[i]) a[k] = aux[j++];
        else a[k] = aux[i++];
}

歸併排序須要掃描序列中全部記錄,耗費O(n)時間,整個歸併排序須要logn,所以,總的時間複雜度爲O(nlgn),空間複雜度爲O(n+logn)。

歸併排序能夠從幾個方面優化:對小規模子數組使用插入排序;測試數組是否已經有序,有序就能夠跳過merge方法;不將元素複製到輔助數組(複製改成排序)。

3.快速排序

快速排序的思想是分割,確保一個元素左邊的元素都小於這個元素,右邊的元素都大於這個元素,而後對這兩部分分別繼續進行分割,從而達到排序的效果。

void sort(int[] i,int low,int high){
    int temp;
    if(low<high){
        temp = partition(i,low,high);
        sort(i,low,temp-1);
        sort(i,temp+1,high);
    }
}

int partition(int[] i,int low,int high){
    int privotkey = i[low];
    while(low<high){
        while(low<high&&i[high]>=privotkey)
            high --;
        swap(i,low,high);
        while(low<high&&i[low]<=privotkey)
            low ++;
        swap(i,low,high);
    }
    return low;
}

快速排序的時間複雜度最優和平均都是O(nlgn),最壞狀況下爲O(n^2),空間複雜度爲O(lgn),快速排序很脆弱,有一些優化手段,好比轉插入等;希爾排序效率高,代碼簡單,不須要額外空間,較爲經常使用;

總結

直接插入排序在序列基本有序時效率較高,對於隨機排序序列,插入排序和選擇排序的運行時間是平方級別的,二者之比應該是一個較小的常數。

排序算法 平均狀況 最好狀況 最壞狀況 輔助空間 穩定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
簡單選擇排序 O(n^2) O(n^2) O(n^2) O(1) 穩定
直接插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(nlogn)~O(n^2) O(n^1.3) O(n^2) O(1) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
快速排序 O(nlogn) O(nlogn) O(n^2) O(logn)~O(n) 不穩定

參考:《大話數據結構》、《算法》。

相關文章
相關標籤/搜索