八大排序算法讀書筆記

排序算法是算法學中最基礎、應用最廣的一類算法,其中最簡單的就是冒泡排序和簡單選擇排序法,然而這兩種算法的時間複雜度都在O(n^2),並不高效,這裏就對八種不一樣的排序算法進行分析。基本的排序算法分爲插入排序、選擇排序、交換排序、歸併排序、基數排序,其中插入排序分爲直接插入排序、希爾排序,選擇排序分爲簡單選擇排序和堆排序,交換排序分爲冒泡排序和快速排序,總共八種基礎的排序算法,其餘的排序算法都是在這八種方法上的組合與變種。git

 

直接插入法十分簡單,就是將亂序的元素逐個插入到已排序好的序列中,即將被插入元素與排好序的元素逐一比較,插入到合適的位置,時間複雜度爲O(n^2)。算法

 

希爾排序是在直接插入法的改進,首先構造一增量序列(遞減至1),按某個增量d分紅若干組子序列,每組中記錄的下標相差d,對每組中所有元素進行直接插入排序,而後再用增量序列中下一個較小的增量進行分組,再對每組進行直接插入排序,直至增量爲1。其效果以下:shell

 

示例代碼:數組

void ShellInsertSort(int a[], int n, int dk)  
{  
    for(int i= dk; i<n; ++i){  
        if(a[i] < a[i-dk]){         
            int j = i-dk;     
            int x = a[i];            
            a[i] = a[i-dk];         
            while(x < a[j]){    
                a[j+dk] = a[j];  
                j -= dk;              
            }  
            a[j+dk] = x;            
        }  
    }  
      
}  
  
void shellSort(int a[], int n){  
  
    int dk = n/2;  
    while( dk >= 1  ){  
        ShellInsertSort(a, n, dk);  
        dk = dk/2;  
    }  
}  

 希爾排序相比於簡單直接插入排序,減小了複製的次數,緣由是當增量較大時數據項每一趟排序須要移動的個數不多,儘管數據項的個數不少;當增量減少時,每一趟須要移動的數據增多,但此時已經接近於它們排序後的最終位置,因此希爾排序的效率比插入排序高不少。希爾排序的平均時間複雜度爲O( n^1.3 ),沒有快速排序算法快,所以對規模很是大的數據排序不是最優選擇。但希爾排序在最壞的狀況下和平均狀況下執行效率相差不是不少,而快速排序在最壞的狀況下執行的效率會很是差。因此,大部分排序工做在開始時均可以用希爾排序,若在實際使用中證實它不夠快,再改爲快速排序這樣更高級的排序算法。性能

 

簡單選擇排序即在首次遍歷容器的過程當中找到最小或最小的元素與第一個元素交換位置,而後在第二次遍歷過程當中重複上述步驟與第二個元素交換位置,以此類推,時間複雜度爲O(n^2)。在此基礎上能夠作一些改進,好比每次搜索過程當中同時搜索最大和最小元素並分別放在容器的頭和尾,這樣遍歷次數能減小一半。ui

 

堆排序是一種樹形選擇排序,對一個n個元素的序列,堆的定義是:spa

           

 堆對應一棵徹底二叉樹,且全部非葉結點的值均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的。堆排序的過程就是先把數組的n個元素創建成堆結構,而後堆頂元素就是排序後數組的第一個元素,再將剩下 n-1 個元素調整爲新的堆結構,其堆頂就是排序後的第二個元素,以此類推,與簡單選擇排序有必定類似之處。指針

先說調整堆的方法:code

1)設有n個元素的堆,輸出堆頂元素後,剩下 n-1 個元素。將堆底元素送入堆頂(最後一個元素與堆頂進行交換),此時根結點不知足堆的性質。xml

2)將根結點與左、右子樹中較小元素的進行交換。

3)若與左子樹交換:若是左子樹堆被破壞,即左子樹的根結點不知足堆的性質,則重複方法 (2).

4)若與右子樹交換,若是右子樹堆被破壞,即右子樹的根結點不知足堆的性質。則重複方法 (2).

5)繼續對不知足堆性質的子樹進行上述交換操做,直到葉子結點,堆被建成。

創建堆的過程就是反覆調整的過程,從第 [n/2] 個元素開始,依次向前直到第一個元素全都重複上述(2)~(5)調整過程,一個堆就被初始化好了。

示例代碼:

void HeapAdjust(int H[],int s, int length)  
{  
    int tmp  = H[s];  
    int child = 2*s+1; 、
    while (child < length) {  
        if(child+1 <length && H[child]<H[child+1]) { 
            ++child ;  
        }  
        if(H[s]<H[child]) {  
            H[s] = H[child]; 
            s = child;         
            child = 2*s+1;  
        }  else {         
             break;  
        }  
        H[s] = tmp;         
    }  
    print(H,length);  
}  
   
void BuildingHeap(int H[], int length)  
{   
    for (int i = (length -1) / 2 ; i >= 0; --i)  
        HeapAdjust(H,i,length);  
}  

void HeapSort(int H[],int length)  
{  
    BuildingHeap(H, length);  
    for (int i = length - 1; i > 0; --i)  
    {  
        int temp = H[i]; H[i] = H[0]; H[0] = temp;   
        HeapAdjust(H,0,i);  
  }  
}   

堆排序的平均時間複雜度爲O(nlogn),且最差、最好時間複雜度都在這個量級。

 

