[Data Structure & Algorithm] 八大排序算法

  排序有內部排序和外部排序之分,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。我們這裏說的八大排序算法均爲內部排序。

  下圖爲排序算法體系結構圖:

 

  常見的分類算法還可以根據排序方式分爲兩大類:比較排序和非比較排序。本文中前七種算法都是比較排序,非比較排序有三種,分別爲:

  1)計數排序(Count Sort)(複雜度O(n+k)(其中k是待排序的n個數字中最大值),參見《計數排序-Counting Sort》

  2)基數排序(Bucket Sort)(複雜度O(nk)(其中k是最大數字的位數),參見《最快最簡單的排序—桶排序》

  3)桶排序(Radix Sort)(複雜度O(n+k)(其中k是待排序的n個數字中最大值),參見《基數排序(Radix Sorting)》

  非比較排序的特點是時間複雜度很低,都是線性複雜度O(n),但是非比較排序受到的限制比較多,不是通用的排序算法。本文主要講解七種比較排序算法,最後單獨介紹一下非比較排序算法。

1. 直接插入排序(Straight Insertion Sort)

  基本思想:將待排序的無序數列看成是一個僅含有一個元素的有序數列和一個無序數列,將無序數列中的元素逐次插入到有序數列中,從而獲得最終的有序數列。

  算法流程:

  1)初始時, a[0]自成一個有序區, 無序區爲a[1, ... , n-1], 令i=1;

  2)將a[i]併入當前的有序區a[0, ... , i-1];

  3)i++並重復2)直到i=n-1, 排序完成。

  時間複雜度:O(n^2)。

  示意圖:初始無序數列爲 49, 38, 65, 97, 76, 13, 27 ,49

 

  說明:如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的

  C++實現源碼:

複製代碼
//直接插入排序,版本1
void StraightInsertionSort1(int a[], int n)
{
    int i, j, k;
    for(i=1; i<n; i++)
    {
        //找到要插入的位置
        for(j=0; j<i; j++)
            if(a[i] < a[j])
                break;
        //插入,並後移剩餘元素
        if(j != i)
        {
            int temp = a[i];
            for(int k=i-1; k>=j; k--)
                a[k+1] = a[k];
            a[j] = temp;
        }
    }
    PrintDataArray(a, n);
}
複製代碼

  兩種簡化版本,推薦第三版本。

複製代碼
//直接插入法,版本2:搜索和後移同時進行
void StraightInsertionSort2(int a[], int n)
{
    int i, j, k;
    for(i=1; i<n; i++)
        if(a[i] < a[i-1])
        {
            int temp = a[i];
            for(j=i-1; j>=0 && a[j]>temp; j--)
                a[j+1] = a[j];
            a[j+1] = temp;
        }
    PrintDataArray(a, n);
}

//插入排序,版本3:用數據交換代替版本2的數據後移(比較對象只考慮兩個元素)
void StraightInsertionSort3(int a[], int n)
{
    for(int i=1; i<n; i++)
        for(int j=i-1; j>=0 && a[j]>a[j+1]; j--)
            Swap(a[j], a[j+1]);
    PrintDataArray(a, n);
}
複製代碼

2. 希爾排序(Shells Sort)

  希爾排序是1959 年由D.L.Shell 提出來的,相對直接排序有較大的改進。希爾排序又叫縮小增量排序

  基本思想:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

  算法流程:

  1)選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;

  2)按增量序列個數k,對序列進行k 趟排序;

  3)每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

  時間複雜度:O(n^(1+e))(其中0<e<1),在元素基本有序的情況下,效率很高。希爾排序是一種不穩定的排序算法。

  希爾排序的示例:

 

  C++實現源碼:

複製代碼
//希爾排序
void
ShellSort(int a[], int n) { int i, j, gap; //分組 for(gap=n/2; gap>0; gap/=2) //直接插入排序 for(i=gap; i<n; i++) for(j=i-gap; j>=0 && a[j]>a[j+gap]; j-=gap) Swap(a[j], a[j+gap]); PrintDataArray(a, n); }
複製代碼

  通過源代碼我們也能看出來,希爾排序就是在直接插入排序的基礎上加入了分組策略。

3. 直接選擇排序(Straight Selection Sort)

  基本思想:在要排序的一組數中,選出最小(或者最大)的個數與第1個位置的數交換;然後在剩下的數當中再找最小(或者最大)的與第2個位置的數交換,依次類推,直到第n-1個元素(倒數第二個數)和第n個元素(最後個數)比較爲止。

  算法流程:

  1)初始時,數組全爲無序區a[0, ... , n-1], 令i=0;

  2)在無序區a[i, ... , n-1]中選取一個最小的元素與a[i]交換,交換之後a[0, ... , i]即爲有序區;

  3)重複2),直到i=n-1,排序完成。

  時間複雜度分析O(n^2),直接選擇排序是一種不穩定的排序算法。

  直接選擇排序的示例:

  C++實現源碼: 

