六大排序算法之 PHP和C++實現 - 算法思路解析

前言:

一直以來對於排序算法總有些熟悉又陌生的感覺,這幾天看到一篇挺不錯的博客講排序的,http://blog.csdn.net/xiazdong/article/details/8462393 於是學習參考他的思路自己動手用php實現了一下。並且結合每個算法的特性思路寫了這篇博客。

源碼

源碼可以直接在github上down,也歡迎修改 https://github.com/fangkehang/php_sort
最好是結合代碼看思路,會比較清楚。

概念

一、 In-place sort/Out-place sort

· In-place sort(不佔用額外內存或佔用常數的內存):插入排序、選擇排序、冒泡排序、堆排序、快速排序。
· Out-place sort:歸併排序、計數排序、基數排序、桶排序。
當需要對大量數據進行排序時,In-place sort就顯示出優點,因爲只需要佔用常數的內存。
· 設想一下,如果要對10000個數據排序,如果使用了Out-place sort, 則假設需要用200G的額外空間,則一臺老式電腦會吃不消,但是如果使用In-place sort,則不需花費額外內存。

二、stable sort/unstable sort

· stable sort:插入排序、冒泡排序、歸併排序、計數排序、基數排序、桶排序。
· unstable sort:選擇排序(5 8 5 2 9)、快速排序、堆排序。
穩定性是指兩個一樣的元素排位是否穩定,這對於基數排序特別重要

三、 冒泡排序和插入排序哪個更快?

答:插入執行時間至少比冒泡排序快,插入排序的速度直接是逆序對的個數,而冒泡排序中執行“交換“的次數是逆序對的個數,因此冒泡排序執行的時間至少是逆序對的個數, 下面在算法中會更加清楚的解釋說明。

一、選擇排序

介紹:選擇排序是所有排序中最簡單的,相信很多沒有接觸排序算法的程序員都可以自己想出這個排序。
思路:思路很清晰,每一次內層遍歷找出最小的數,然後進行n-1次,找出n-1個最小的依次排好即可。
這裏寫圖片描述

遞歸版:遞歸版也是相當直觀,因爲每次照最小的步驟都是一樣的,所以可以用一個function表示,然後每一次遞歸調用這個function即可。
算法特性:In-place sort,unstable sort
時間複雜度分析:無論正序逆序亂序,總之兩層循環必不可少,所以它的最佳最差複雜度都是O(n^2)。雖然這個算法從思路到實現都很容易,但性能是最差的,所以學過的算法的程序猿們就不能再用這個方法了。

/**
 *  選擇排序  
 **/
void select(int a[], int num) 
{
    for(int i = 0; i<num; i++) {
        for(int j = i+1; j<num ; j++) {
            if(a[i] > a[j]) {
                exchange(&a[i], &a[j]);
            }
        }
    }
}


二、 冒泡排序

介紹:顧名思義,這個算法是仿照現實生活中水中氣泡,小氣泡先冒出,大氣泡後冒出的思想產生的。
思路:每一次都浮出一個最小的數,所以外層需要n-1次,內層從後往前,每相鄰兩個進行比較,當後面的小於前面時,就交換,這樣就可以把最小的數一直排到前方去了。
第一趟找最小值

遞歸版:遞歸版的思路跟選擇排序是一樣的,每一次的冒泡實際上是同樣的操作,所以遞歸調用function即可。
改進版:這個算法還有個改進之處,觀察上圖,你是不是發現從3到n實際上是排好序了的。對,結合這個思路,只要你在某一趟冒泡中,發現不需要進行任何一次交換,這就說明其實這個數列就已經是有序了,可以提前結束算法。這個用程序來實現可以通過一個flag變量來標識,單有交換把flag設爲true,每一趟遍歷結束要是發現flag爲false,就可以提前結束程序了。
算法特性:stable sort、In-place sort
時間複雜度分析未改進時候:無論正序逆序亂序,兩趟遍歷是必不可少的,所以最佳最差複雜度也是O(n^2). 然而,改進之後,當正序時,外層就不用遍歷了,在內層遍歷一次就可以發現不需要交換,所以已經是有序就提前結束算法,所以最佳的複雜度是O(n),這種情況就會跟插入排序是一個效率的。

