排序算法詳解 以及各類比較


一)排序算法穩定性劃分:

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

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


二)時間複雜度比較:

冒泡法:  複雜度爲O(n*n)。當數據爲正序,將不會有交換。複雜度爲O(0)。

直接插入排序:O(n*n)

選擇排序:O(n*n)

快速排序:平均時間複雜度log2(n)*n,所有內部排序方法中最高好的,大多數情況下總是最好的。

歸併排序:log2(n)*n

堆排序:log2(n)*n

希爾排序:算法的複雜度爲n的1.3次冪


簡單分析一下常見的排序算法的穩定性,每個都給出簡單的理由。

  (1)冒泡排序

       冒泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以冒泡排序是一種穩定排序算法。

(2)選擇排序

     選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裏面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因爲只剩下它一個最大的元素了。那麼,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序算法。

(3)插入排序
    插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。

(4)快速排序
   快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的數組下標,一般取爲數組第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列爲 5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序算法,不穩定發生在中樞元素和a[j]交換的時刻。

(5)歸併排序
   歸併排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認爲直接有序)或者2個序列(1次比較和交換),然後把各個有序的段序列合併成一個有序的長序列,不斷合併直到原序列全部排好序。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程中,穩定是是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序算法。

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

(7)希爾排序(shell)
   希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

(8)堆排序
  我們知道堆的結構是節點i的孩子爲2*i和2*i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長爲n的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當爲n/2-1, n/2-2, ...1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序算法



1 快速排序(QuickSort)

快速排序是一個就地排序,分而治之,大規模遞歸的算法。從本質上來說,它是歸併排序的就地版本。快速排序可以由下面四步組成。

(1) 如果不多於1個數據,直接返回。
(2) 一般選擇序列最左邊的值作爲支點數據。
(3) 將序列分成2部分,一部分都大於支點數據,另外一部分都小於支點數據。
(4) 對兩邊利用遞歸排序數列。

快速排序比大部分排序算法都要快。儘管我們可以在某些特殊的情況下寫出比快速排序快的算法,但是就通常情況而言,沒有比它更快的了。快速排序是遞歸的,對於內存非常有限的機器來說,它不是一個好的選擇。

2 歸併排序(MergeSort)

歸併排序先分解要排序的序列,從1分成2,2分成4,依次分解,當分解到只有1個一組的時候,就可以排序這些分組,然後依次合併回原來的序列中,這樣就可以排序所有數據。合併排序比堆排序稍微快一點,但是需要比堆排序多一倍的內存空間,因爲它需要一個額外的數組。

3 堆排序(HeapSort)

堆排序適合於數據量非常大的場合(百萬數據)。

堆排序不需要大量的遞歸或者多維的暫存數組。這對於數據量非常巨大的序列是合適的。比如超過數百萬條記錄,因爲快速排序,歸併排序都使用遞歸來設計算法,在數據量非常大的時候,可能會發生堆棧溢出錯誤。

堆排序會將所有的數據建成一個堆,最大的數據在堆頂,然後將堆頂數據和序列的最後一個數據交換。接下來再次重建堆,交換數據,依次下去,就可以排序所有的數據。

4 Shell排序(ShellSort)

Shell排序通過將數據分成不同的組,先對每一組進行排序,然後再對所有的元素進行一次插入排序,以減少數據交換和移動的次數。平均效率是O(nlogn)。其中分組的合理性會對算法產生重要的影響。現在多用D.E.Knuth的分組方法。

Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相對比較簡單,它適合於數據量在5000以下並且速度並不是特別重要的場合。它對於數據量較小的數列重複排序是非常好的。

5 插入排序(InsertSort)

插入排序通過把序列中的值插入一個已經排序好的序列中,直到該序列的結束。插入排序是對冒泡排序的改進。它比冒泡排序快2倍。一般不用在數據大於1000的場合下使用插入排序,或者重複排序超過200數據項的序列。

6 冒泡排序(BubbleSort)

冒泡排序是最慢的排序算法。在實際運用中它是效率最低的算法。它通過一趟又一趟地比較數組中的每一個元素,使較大的數據下沉,較小的數據上升。它是O(n^2)的算法。

7 交換排序(ExchangeSort)和選擇排序(SelectSort)

這兩種排序方法都是交換方法的排序算法,效率都是 O(n2)。在實際應用中處於和冒泡排序基本相同的地位。它們只是排序算法發展的初級階段,在實際中使用較少。

8 基數排序(RadixSort)