複製代碼
//直接選擇排序
void StraightSelectionSort(int a[], int n)
{
    int i, j, minIndex;
    for(i=0; i<n; i++)
    {
        minIndex=i;
        for(j=i+1; j<n; j++)
            if(a[j]<a[minIndex])
                minIndex=j;
        Swap(a[i], a[minIndex]);
    }
    PrintDataArray(a, n);
}
複製代碼

4. 堆排序(Heap Sort)

  堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。

  堆的定義如下:具有n個元素的序列(k1,k2,...,kn),當且僅當滿足

時稱之爲堆。由堆的定義可以看出,堆頂元素(即第一個元素)必爲最小項(小頂堆)。
  若以一維數組存儲一個堆,則堆對應一棵完全二叉樹,且所有非葉結點的值均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的。如:

  (a)大頂堆序列:(96, 83,27,38,11,09)

  (b)小頂堆序列:(12,36,24,85,47,30,53,91)

 

  基本思想:初始時把要排序的n個數的序列看作是一棵順序存儲的二叉樹(一維數組存儲二叉樹),調整它們的存儲序,使之成爲一個堆,將堆頂元素輸出,得到n 個元素中最小(或最大)的元素,這時堆的根節點的數最小(或者最大)。然後對前面(n-1)個元素重新調整使之成爲堆,輸出堆頂元素,得到n 個元素中次小(或次大)的元素。依此類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。稱這個過程爲堆排序

  時間複雜度分析:O(nlog(n)),堆排序是一種不穩定的排序算法。

  因此,實現堆排序需解決兩個問題:
  1. 如何將n 個待排序的數建成堆?
  2. 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成爲一個新堆?

  首先討論第二個問題:輸出堆頂元素後,怎樣對剩餘n-1元素重新建成堆?
  調整小頂堆的方法:

  1)設有m 個元素的堆,輸出堆頂元素後,剩下m-1 個元素。將堆底元素送入堆頂((最後一個元素與堆頂進行交換),堆被破壞,其原因僅是根結點不滿足堆的性質。

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

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

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

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

  稱這個自根結點到葉子結點的調整過程爲篩選。如圖:

 

  再討論第一個問題,如何將n 個待排序元素初始建堆?
  建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。

  1)n 個結點的完全二叉樹,則最後一個結點是第n/2個結點的子樹。

  2)篩選從第n/2個結點爲根的子樹開始,該子樹成爲堆。

  3)之後向前依次對各結點爲根的子樹進行篩選,使之成爲堆,直到根結點。

  如圖建堆初始過程:無序序列:(49,38,65,97,76,13,27,49)
   


  

    C++實現源碼:

複製代碼
//堆排序問題二:如何調整一個堆?
void HeapAdjusting(int a[], int root, int n)
{
    int temp = a[root];
    int child = 2*root+1; //左孩子的位置
    while(child<n)
    {
        //找到孩子節點中較小的那個
        if(child+1<n && a[child+1]<a[child])
            child++;
        //如果較大的孩子節點小於父節點,用較小的子節點替換父節點,並重新設置下一個需要調整的父節點和子節點。
        if(a[root]>a[child])
        {
            a[root] = a[child];
            root = child;
            child = 2*root+1;
        }
        else
            break;
        //將調整前父節點的值賦給調整後的位置。
        a[root] = temp;
    }
}

//堆排序問題一:如何初始化建堆?
void HeapBuilding(int a[], int n)
{
    //從最後一個有孩子節點的位置開始調整,最後一個有孩子節點的位置爲(n-1)/2
    for(int i=(n-1)/2; i>=0; i--)
        HeapAdjusting(a, i, n);
}