/**
 *  冒泡排序
 *  @param flag: 當一趟跑完,發現不需要任何冒泡交換,說明已經有序,提前結束算法。  
 **/
void bubble(int a[], int num) 
{
    int i ,flag = 1;
    for(i = 0; i<num; i++) {
        if(flag == 0) {
            return;
        }
        flag = 0;
        for(int j = num;j > i;j--) {
            cout << j ; 
            if(a[j] < a[j-1]) {
                exchange(&a[j],&a[j-1]);
                flag = 1;
            }
        }
        var_dump(a,5);
    }    
}


三、 插入排序

介紹: 插入排序實際上我們在生活中就經常運用,大多數人撲克牌的洗牌就是採用這種方法。
思路:從第二個數往後開始遍歷,遍歷到的這個數之前的數列是已經排好序了的,所以把遍歷到的這個數和之前最後一個數比較,比他小就跟他交換,比他大就停止。
這裏寫圖片描述

/**
 *  插入排序
 */
void insert(int a[],int num)
{
    int i,j;
    for(i = 1; i<=num; i++) {       
        for(j = i; j>=0 ; j--) {
            if(a[j] < a[j-1]) {
                exchange(&a[j],&a[j-1]);
            }else {
                break;
            }
        }
    }
}

遞歸版:遞歸版中我們要先假設之前的n-1個數列已經採用這個插入法排好序,所以現在只需把第n個插入過去即可。
算法特性:stable sort、In-place sort
時間複雜度分析當數組是正序的話,我們發現每一次的外層遍歷,遍歷到的數與前一個數比較之後不需要交換就進入下一個數了,所以內層循環不需要執行,所以最佳複雜度是O(n);而最差的是逆序,每一個內層循環都要走到底。爲O(n^2),所以它和冒泡改進版的效率相當。

四、歸併排序

介紹:歸併排序是個非常有效率的排序,但它的思想和實現不是特別直觀,這個算法的地位也是特別的重要。
思路:採用的是分治的思想,實現則考慮用遞歸實現比較快捷方便。
歸併就是把兩個已有序的數組,併成一個,思路就是用兩個temp的數組來存放左右兩個有序數組,並使得它們最後一個數MAX_INT,然後再重新整合到原來的數組中去。

/**
 *  將兩個有序的數列歸併,採用的思路是用兩個temp的數組來存放左右兩個有序數組,並使得它們最後一個數MAX_INT,然後再重新整合到原來的數組中去。
 *
 **/
void merge(int a[], int start, int middle, int end) {
    int i,j,k;

    int l[middle - start + 1 + 1],r[end - middle + 1]; //多出一位來放一個INT_MAX
    for(i = start,k = 0; i <= middle; i++,k++) {
        l[k] = a[i];
    }
    l[k] = INT_MAX; 
    for(j = middle+1,k = 0; j <= end; j++,k++) {
        r[k] = a[j];
    }
    r[k] = INT_MAX;
    for(i = start,k = 0,j = 0;i<=end;i++) {
        if(r[k]<l[j]) {
            a[i] = r[k];
            k++;
        }else{
            a[i] = l[j];
            j++;
        }
    }
} 

/**
 *  歸併排序
 */
void merge_sort(int a[], int start, int end)
{
    if(start < end) {  //這步遞歸退出條件要記得
        int middle = (end - start) / 2 + start;
        merge_sort(a,start,middle);
        merge_sort(a,middle+1,end);
        merge(a,start,middle,end);
    }
}

遞歸的思想就是 先把數列分兩個,然後假設左右兩邊都用歸併排好序了,這時候只需要把這兩個歸併,歸併的思想從上圖可知。
算法特性:stable sort、Out-place sort
時間複雜度分析
首先先看merge 歸併兩個有序數列的算法的時間複雜度,可見這個時間複雜度是O(n),兩個數列實際上是同時一起遍歷的。
再假設歸併一個n位的數列需要T(n)的時間,那麼歸併一個n/2,則需要T(n/2)的時間。OK,根據歸併的遞歸式:
T(n) = 2T(n/2)+O(n),這裏套用別人非常詳盡的一個解釋:來源於http://blog.csdn.net/yuzhihui_no1/article/details/44198701#t2
這裏寫圖片描述

