中級程式員必須懂的20大基礎演算法(1)——快速排序

來源:互聯網
上載者:User

演算法的入門級研究一般都是從“排序”和“尋找”開始的。“排序演算法”和她的姊妹“尋找演算法”是很多複雜演算法的基礎,也是很多複雜系統的基礎。比如Linux中最複雜的虛擬記憶體管理就是基於“紅-黑樹”尋找演算法的;Solaris是基於AVL樹尋找演算法;MySQL是基於B樹尋找演算法;P2P技術是基於DHT雜湊演算法……(待補充。。。)

今天推出的中級程式員必須懂的20大基礎演算法專題,通過介紹各種基本排序和尋找演算法,梳理一下電腦演算法的基礎,為後面介紹那些複雜演算法做鋪墊。這裡首先從各種排序演算法的梳理開始.。

對於排序的演算法我想先做一點簡單的比較,首先是感性比較(比較符合我的性格一點),通過編程把《資料結構》中9大經典排序演算法的時間效能進行比較,我的環境是
VC6.0(Release)+win2000pro+128MDDR+P4(1.6G)
因為在多任務作業系統下,系統將進行進程式調度,影響實驗結果。以下是經過稍微修正過的值。如果要取得更準確的值,我們得多次實驗求其平均值
。

排序演算法實驗比較(單位:秒)

 

方法

1K

10K

100K

200K

100K

正序

逆序

冒泡排序

0

0.422

44.790

188.462

0

31.459

冒泡排序2

0

0.281

30.335

131.771

0

27.568

快速排序

0

0

0.016

0.047

5.095

7.002

直接選擇排序

0

0.141

16.878

79.332

16.785

33.242

堆排序

0

0

0.031

0.109

0.031

0.015

直接插入排序

0

0.047

8.705

57.800

0

24.865

Shell排序

0

0

0.047

0.110

0.015

0.015

歸併排序

0

0

0.031

0.094

0.032

0.032

基數排序

0

0

0.47

0.109

0.047

0.046

有了感性認識,我們再來理性認識它:

(1)穩定性比較
 插入排序、冒泡排序、二叉樹排序、二路歸併排序及其他線形排序是穩定的
 選擇排序、希爾排序、快速排序、堆排序是不穩定的

(2)時間複雜性比較
 插入排序、冒泡排序、選擇排序的時間複雜性為O(n^2)
 其它非線形排序的時間複雜性為O(nlog2n)
 線形排序的時間複雜性為O(n)(線性排序包括計數排序、基數排序後面會介紹);

(3)輔助空間的比較
 線形排序、二路歸併排序的輔助空間為O(n),其它排序的輔助空間為O(1);

(4)其它比較
插入、冒泡排序的速度較慢,但參加排序的序列局部或整體有序時,這種排序能達到較快的速度。反而在這種情況下,快速排序反而慢了。
當n較小時,對穩定性不作要求時宜用選擇排序,對穩定性有要求時宜用插入或冒泡排序。
若待排序的記錄的關鍵字在一個明顯有限範圍內時,且空間允許是用桶排序。
當n較大時,關鍵字元素比較隨機,對穩定性沒要求宜用快速排序。
當n較大時,關鍵字元素可能出現本身是有序的,對穩定性有要求時,空間允許的情況下。宜用歸併排序。
當n較大時,關鍵字元素可能出現本身是有序的,對穩定性沒有要求時宜用堆排序。

綜上,我們編程時,最常見的情況是n較大時,元素比較隨機,但對穩定性一般不作要求,所以引出快速排序。 

快速排序的演算法思想如下:首先檢查資料列表中的資料數,如果小於兩個,則直接退出程式。如果有超過兩個以上的資料,就選擇一個分割點將資料分成兩個部分,小於分割點的資料放在一組,其餘的放在另一組,然後分別對兩組資料排序。 通常分割點的資料是隨機選取的。這樣無論你的資料是否已被排列過,你所分割成的兩個字列表的大小是差不多的。而只要兩個子列表的大小差不多。

#include <iostream.h> void run(int* pData,int left,int right) {     int i,j;     int 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); } void QuickSort(int* pData,int Count) {      run(pData,0,Count-1); } void main() {     int data[] = {10,9,8,7,6,5,4};     QuickSort(data,7);     for (int i=0;i<7;i++)         cout<<data[i]<<" ";     cout<<"/n"; } 