基數排序和通常的排序算法並不走同樣的路線。它是一種比較新穎的算法,但是它只能用於整數的排序,如果我們要把同樣的辦法運用到浮點數上,我們必須瞭解浮點數的存儲格式,並通過特殊的方式將浮點數映射到整數上,然後再映射回去,這是非常麻煩的事情,因此,它的使用同樣也不多。而且,最重要的是,這樣算法也需要較多的存儲空間。

9 總結

下面是一個總的表格,大致總結了我們常見的所有的排序算法的特點。

排序法平均時間最差情形穩定度額外空間備註
冒泡O(n2)    O(n2)穩定O(1)n小時較好
交換    O(n2)    O(n2)不穩定O(1)n小時較好
選擇O(n2)O(n2)不穩定O(1)n小時較好
插入O(n2)O(n2)穩定O(1)大部分已排序時較好
基數O(logRB)O(logRB)穩定O(n)

B是真數(0-9),

R是基數(個十百)

ShellO(nlogn)O(ns) 1<s<2不穩定O(1)s是所選分組
快速O(nlogn)O(n2)不穩定O(nlogn)n大時較好
歸併O(nlogn)O(nlogn)穩定O(1)n大時較好
O(nlogn)O(nlogn)不穩定O(1)n大時較好

一個基於模板的通用排序:166L889MKV83.jpgMy


myData.h

///////////////////////////////////////////////////////

class CMyData
{
public:
   CMyData(int Index,char* strData);
   CMyData();
   virtual ~CMyData();

   int m_iIndex;
   int GetDataSize(){ return m_iDataSize; };
   const char* GetData(){ return m_strDatamember; };
   //這裏重載了操作符:
   CMyData& operator =(CMyData &SrcData);
   bool operator <(CMyData& data );
   bool operator >(CMyData& data );

private:
   char* m_strDatamember;
   int m_iDataSize;
};
////////////////////////////////////////////////////////

MyData.cpp文件
////////////////////////////////////////////////////////
CMyData::CMyData():
m_iIndex(0),
m_iDataSize(0),
m_strDatamember(NULL)
{
}

CMyData::~CMyData()
{
   if(m_strDatamember != NULL)
     delete[] m_strDatamember;
   m_strDatamember = NULL;
}

CMyData::CMyData(int Index,char* strData):
m_iIndex(Index),
m_iDataSize(0),
m_strDatamember(NULL)
{
   m_iDataSize = strlen(strData);
   m_strDatamember = new char[m_iDataSize+1];
   strcpy(m_strDatamember,strData);
}

CMyData& CMyData::operator =(CMyData &SrcData)
{
   m_iIndex = SrcData.m_iIndex;
   m_iDataSize = SrcData.GetDataSize();
   m_strDatamember = new char[m_iDataSize+1];
   strcpy(m_strDatamember,SrcData.GetData());
   return *this;
}

bool CMyData::operator <(CMyData& data )
{
   return m_iIndex<data.m_iIndex;
}

bool CMyData::operator >(CMyData& data )
{
   return m_iIndex>data.m_iIndex;
}
///////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////
//主程序部分
#include <iostream.h>
#include "MyData.h"

template <class T>
void run(T* pData,int left,int right)
{
   int i,j;
   T middle,iTemp;
   i = left;
   j = right;
   //下面的比較都調用我們重載的操作符函數
   middle = pData[(left+right)/2]; //求中間值
   do{
     while((pData[i]<middle) && (i<right))//從左掃描大於中值的數
       i++;      
     while((pData[j]>middle) && (j>left))//從右掃描大於中值的數
       j--;
     if(i<=j)//找到了一對值
     {
       //交換
       iTemp = pData[i];
       pData[i] = pData[j];
       pData[j] = iTemp;
       i++;
       j--;
     }
   }while(i<=j);//如果兩邊掃描的下標交錯,就停止(完成一次)

   //當左邊部分有值(left<j),遞歸左半邊
   if(left<j)
     run(pData,left,j);
   //當右邊部分有值(right>i),遞歸右半邊
   if(right>i)
     run(pData,i,right);
}

template <class T>
void QuickSort(T* pData,int Count)
{
   run(pData,0,Count-1);
}

void main()
{
   CMyData data[] = {
     CMyData(8,"xulion"),
     CMyData(7,"sanzoo"),
     CMyData(6,"wangjun"),
     CMyData(5,"VCKBASE"),
     CMyData(4,"jacky2000"),
     CMyData(3,"cwally"),
     CMyData(2,"VCUSER"),
     CMyData(1,"isdong")
   };
   QuickSort(data,8);
   for (int i=0;i<8;i++)
     cout<<data[i].m_iIndex<<" "<<data[i].GetData()<<"/n";
   cout<<"/n";
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章