因此,這個算法的時間複雜度是相當低的。

五、快速排序

介紹:快速排序是和歸併同等級別的重要算法。
思路:思想也是分治法,每次選定左邊的數做基準,從右往左找比基準小的數,再從左往右找比基準大的數,注意順序不能顛倒。然後交換這兩個。依次下去直到最後一個跟基準交換。就形成基準左邊比它小,右邊比它大。
這裏寫圖片描述

這個算法採用遞歸來做更加快捷,只需先parition,分成左邊比基準小,右邊比基準大,再把左邊右邊遞歸去解決。
算法特性:unstable sort、In-place sort
時間複雜度分析
最優的情況就是剛好選到中分的點,這個時候partition只要找一次,換一下就可以了。
T[n] = 2T[n/2] +O(n) 所以O( nlogn )
最差的情況就是當正序或者倒序的時候,這個時候就相當於冒泡排序,O(n^2)
這裏寫圖片描述
如圖這個樹,左右兩邊是對稱的,所以深度是4,但如果樹頂是20,可想而知,深度將不再是4。就是這個道理。

/**
 *  快排
 **/
int partition(int a[], int start, int end) {
    //random 隨機化降低最壞情況出現的頻率
    int randNum = rand()%(end - start);
    exchange(&a[start],&a[randNum + start]);

    int i = start, j = end, temp = a[start];
    while(i<j) {
        //一定要先從後面開始
        while(i < j && a[j] > temp) {
            j--;
        }
        if(i < j) {
            a[i] = a[j];
            i++;
        }
        while(i < j && a[i] < temp) {
            i++;
        }
        if(i < j) {
            a[j] = a[i];
            j--;
        }
    }
    a[i] = temp;
    return i;
}

void quick_sort(int a[], int start,int end)
{
    if(start < end) {
        int r = partition(a,start,end);
        quick_sort(a,start,r-1);
        quick_sort(a,r+1,end);
    }
}

六、堆排序

介紹:堆排序是選擇排序的一種,堆排序有大根堆和小根堆之分,原理則是一樣的。
思路:

算法特性:unstable sort、In-place sort
時間複雜度分析最優時間:O(nlgn) 最差時間:O(nlgn)。這個算法性能是相當好的,但是代碼量也相對較大。

首先是建立大根堆的演示和思路:

這裏寫圖片描述

即從最後一個葉子結點開始,到第一個結點。每次調整這些非葉子結點和自己的子孫(是子孫,不僅僅是兒子)的大小關係。

void BuildMaxHeap(ElemType A[], int len) {
    for(int i=len/2;i>0;i--) //從葉子結點開始
        AdjustDown(A,i,len);
}
void AdjustDown(ElemType A[],int k, int len) {
    A[0] = A[k];
    for(i = 2* k;i<=len;i*=2){ //遍歷它的子孫結點
        if(i<len&&A[i]<A[i+1])
            i++;
        if(A[0]>=A[j]) break;
        else{
            A[k] = A[i];
            k = i;
        }
    }
    A[k] = A[0];
}

實際上只要把建立的過程給理解清楚了,後面排序的操作就會變得簡單易懂

void HeapSort(ElemType A[],int len) {
    BuildMaxHeap(A,len);
    for(i=len;i>1;i--) {
        Swap(A[i],A[1]); //把最大的值棧頂調到最後一個去,相當於輸出,因爲下一步會把len縮小i-1;
        AdjustDown(A,1,i-1);
    }
}

此外堆還可以進行插入和刪除操作。
對堆進行插入操作時,先將新結點放在堆的末端,再對這個新結點執行向上調整操作

void AdjuestUp(ElemType A[],int k) {
    A[0] = A[k];
    int i=k/2;
    while(i>0&&A[i]<A[0]) {
        A[k] = A[i];
        k = i;
        i = k/2;
    }
    A[k] = A[o];
}


七、希爾排序

未完待續

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