快速排序是冒泡排序的改進,它通過一次交換能消除多個逆序,這樣可以減少逆序時所消耗的掃描和資料交換次數。在最優情況下,它的排序時間複雜度為O(nlog2n)。即每次劃分序列時,能均勻分成兩個子串。但最差情況下它的時間複雜度將是O(n^2)。即每次劃分子串時,一串為空白,另一串為m-1(程式中的100K正序和逆序就正是這樣,如果程式中採用每次取序列中部資料作為劃分點,那將在正序和逆時達到最優)。從100K中正序的結果上看“快速排序”會比“冒泡排序”更慢,這主要是“冒泡排序”中採用了提前結束排序的方法。有的書上這解釋“快速排序”,在理論上講,如果每次能均勻劃分序列,它將是最快的排序演算法,因此稱它作快速排序。雖然很難均勻劃分序列,但就平均效能而言,它仍是基於關鍵字比較的內部排序演算法中速度最快者。

 

冒泡排序的核心思想是掃描資料清單,尋找出現亂序的兩個相鄰的項目。當找到這兩個項目後,交換項目的位置然後繼續掃描。重複上面的操作直到所有的項目都按順序排好:

#include <iostream.h> void BubbleSort(int* pData,int Count) {     int iTemp;     for(int i=1;i<Count;i++)     {         for(int j=Count-1;j>=i;j--)         {             if(pData[j]<pData[j-1])             {                 iTemp = pData[j-1];                 pData[j-1] = pData[j];                 pData[j] = iTemp;             }         }     } } void main() {     int data[] = {10,9,8,7,6,5,4};     BubbleSort(data,7);     for (int i=0;i<7;i++)         cout<<data[i]<<" ";     cout<<"/n"; } 

在最優情況下只需要經過n-1次比較即可得出結果,(這個最優情況那就是序列己是正序,從100K的正序結果可以看出結果正是如此),但在最壞情況下,即倒序(或一個較小值在最後),下沉演算法將需要n(n-1)/2次比較。所以一般情況下,特別是在逆序時,它很不理想。它是對資料有序性非常敏感的排序演算法。
 
通常的冒泡是單向的,還有一種雙向的冒泡演算法,也就是說還要進行反向的工作。雙向冒泡代碼看起來複雜,仔細理一下就明白了,是一個來回震蕩的方式。寫這段代碼的作者認為這樣可以在冒泡的基礎上減少一些交換:

#include <iostream.h> void Bubble2Sort(int* pData,int Count)  {     int iTemp;     int left = 1;     int right =Count -1;     int t;     do     {         //正向的部分         for(int i=right;i>=left;i--)         {            if(pData[i]<pData[i-1])            {                iTemp = pData[i];                pData[i] = pData[i-1];                pData[i-1] = iTemp;                t = i;            }         }         left = t+1;         //反向的部分         for(i=left;i<right+1;i++)         {             if(pData[i]<pData[i-1])             {                 iTemp = pData[i];                 pData[i] = pData[i-1];                 pData[i-1] = iTemp;                 t = i;             }         }         right = t-1;     }while(left<=right); } void main() {     int data[] = {10,9,8,7,6,5,4};     Bubble2Sort(data,7);     for (int i=0;i<7;i++)         cout<<data[i]<<" ";     cout<<"/n"; } 

它是冒泡排序的改良(一次下沉再一次上浮),最優情況和最壞情況與冒泡排序差不多,但是一般情況下它要好過冒泡排序,它一次下沉,再一次上浮,這樣避免了因一個數的逆序,而造成巨大的比較。如(2,3,4,…,n-1,n,1),用冒泡排序需要n(n-1)/2次比較,而此排序只要3輪,共比較(n-1)+(n-2)+(n-3)次,第一輪1將上移一位,第二輪1將移到首位,第三輪將發現無資料交換,序列有序而結束。但它同樣是一個對資料有序性非常敏感的排序演算法,只適合於資料基本有序的排序。

 

介紹完了快速排序以及其穩定版的冒泡排序,接下來附帶介紹三個簡單的比較排序。這三個方法很簡單,作為跟快速排序對比的排序方法。

(1)直接選擇排序

 演算法思想:首先找到資料清單中的最小的資料,然後將這個資料同第一個資料交換位置;接下來找第二小的資料,再將其同第二個資料交換位置,以此類推。