//堆排序
void HeapSort(int a[], int n)
{
    //初始化堆
    HeapBuilding(a, n);
    //從最後一個節點開始進行調整
    for(int i=n-1; i>0; i--)
    {
        //交換堆頂元素和最後一個元素
        Swap(a[0], a[i]);
        //每次交換後都要進行調整
        HeapAdjusting(a, 0, i);
    }
}
複製代碼

  在這裏多說幾句堆排序的強大之處,堆排序可以看成是一種算法,也可以看成是一種數據結構。它可以分爲小頂堆和大頂堆。與堆這種數據結構聯繫緊密的一種典型問題就是我們經常遇到的top-K問題。

  我們先看一個大數據top-K示例:

  例子:搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

  首先,我們知道這是一個典型的top-K問題。

  針對大數據問題進行統計首先應該想到的就是Hash_map。所以第一步就是先遍歷全部的1千萬Query,構建出一個大小爲3百萬的Hash_map,其中的key值爲某條Query,對應的value值爲該條Query的查詢次數。

  建好Hash_map以後,我們接下來的問題就是如何在3百萬的Query中找出10個最熱門的Query,也就是要用到排序算法。排序算法中效率最高的時間複雜度爲O(n*log(n)),這是最簡單粗暴的方法,也是最直接的方法。或者我們進一步優化,該題目是要求尋找top-K問題,那麼我們可以直接去前K個Query構建一個數組,然後對其進行排序。遍歷剩餘的全部Query,如果某條Query的查詢次數大於數組中最小的一個,將數組中最小的Query剔除,加入這條新的Query。接着調整數組順序,依次進行遍歷,這樣的最壞情況下的複雜度爲O(n*K)。

  但是還可以繼續優化尋找top-K的操作,那就是藉助小根堆來實現。基於以上的分析,我們想想,有沒有一種既能快速查找,又能快速移動元素的數據結構呢?回答是肯定的,那就是堆。

  具體過程是,堆頂存放的是整個堆中最小的數,現在遍歷N個數,把最先遍歷到的k個數存放到最小堆中,並假設它們就是我們要找的最大的k個數,X1>X2...Xmin(堆頂),而後遍歷後續的(n-K)個數,一一與堆頂元素進行比較,如果遍歷到的Xi大於堆頂元素Xmin,則把Xi放入堆中,而後更新整個堆,更新的時間複雜度爲logK,如果Xi<Xmin,則不更新堆,整個過程的複雜度爲O(K)+O((N-K)*logK)=O(N*logK)。

  一個有關小根堆解決top-K問題的小動畫,請點擊這個鏈接

  思想與上述算法二一致,只是算法在算法三,我們採用了最小堆這種數據結構代替數組,把查找目標元素的時間複雜度有O(K)降到了O(logK)。那麼這樣,採用堆數據結構,算法三,最終的時間複雜度就降到了O(n*logK),和算法二相比,又有了比較大的改進。

5. 冒泡排序(Bubble Sort)

  基本思想:在要排序的一組數中,對當前還未排好序的範圍內的全部數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。每一趟排序後的效果都是講沒有沉下去的元素給沉下去。

  算法流程:

  1)比較相鄰的兩個元素,如果前面的數據大於後面的數據,就將兩個數據進行交換;這樣對數組第0個元素到第n-1個元素進行一次遍歷後,最大的一個元素就沉到數組的第n-1個位置;

  2)重複第2)操作,直到i=n-1。

  時間複雜度分析:O(n^2),冒泡排序是一種不穩定排序算法。

  冒泡排序的示例:

 

  C++實現源碼:

複製代碼
//冒泡排序
void BubbleSort(int a[], int n)
{
    int i, j;
    for(i=0; i<n; i++)
        //j的起始位置爲1,終止位置爲n-i
        for(j=1; j<n-i; j++)
            if(a[j]<a[j-1])
                Swap(a[j-1], a[j]);
    PrintDataArray(a, n);
}
複製代碼

6. 快速排序(Quick Sort)

  基本思想:快速排序算法的基本思想爲分治思想。

  1)先從數列中取出一個數作爲基準數;

  2)根據基準數將數列進行分區,小於基準數的放左邊,大於基準數的放右邊;

  3)重複分區操作,知道各區間只有一個數爲止。

  算法流程:(遞歸+挖坑填數)

  1)i=L,j=R,將基準數挖出形成第一個坑a[i];

  2)j--由後向前找出比它小的數,找到後挖出此數a[j]填到前一個坑a[i]中;

  3)i++從前向後找出比它大的數,找到後也挖出此數填到前一個坑a[j]中;

  4)再重複2,3),直到i=j,將基準數填到a[i]。

  時間複雜度:O(nlog(n)),但若初始數列基本有序時,快排序反而退化爲冒泡排序。

  快速排序的示例:

  (a)一趟排序的過程:

  (b)排序的全過程

  C++實現源碼:

複製代碼
//快速排序
void QuickSort(int a[], int L, int R)
{
    if(L<R)
    {
        int i=L, j=R, temp=a[i];
        while(i<j)
        {
            //從右向左找小於基準值a[i]的元素
            while(i<j && a[j]>=temp)
                j--;
            if(i<j)
                a[i++]=a[j];
            //從左向右找大於基準值a[i]的元素
            while(i<j && a[i]<temp)
                i++;
            if(i<j)
                a[j--]=a[i];
        }
        //將基準值填入最後的坑中
        a[i]=temp;
        //遞歸調用,分治法的思想
        QuickSort(a, L, i-1);
        QuickSort(a, i+1, R);
    }
}
複製代碼

7. 歸併排序(Merge Sort)

  基本思想:歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分爲若干個子序列,每個子序列是有序的。然後再把有序子序列合併爲整體有序序列。

  算法流程:(迭代+兩個有序數列合併爲一個有序數列)

  時間複雜度:O(nlog(n)),歸併算法是一種穩定排序算法。

  歸併排序示例:

 

  C++實現源碼:

複製代碼
//merge兩個有序數列爲一個有序數列
void MergeArr(int a[], int first, int mid, int last, int temp[])
{
    int i = first, j = mid+1;
    int m = mid, n = last;
    int k=0;
    //通過比較,歸併數列a和b
    while(i<=m && j<=n)
    {
        if(a[i]<a[j])
            temp[k++] = a[i++];
        else
            temp[k++] = a[j++];
    }
    //將數列a或者b剩餘的元素直接插入到新數列後邊
    while(i<=m)
        temp[k++] = a[i++];
    while(j<=n)
        temp[k++] = a[j++];

    for(i=0; i<k; i++)
        a[first+i] = temp[i];
}

//歸併排序
void MergeSort(int a[], int first, int last, int temp[])
{
    if(first<last)
    {
        int mid = (first+last)/2;
        MergeSort(a, first, mid, temp);
        MergeSort(a, mid+1, last, temp);
        MergeArr(a, first, mid, last, temp);
    }
}
複製代碼