冒泡排序是最簡單的一種交換排序,其算法是不斷遍歷數組,每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換,時間複雜度爲O(n^2)。

快速排序是另外一種交換排序,也是速度最快的一種排序算法,其算法以下:

1)選擇一個基準元素,一般選擇第一個元素或者最後一個元素,

2)經過一趟排序將待排序的記錄分割成獨立的兩部分,其中一部分記錄的元素值均比基準元素值小。另外一部分記錄的元素值比基準值大。

3)此時基準元素在其排好序後的正確位置。

4)而後分別對這兩部分記錄用一樣的方法繼續進行排序,直到整個序列有序。

具體過程如圖所示:

示例代碼:

int partition(int a[], int low, int high)  
{  
    int privotKey = a[low];                             
    while(low < high){                                  
        while(low < high  && a[high] >= privotKey) --high; 
        swap(&a[low], &a[high]);  
        while(low < high  && a[low] <= privotKey ) ++low;  
        swap(&a[low], &a[high]);  
    }  
    print(a,10);  
    return low;  
}  
  
void quickSort(int a[], int low, int high){  
    if(low < high){  
        int privotLoc = partition(a,  low,  high);   
        quickSort(a,  low,  privotLoc -1);         
        quickSort(a,   privotLoc + 1, high);        
    }  
}  

快速排序的時間複雜度爲O(nlogn),且其平均性能是同數量級算法中最好的。但若初始序列按關鍵碼有序或基本有序時,快排反而蛻化爲冒泡排序。

 

歸併排序法的思想是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干有序個子序列,而後再將它們合併爲總體有序序列。1 個元素的表老是有序的,因此對n 個元素的待排序列,每一個元素可當作1 個有序子表,對子表兩兩合併生成n/2個子表,所得子表除最後一個子表長度可能爲1 外,其他子表長度均爲2。再進行兩兩合併,直到生成n 個元素按關鍵碼有序的表。效果如圖:

示例代碼:

void Merge(ElemType *r,ElemType *rf, int i, int m, int n)  
{  
    int j,k;  
    for(j=m+1,k=i; i<=m && j <=n ; ++k){  
        if(r[j] < r[i]) rf[k] = r[j++];  
        else rf[k] = r[i++];  
    }  
    while(i <= m)  rf[k++] = r[i++];  
    while(j <= n)  rf[k++] = r[j++];  
}  

歸併排序的平均時間複雜度爲O(nlogn),且最好、最壞時間複雜度都在這個量級。

 

基數排序是一種多關鍵字排序,分爲最高位優先(Most Significant Digit first)法和最低位優先(Least Significant Digit first)法。設待排序列爲n個記錄,d個關鍵碼,關鍵程度從k1到kd遞減,關鍵碼的取值範圍爲radix。(關鍵碼的意義是隻要k1大,則排序高,k1相等則比較其他關鍵碼,對 k2~kd 同理)

MSD法:先按k1排序分組,同一組中記錄,關鍵碼k1相等,再對各組按k2排序分紅子組,以後,對後面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序後。再將各組鏈接起來,便獲得一個有序序列;

LSD法:先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便獲得一個有序序列。
以LSD爲例,假設原來有一串數值以下所示: 73, 22, 93, 43, 55, 14, 28, 65, 39, 81

第一步

首先根據個位數的數值,在走訪數值時將它們分配至編號0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39

第二步

接下來將這些桶子中的數值從新串接起來,成爲如下的數列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再進行一次分配,此次是根據十位數來分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93

第三步

接下來將這些桶子中的數值從新串接起來,成爲如下的數列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
 排序完成。
基數排序的時間複雜度爲O(d(n+radix)),其中,一趟分配時間複雜度爲O(n),一趟收集時間複雜度爲O(radix),共進行d趟分配和收集;空間複雜度爲O(n+d*radix),須要2*radix個指向隊列的輔助空間,以及用於靜態鏈表的n個指針。
 
以上就是八種基本排序算法,具體使用哪種須要結合具體狀況分析。影響排序的因素有不少,平均時間複雜度低的算法並不必定就是最優的。相反,有時平均時間複雜度高的算法可能更適合某些特殊狀況。同時,選擇算法時還得考慮它的可讀性,以利於軟件的維護。通常而言,須要考慮的因素有如下四點:(1)待排序的記錄數目n的大小;(2)記錄自己數據量的大小,也就是記錄中除關鍵字外的其餘信息量的大小;(3)關鍵字的結構及其分佈狀況;(4)對排序穩定性的要求。(排序算法的穩定性是指:若待排序的序列中存在多個具備相同關鍵字的記錄,通過排序這些記錄的相對次序保持不變,則稱該算法是穩定的;若經排序後,記錄的相對次序發生了改變,則稱該算法是不穩定的。穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序;不穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。)

排序算法選取準則:

  1. 當n較大,則應採用時間複雜度爲O(nlogn)的排序方法:快速排序、堆排序或歸併排序序。快速排序是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短。
  2. 當n較大,內存空間容許,且要求穩定性:歸併排序。
  3. 當n較小,可採用直接插入或直接選擇排序。(當元素分佈有序,直接插入排序將大大減小比較次數和移動記錄的次數。)
  4. 基數排序是一種穩定的排序算法,但有必定的侷限性:(1)關鍵字可分解;(2)記錄的關鍵字位數較少,若是密集更好;(3)若是是數字時,最好是無符號的,不然將增長相應的映射覆雜度,可先將其正負分開排序。
相關文章
相關標籤/搜索