這種方法類似我們人為的排序習慣:從資料中選擇最小的同第一個值交換,在從省下的部分中選擇最小的與第二個交換,這樣往複下去。

#include <iostream.h> void SelectSort(int* pData,int Count) {     int iTemp;     int iPos;     for(int i=0;i<Count-1;i++)     {         iTemp = pData[i];         iPos = i;         for(int j=i+1;j<Count;j++)         {             if(pData[j]<iTemp)             {                 iTemp = pData[j];                 iPos = j;             }         }         pData[iPos] = pData[i];         pData[i] = iTemp;     } } void main() {     int data[] = {10,9,8,7,6,5,4};     SelectSort(data,7);     for (int i=0;i<7;i++)         cout<<data[i]<<" ";     cout<<"/n"; } 

簡單的選擇排序,它的比較次數一定:n(n-1)/2。也因此無論在序列何種情況下,它都不會有優秀的表現(從上100K的正序和反序資料可以發現它耗時相差不多,相差的只是資料移動時間),可見對資料的有序性不敏感。它雖然比較次數多,但它的資料交換量卻很少。所以我們將發現它在一般情況下將快於冒泡排序。

(2)直接插入排序

 插入法較為複雜,它的基本工作原理是抽出牌,在前面的牌中尋找相應的位置插入,然後繼續下一張。演算法思想:經過i-1遍處理後,L[1..i-1]己排好序。第i遍處理僅將L[i]插入L[1..i-1]的適當位置,使得L[1..i]又是排好序的序列。要達到這個目的,我們可以用順序比較的方法。首先比較L[i]和L[i-1],如果L[i-1]≤ L[i],則L[1..i]已排好序,第i遍處理就結束了;否則交換L[i]與L[i-1]的位置,繼續比較L[i-1]和L[i-2],直到找到某一個位置j(1≤j≤i-1),使得L[j]
≤L[j+1]時為止。

 

#include <iostream.h> void InsertSort(int* pData,int Count) {     int iTemp;     int iPos;     for(int i=1;i<Count;i++)     {         iTemp = pData[i];         iPos = i-1;         while((iPos>=0) && (iTemp<pData[iPos]))         {             pData[iPos+1] = pData[iPos];             iPos--;         }         pData[iPos+1] = iTemp;     } } void main() {     int data[] = {10,9,8,7,6,5,4};     InsertSort(data,7);     for (int i=0;i<7;i++)         cout<<data[i]<<" ";     cout<<"/n"; } 

簡單的插入排序,每次比較後最多移掉一個逆序,因此與冒泡排序的效率相同。但它在速度上還是要高點,這是因為在冒泡排序下是進行值交換,而在插入排序下是值移動,所以直接插入排序將要優於冒泡排序。直接插入法也是一種對資料的有序性非常敏感的一種演算法。在有序情況下只需要經過n-1次比較,在最壞情況下,將需要n(n-1)/2次比較。

(3)歸併排序

歸併演算法(merge),也叫合并演算法,指的是將兩個已經排序的序列合并成一個序列的操作。   
如 設有數列{6,202,100,301,38,8,1}   
初始狀態: [6] [202] [100] [301] [38] [8] [1]
比較次數  i=1 [6 202 ] [ 100 301] [ 8 38] [ 1 ]    3   
           i=2 [ 6 100 202 301 ] [ 1 8 38 ]         4   
           i=3 [ 1 6 8 38 100 202 301 ]              4   
總計: 11次

void MergeSort(int array[], int first, int last){   int mid = 0;   if(first<last)   {       mid = (first+last)/2;       MergeSort(array, first, mid);       MergeSort(array, mid+1,last);       Merge(array,first,mid,last);   }} 

 

我們可以看到,歸併演算法運用了遞迴非常簡單,歸併排序是一種非就地排序,將需要與待排序序列一樣多的輔助空間。在使用它對兩個己有序的序列歸併,將有無比的優勢。其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlog2n)。對資料的有序性不敏感。若資料節點資料量大,那將不適合。但可改造成索引操作,效果將非常出色。

最後,我在網上找到一個最漂亮的快速排序代碼,使用C++模板類來寫的,以作為本篇文章的結尾:

//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"; } 

 

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.