8. 桶排序(Bucket Sort)/基數排序(Radix Sort)

  說基數排序之前,我們先說桶排序:

  基本思想:是將數列分到有限數量的桶裏。每個桶再個別排序(有可能再使用別的排序算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間O(n)。但桶排序並不是比較排序,不受到O(n*log n)下限的影響。
  簡單來說,就是把數據分組,放在一個個的桶中,然後對每個桶裏面的在進行排序。  

   例如要對大小爲[1..1000]範圍內的n個整數A[1..n]排序  

  首先,可以把桶設爲大小爲10的範圍,具體而言,設集合B[1]存儲[1..10]的整數,集合B[2]存儲(10..20]的整數,…… ,集合B[i]存儲((i-1)*10,   i*10]的整數,i=1,2,..100,總共有100個桶。  

  然後,對A[1, ... , n]從頭到尾掃描一遍,把每個A[i]放入對應的桶B[j]中。 再對這100個桶中每個桶裏的數字排序,這時可用冒泡,選擇,乃至快排,一般來說任何排序法都可以。

  最後,依次輸出每個桶裏面的數字,且每個桶中的數字從小到大輸出,這樣就得到所有數字排好序的一個序列了。  

  假設有n個數字,有m個桶,如果數字是平均分佈的,則每個桶裏面平均有n/m個數字。如果對每個桶中的數字採用快速排序,那麼整個算法的複雜度是  O(n+m*n/m*log(n/m))=O(n+n*logn-n*logm)。

  從上式看出,當m接近n的時候,桶排序複雜度接近O(n)  

  當然,以上複雜度的計算是基於輸入的n個數字是平均分佈這個假設的。這個假設是很強的  ,實際應用中效果並沒有這麼好。如果所有的數字都落在同一個桶中,那就退化成一般的排序了。  

      一個有關桶排序的圖文講解,強力推薦:阿哈磊的《最快最簡單的排序—桶排序》


  桶排序的一個重要的應用場景:Bit-map:

  所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,而Key即是該元素。由於採用了Bit爲單位來存儲數據,因此在存儲空間方面,可以大大節省。

    如果說了這麼多還沒明白什麼是Bit-map,那麼我們來看一個具體的例子,假設我們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼我們就可以採用Bit-map的方法來達到排序的目的。要表示8個數,我們就只需要8個Bit(1Bytes),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置爲0(如下圖):

    然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1(可以這樣操作 p+(i/8)|(0×01<<(i%8)) 當然了這裏的操作涉及到Big-ending和Little-ending的情況,這裏默認爲Big-ending),因爲是從零開始的,所以要把第五位置爲一(如下圖):

      

  然後再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完所有的元素,將相應的位置爲1,這時候的內存的Bit位的狀態如下:

  然後我們現在遍歷一遍Bit區域,將該位是一的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。

  其實Bit-map還有很多用途,這裏只是用排序進行了Bit-map的介紹,Bit-map可以進行查重的操作,尤其是在大數據上應用更爲廣泛,它可以將存儲空間降低10倍左右。


   前面說的幾大排序算法 ,大部分時間複雜度都是O(n2),也有部分排序算法時間複雜度是O(nlogn)。而桶式排序卻能實現O(n)的時間複雜度。但桶排序的缺點是:

  1)首先是空間複雜度比較高,需要的額外開銷大。排序有兩個數組的空間開銷,一個存放待排序數組,一個就是所謂的桶,比如待排序值是從0到m-1,那就需要m個桶,這個桶數組就要至少m個空間。

  2)其次待排序的元素都要在一定的範圍內等等。

  桶式排序是一種分配排序。分配排序的特定是不需要進行關鍵碼的比較,但前提是要知道待排序列的一些具體情況。

  分配排序的基本思想:說白了就是進行多次的桶式排序。

  基數排序過程無須比較關鍵字,而是通過“分配”和“收集”過程來實現排序。它們的時間複雜度可達到線性階:O(n)。

  實例:

  撲克牌中52 張牌,可按花色和麪值分成兩個字段,其大小關係爲:
  花色: 梅花< 方塊< 紅心< 黑心  
  面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A

  若對撲克牌按花色、面值進行升序排序,得到如下序列:

  

  即兩張牌,若花色不同,不論面值怎樣,花色低的那張牌小於花色高的,只有在同花色情況下,大小關係才由面值的大小確定。這就是多關鍵碼排序。

  爲得到排序結果,我們討論兩種排序方法。
  方法1:先對花色排序,將其分爲4 個組,即梅花組、方塊組、紅心組、黑心組。再對每個組分別按面值進行排序,最後,將4 個組連接起來即可。
  方法2:先按13 個面值給出13 個編號組(2 號,3 號,...,A 號),將牌按面值依次放入對應的編號組,分成13 堆。再按花色給出4 個編號組(梅花、方塊、紅心、黑心),將2號組中牌取出分別放入對應花色組,再將3 號組中牌取出分別放入對應花色組,……,這樣,4 個花色組中均按面值有序,然後,將4 個花色組依次連接起來即可。

  設n 個元素的待排序列包含d 個關鍵碼{k1,k2,…,kd},則稱序列對關鍵碼{k1,k2,…,kd}有序是指:對於序列中任兩個記錄r[i]和r[j](1≤i≤j≤n)都滿足下列有序關係:

                                                               

其中k1 稱爲最主位關鍵碼,kd 稱爲最次位關鍵碼。

  兩種多關鍵碼排序方法:

  多關鍵碼排序按照從最主位關鍵碼到最次位關鍵碼或從最次位到最主位關鍵碼的順序逐次排序,分兩種方法:

  最高位優先(Most Significant Digit first)法,簡稱MSD 法:

  1)先按k1排序分組,將序列分成若干子序列,同一組序列的記錄中,關鍵碼k1相等。

  2)再對各組按k2排序分成子組,之後,對後面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序後。

  3)再將各組連接起來,便得到一個有序序列。撲克牌按花色、面值排序中介紹的方法一即是MSD法。

  最低位優先(Least Significant Digit first)法,簡稱LSD法:

  1) 先從kd 開始排序,再對kd-1進行排序,依次重複,直到按k1排序分組分成最小的子序列後。

  2) 最後將各個子序列連接起來,便可得到一個有序的序列, 撲克牌按花色、面值排序中介紹的方法二即是LSD法。

  基於LSD方法的鏈式基數排序的基本思想:

  “多關鍵字排序”的思想實現“單關鍵字排序”。對數字型或字符型的單關鍵字,可以看作由多個數位或多個字符構成的多關鍵字,此時可以採用“分配-收集”的方法進行排序,這一過程稱作基數排序法,其中每個數字或字符可能的取值個數稱爲基數。比如,撲克牌的花色基數爲4,面值基數爲13。在整理撲克牌時,既可以先按花色整理,也可以先按面值整理。按花色整理時,先按紅、黑、方、花的順序分成4摞(分配),再按此順序再疊放在一起(收集),然後按面值的順序分成13摞(分配),再按此順序疊放在一起(收集),如此進行二次分配和收集即可將撲克牌排列有序。   

  基數排序:是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的

