1. 程式人生 > >對排序算法的初步探究

對排序算法的初步探究

更新 治法 binary 整體 amp spa 下劃線 第一個 內部實現

初學排序算法,我覺得只需要掌握算法的精髓,沒必要把所有算法都實現一遍,下面我會實現一些經典的排序算法。(均采用C++實現)

學習的排序算法包含:

1》插入排序(直接插入排序、希爾排序)

2》選擇排序(簡單選擇排序、堆排序)

3》交換排序(快速排序、冒泡排序)

4》歸並排序

5》基數排序

我認為初學者掌握基本的排序算法的思想即可,其他排序算法基於特定的數據結構和存儲結構,遇到具體的實例再學習即可。

下面就開始學習了。

插入排序

插入排序:把一個數插入到一個有序的序列中,並要求插入後此數據序列仍然有序。這種排序思想就是插入排序。

直接插入排序:把余下元素一個一個插入有序表中,每次都從有序表的最後一個元素開始比較,若是小於該元素:data<a[i],則再與前一個元素比較data[i-1],...直到找到合適位置,最後把該位置及其以後的元素都向後移動:a[j]=a[j-1],騰出一個位置讓新元素插入。這也是最簡單的插入排序算法。

下面是直接插入排序的簡單實現:

void insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1)
    return;
    for(int i=1;i<n;i++)
    {
        /* 存放當前無序的初始序號,
        *初始先將數組的第一個元素構成有序序列,
        *將數組的後面的序列看成無序序列,
        *從數組的第二個元素開始做插入排序
        *註意:有序序列已經是從大到小排好的
        */
        int j=i;
        int temp=a[i];
        
while(j && temp<a[j-1]) //一直找到在有序序列中的位置,記錄在j中 { a[j]=a[j-1]; //不斷將數組元素往後移動,騰出位置 j--; } a[j]=temp; //插入temp,即存放的無序序列中的第一個值 } }

交換插入排序:可以在比較的過程中,直接交換前後位置的元素,一步一步地把插入的元素交換到合適的位置。

void insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1
) return; for(int i=1;i<n;i++) { int j=i; //一邊插入一邊比較交換排序,不需要元素後移騰出空間。 while(j && temp<a[j-1]) { int temp=a[i]; a[j] = a[j-1]; a[j-1] = temp; j--; } } }

折半插入排序:在直接插入中,我們每次都是把一個元素插入到一個有序的序列中。為了使找到位置更高效,我們可以借鑒二分查找的方法,減少比較次數,快速找到合適的插入位置。這就是折半插入的來歷。

void binary_insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1) return;
    int low,high,mid; 
    for(int i=1;i<n;i++)
    {
        low = 0;
        high = i-1;
        while(low <= high) //采用左閉右閉的區間
        {
            mid = low + (high - low)>>1; //不寫成/2是為了防止溢出
            if(a[i]>a[mid])   //不斷的判斷條件,來改變low,mid,high
                low = mid + 1;  
            else
                high = mid - 1;
            
        }  //循環結束的low值就是這個元素插入的位置,至於為什麽,自己畫個程序的流程就明白了。值就是這個元素插入的位置,至於為什麽,自己畫個程序的流程就明白了。
        
        int temp = a[i];  
        //不斷的將元素後移
        for(int j=i;j>low;j--)
            a[j] = a[j-1];
        a[low] = temp;
    }
    
}

希爾排序

我們知道當一個序列基本有序時,直接插入會變得很高效。因為此時只需少量的移動元素,操作集中在元素的比較上。基於這種想法,我們就試圖把一個序列在進行直接插入前調整得盡量有序。這就是希爾排序(Shell Sort)的核心思路。(Shell只是算法發明者的名字,無特殊含義)

那到底該怎麽做呢?

希爾排序一反以前的做法,插入為何只在相鄰的元素之間進行,不相鄰的同樣可以進行。於是,希爾排序也被形象地稱為”跳著插“。

那應該隔幾個元素進行插入呢?

這就說到希爾排序的另一個名字:縮小增量排序(Diminishing Increment Sort)。實際上這個增量序列或者說步長序列是可以提前指定的。不同的序列可以極大地影響到算法效率,至於最好的步長序列貌似還在研究。不過可以肯定的是最後的一個步長一定是1。因為最後一次是直接插入。

先來看一種最簡單、也最常用的步長序列:n/2、n/2/2、... 1 (n是待排序的元素個數)。也是就說,初始步長是n/2,以後每次減半,直到步長為1。

利用這種步長序列,舉一個例子:開始序列中加下劃線的字體表示每一趟待排序的數字。

原序列: 21 12 4 9 9 5 78 1 (n=8)

下標: 0 1 2 3 4 5 6 7

第一趟:步長step=4,0、4號元素直接插入排序

開始 21 12 4 9 9 5 78 1

結束 9 12 4 9 21 5 78 1

第二趟:步長step=2, 0、2、4、6號元素直接插入排序

開始 9 12 4 9 21 5 78 1

結束 4 12 9 9 21 5 78 1

第三趟:步長step=1,0、1、2、3、4、5、6、7、8號元素直接插入排序(顯然這是整體直接插入排序)