9. 各種排序算法性能比較

  1)各種排序的穩定性,時間複雜度和空間複雜度總結:

 

  改錯:上述快速排序算法的空間複雜度應改爲O(log2n)。

  爲什麼快速排序算法的空間複雜度爲O(log2n)~O(n)?

  快速排序算法的實現需要棧的輔助,棧的遞歸深度爲O(log2n);當整個數列均有序時,棧的深度會達到O(n)。

  我們比較時間複雜度函數的情況:

  2)時間複雜度來說:

  (1)平方階(O(n2))排序
    各類簡單排序:直接插入、直接選擇和冒泡排序;
  (2)線性對數階(O(n*logn))排序
    快速排序、堆排序和歸併排序;
  (3)O(n1+§))排序,§是介於0和1之間的常數

    希爾排序

  (4)線性階(O(n))排序
    基數排序,此外還有桶、箱排序。

  說明:

  (1)當原表有序或基本有序時,直接插入排序和冒泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n);

  (2)而快速排序則相反,當原表基本有序時,將蛻化爲冒泡排序,時間複雜度提高爲O(n^2);

  (3)原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。

  3)穩定性:排序算法的穩定性:若待排序的序列中,存在多個具有相同關鍵字的記錄,經過排序, 這些記錄的相對次序保持不變,則稱該算法是穩定的;若經排序後,記錄的相對次序發生了改變,則稱該算法是不穩定的。 

     穩定性的好處:排序算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以爲第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序算法穩定,可以避免多餘的比較。

  穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序。

  不是穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序。

  4)選擇排序算法準則:

  每種排序算法都各有優缺點。因此,在實用時需根據不同情況適當選用,甚至可以將多種方法結合起來使用。

  選擇排序算法的依據:

  影響排序的因素有很多,平均時間複雜度低的算法並不一定就是最優的。相反,有時平均時間複雜度高的算法可能更適合某些特殊情況。同時,選擇算法時還得考慮它的可讀性,以利於軟件的維護。一般而言,需要考慮的因素有以下四點:

  (1)待排序的記錄數目n的大小;

  (2)記錄本身數據量的大小,也就是記錄中除關鍵字外的其他信息量的大小;

  (3)關鍵字的結構及其分佈情況;

  (4)對排序穩定性的要求。

  設待排序元素的個數爲n.

  (1)當n較大,則應採用時間複雜度爲O(n*logn)的排序方法:快速排序、堆排序或歸併排序。

    快速排序:是目前基於比較的內部排序中被認爲是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;

    堆排序:如果內存空間允許且要求穩定性的;

    歸併排序:它有一定數量的數據移動,所以我們可能過與插入排序組合,先獲得一定長度的序列,然後再合併,在效率上將有所提高。

  (2)當n較大,內存空間允許,且要求穩定性:歸併排序

  (3)當n較小,可採用直接插入或直接選擇排序。

      直接插入排序:當元素分佈有序,直接插入排序將大大減少比較次數和移動記錄的次數。

      直接選擇排序:當元素分佈有序,如果不要求穩定性,選擇直接選擇排序。

  (4)一般不使用或不直接使用傳統的冒泡排序。

  (5)基數排序
    它是一種穩定的排序算法,但有一定的侷限性:
    1、關鍵字可分解;
    2、記錄的關鍵字位數較少,如果密集更好;
    3、如果是數字時,最好是無符號的,否則將增加相應的映射複雜度,可先將其正負分開排序。

轉載自:http://www.cnblogs.com/maybe2030/p/4715042.html#top

發佈了170 篇原創文章 · 獲贊 434 · 訪問量 121萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章