開始 4 12 9 9 21 5 78 1

結束 1 4 5 9 9 12 21 78

如何理解每一趟排序:

  1. 步長序列的長度決定了排序的趟數。
  2. 每一趟排序,並不是所有元素都參與。參與排序的只能是下標為 0、step、2*step...的元素。
  3. 插入排序時,采用何種策略。如直接插入、交換插入、折半插入、表插入或者表折半插入,這可任意選擇。
  4. 最後一趟排序的步長一定是1。由第二點可知,最後一趟,全體元素都參與排序。

排序結果中紅色9出現在了黑色9的前面,表明希爾排序是不穩定的。

希爾排序的簡單實現:

void ShellSort(int *a,int n)
{
    if(a == NULL || n <= 1)
    return;
    for(int step = n/2;step >= 1;step = step/2) //規定步長
    {
        //下面采用交換插入排序算法
        for(int i=step;i<n;i+=step)
            for(int j=i;j>0;j-=step)
            if(a[j]<a[j-step])
                swap(a[j],a[j-step]);
    }
}

插入排序算法的個人感受:整體來說,直接插入排序的算法時間復雜度最壞情況下為O(n^2),空間復雜度可以忽略不計。而采用改進後的插入排序算法(包括希爾排序)只是減少元素移動的個數,提高排序的效率。

選擇排序(select sort)

簡單選擇排序

經過一趟排序,可以從n-i+1(i=1,2...)個記錄中選取關鍵字最小的記錄作為有序序列中第i個記錄。也就是說,每一趟排序,都會排好一個元素的最終位置。

最簡單的是簡單選擇排序。

簡單選擇排序(Simple Selection Sort,也叫直接選擇排序)

簡單選擇排序的思想:在每一趟排序中,通過n-i次關鍵字的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i個記錄交換,以此確定第i個記錄的最終位置。簡單說,逐個找出第i小的記錄,並將其放到數組的第i個位置。

下面是簡單交換排序的算法實現:

void SimpleSelectSort(int *a,int n)
{
    if(a == NULL && n <= 1)
        return;
    int index; //用來記錄關鍵字最小的數組元素的下標
    for(int i=0;i<n;i++)
    {
        index = i; 
        for(int j=i+1;j<n;j++) //遍歷數組的後面的元素找到關鍵字最小的元素的下標
        {
            if(a[j]<a[index])
                index = j;
        }
        if(i != index) //當最小關鍵字的元素的下標和當前的元素不同的時候進行交換
        {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }
}

堆排序

堆排序(Heap Sort):使用堆這種數據結構來實現排序。

先看下堆的定義:

最小堆(Min-Heap)是關鍵碼序列{k0,k1,…,kn-1},它具有如下特性:

ki<=k2i+1,

ki<=k2i+2(i=0,1,…)

簡單講:孩子的關鍵碼值大於雙親的。

同理可得,最大堆(Max-Heap)的定義:

ki>=k2i+1,

ki>=k2i+2(i=0,1,…)

同樣的:對於最大堆,雙親的關鍵碼值大於兩個孩子的(如果有孩子)。

堆的特點:

  1. 堆是一種樹形結構,而且是一種特殊的完全二叉樹。
  2. 其特殊性表現在:它是局部有序的,其有序性只體現在雙親節點和孩子節點的關系上(樹的每一層的大小關系也是可以體現的)。兄弟節點無必然聯系。
  3. 最小堆也被稱為小頂堆(根節點是最小的),最大堆也被稱為大頂堆(根節點是最大的)。我們常利用最小堆實現從小到大的排序,最大堆實現從大到小的排序。
最小堆和最大堆的兩個示例圖: 技術分享圖片

這裏只講“堆”這種數據結構的應用之一-------堆排序,而不展開講“堆”這種數據結構的內部實現的細節。

堆排序的基本原理:大體來說就是根據數組元素建立一個最小堆(從小到大排序)或者最大堆(從大到小排序),然後不斷的返回堆頂的元素,刪除堆頂的元素後堆內部的結構會發生一些改變,改變結構是為了滿足堆的性質,從而不斷輸出堆頂的元素就是排好序的序列。

堆排序步驟

  1. 先建好堆。
  2. 不斷地刪除堆頂即可(刪除前記得打印堆頂元素),直到只剩下一個元素。
似乎堆的插入操作沒有用到。其實,當有新的元素加入到一個已建好堆序的序列中,就用到了。

堆排序的簡單實現如下所示:

void HeapSort(int *a,int n)
{
    if(a == NULL && n <= 1)
        return;
    createHeap(a,n); //創建堆
    while(n > 1) //不斷刪除堆頂的元素,刪除前打印。即是堆排序的順序。
    {
        cout<<a[0];
        deleteAt(a,n,0);
    }
    cout<<a[0];
}

具體的原理以及實現的細節可以參考:http://blog.csdn.net/zhangxiangdavaid/article/details/30069623

交換排序兩兩比較待排序記錄的關鍵碼,若是逆序,則交換,直到無逆序。

其中最簡單的交換排序是:冒泡排序。冒泡排序法很穩定,但是不夠高效,時間復雜度是O(n^2)。

效率比較高的交換排序法有快速排序。

冒泡排序(Bubble Sort,也叫起泡排序):

不斷地比較相鄰的記錄,若是不滿足排序要求,則交換。

交換時,可從前向後,也可從後向前。

看一個從前向後的排序過程:

原序列 12 3 45 33 6

下標 0 1 2 3 4

第一趟:

3 12 45 33 6 (3,12交換)

3 12 45 33 6 (12,45不用交換)

3 12 33 45 6 (45,33交換)

3 12 33 6 45 (45,6交換)

第二趟:

3 12 33 6 45 (3,12不用交換)

3 12 33 6 45 (12,33不用交換)

3 12 6 33 45 (33,6交換)

第三趟:

3 12 6 33 45 (3,12不用交換)

3 6 12 33 45 (12,6交換)

第四趟:

3 6 12 33 45 (3,6不用交換)

結束。

冒泡排序法的實現(未經優化):

void swap(int &a,int& b) //交換函數
{
    int temp = a;
    a = b;
    b = temp;
}

void swap(int& a,int& b) //交換函數也可以這樣寫,可以自己驗證下
{
    if(a!=b)
    {
        a^=b;
        b^=a;
        a^=b;
    }
}

/*冒泡排序法
*從左到右:遍歷的數組下標從0--n-1、0--n-2、...、0--1
*可以看作是將最大元素冒泡到最右邊
*從右到左:遍歷的數組下標從n-1--0、n-1--1、...、n-1--n-2
*可以看作是將最大元素冒泡到最左邊
*/
void BubbleSort1(int *a,int n) //冒泡排序法,從左往右
{
    for(int i=1;i<n;i++)
        for(int j=0;j<n-i;j++)
        {
            if(a[j]>a[j+1])
                swap(a[j],a[j+1]);
        }
}

void BubbleSort2(int *a,int n)  //冒泡排序法,從右往左
{
    for(int i=1;i<n;i++)
        for(int j=n-1;j>=i;j--)
        {
            if(a[j]>a[j-1])
                swap(a[j],a[j-1]);
        }
}

現在可以進行一下優化:

如果在某趟排序的時候沒有進行數據的交換,那麽就表明已經是有序排列的了,此時就不需要進行排序了,結束程序就可以了。

改進的冒泡排序如下:

void swap(int& a,int& b) 
{
    a ^= b;
    b ^= a;
    a ^= b;
}


void BuubbleSort(int* a,int n)
{
    if(a == NULL && n >= 1)
        return;
    bool flag = true;  //設置一個標誌位,當標誌位為true才會循環
    while(flag == true)
    {
        for(int i=1;i<n;i++)
            for(int j=0;j<n-i;j++)
            {
                if(a[j]>a[j+1])
                {
                    swap(a[j],a[j+1]);
                }
                else
                    flag = false; //當某一趟循環中沒有數據交換的時候,就會將標誌位置false
            }
    }
}

再度優化:下一趟排序向右(或向左)的最遠位置,只是簡單的減一嗎?可否更高效?

可以更加高效,記錄上一趟排序時交換元素的最遠距離,下一趟排序最遠只到這個位置即可。

下面是改進後:

void swap(int& a,int& b) 
{
    a ^= b;
    b ^= a;
    a ^= b;
}


void BuubbleSort(int* a,int n)
{
    if(a == NULL && n >= 1)
        return;
    bool flag = true; //仍然設置標誌位
    int j = n-1; //初始的循環上限
    while(flag)
    {
        for(int i=0;i<j;i++)
        {
            if(a[i]>a[i+1])
            {
                swap(a[i],a[i+1]);
                j = i; //每次記錄交換的下標 ,最後一次的j值就是最遠交換的下標,也就是下次的循環上限
            }
            else
                flag = false; //如果某一趟遍歷中沒有數據的交換,那麽就表明已經是排好序的了,可以結束程序了
        }
    }
}

快速排序(Quick Sort)也是一種交換排序,它在排序中采取了分治策略。

快速排序的主要思想

  1. 從待排序列中選取一元素作為軸值(也叫主元)。
  2. 將序列中的剩余元素以該軸值為基準,分為左右兩部分。左部分元素不大於軸值,右部分元素不小於軸值。軸值最終位於兩部分的分割處。
  3. 對左右兩部分重復進行這樣的分割,直至無可分割。
從快速排序的算法思想可以看出,這是一遞歸的過程 要想徹底弄懂快速排序,得解決兩個問題:
  1. 如何選擇軸值?(軸值不同,對排序有影響嗎?)
  2. 如何分割?
問題一:軸值的選取? 軸值的重要性在於:經過分割應使序列盡量分為長度相等的兩個部分,這樣分治法才會起作用。若是軸值正好為序列的最值,分割後,元素統統跑到一邊兒去了,分治法就無效了。算法效率無法提高。-看別人寫快排的時候,註意他軸值的選取哦。 問題二:如何分割? 這涉及到具體的技巧和策略。 未完待更新。。。

對排序算法的初步探究