1. 程式人生 > >數據結構與算法分析——第七章 排序

數據結構與算法分析——第七章 排序

ref 逆序數 排好序 賦值 劃分 個數 最小值 完成 數據結構與算法

7.1 預備知識

1,算法接收 含元素的數組和包含元素個數的整數

2,基於比較的排序

7.2 插入排序

代碼實現

void InsertionSort(ElementType A[],int N)
{
    int i,j;
    ElementType Tmp;
    for(i = 1 ; i < N ; i++)
    {
        Tmp = A[i];
        for(j = i ; j > 0 && A[j - 1] > Tmp ; j--)
        {
            A[j] 
= A[j - 1]; } A[j] = Tmp;//避免明顯使用交換 } }

  理解描述

  位置i上元素存於Tmp中,i之前所有更大的元素向右移一位(i前所有元素已排序),Tmp被置於正確位置。

  分析

  1,(未排序)嵌套循環每個花費N次叠代,為O(N^2)

  2,(已排序)為O(N)

  *由於差距較大,故值得分析平均情形 7.3 一些簡單排序算法的下界   1,怎樣概括“一些”的共性?   引入逆序 —— 插排交換次數=逆序數(O(N) <=> O(N + I),N:其他工作,I:逆序數)—— 平均運行時間 <=> 平均逆序數 * 以插排為例分析(插排 代表 通過交換相鄰元素進行排序的算法),得出共性

  2,結論   定理7.1 N個互異數的數組的平均逆序數是N(N-1)/4   定理7.2 通過交換相鄰元素進行排序的任何算法平均需要Ω(N^2)時間 7.4 希爾排序
  代碼實現
void ShellSort(ElementType A[],int N)
{
    int i,j,Increment;
    ElementType Tmp;
    for(Increment = N / 2 ; Increment > 0 ; Increment /= 2)
    {
        for(i = Increment ; i < N ; i++)//小型插排
        {
            Tmp = A[i];
            for(j = i ; j >= Increment && A[j - Increment] > Tmp ; j -= Increment)
            {
                A[j] 
= A[j - Increment]; } A[j] = Tmp; } } }

  理解描述

  不斷縮小增量的插入排序,通過遠距離交換以達到一次消除多對逆序(需保證增量變換時不幹擾已處理的順序),減小每一趟工作量,即亞二次運行時間。

  分析

  定理7.3 使用希爾增量的希爾排序的最壞運行時間為Θ(N^2)

  定理7.4 使用 Hibbard 增量的希爾排序的最壞運行時間為Θ(N^2) (Hibbard增量形如 1,3,7,...,2^k - 1)

7.5 堆排序

  代碼實現

#define LeftChild(i) (2 * (i) + 1)//下標從0開始,因此為 2*i+1
void PercDown(ElementType A[],int i,int N)
{
    int Child;
    ElementType Tmp;
    for(Tmp = A[i] ; LeftChild(i) < N ; i = Child)//下標從0開始,註意邊界
    {
        Child = LeftChild(i);
        if(Child != N - 1 && A[Child + 1] > A[Child])//Child != N - 1 即右邊還有數據
        {
            Child++;//選出較大的兒子
        }
        if(Tmp < A[Child])
        {
            A[i] = A[Child];
        }
        else
        {
            break;
        }
    }
    A[i] = Tmp;
}
void HeapSort(ElementType A[] , int N)
{
    int i;
    //BuildHeap
    for(i = N / 2 ; i >= 0 ; i--)//從右向左,從下向上,逐步下濾
    {
        PercDown(A,i,N);
    }
    //DeleteMax
    for(i = N - 1 ; i > 0 ; i--)
    {
        Swap(&A[0] , &A[i]);
        PercDown(A , 0 , i);//位置0處元素下濾,且元素個數改變
    }
}

技術分享圖片

上圖為構建初始堆及排序過程,數組中表,97.58,31,26,41,58,31  

  理解描述

  1,對已有數組元素進行整理,構建max堆(max堆便於實施從小到大排序)

  2,排序時通過回收利用空間,避免使用第二個數組

  分析

  定理7.5 對N個互異項的隨機排列進行堆排序,所用的平均比較次數為 2NlogN - O(NlogN)

7.6歸並排序

  代碼實現

void MergeSort(ElementType A[],int N)//驅動例程
{
    ElementType *TmpArray;
    TmpArray = malloc(N * sizeof(ElementType));
    if(TmpArray != NULL)
    {
        MSort(A,TmpArray,0,N - 1);
        free(TmpArray);
    }
    else
    {
        FatalError("No space for tmp array");
    }
}
void MSort(ElementType A[],ElementType TmpArray[],int Left,int Right)
{
    int Center;
    if(Left < Right)//條件成立時再對Center賦值
    {
        Center = (Left + Right) / 2;
        MSort(A , TmpArray , Left , Center);
        MSort(A , TmpArray , Center + 1 , Right);
        Merge(A , TmpArray , Left , Center + 1 , Right);//Center + 1 即右半部分始位置
    }
}
void Merge(ElementType A[] , ElementType TmpArray[] , int Lpos , int Rpos , int RightEnd)
{
    int i , LeftEnd , TmpPos , NumElements;
    //NumElements 存放每次排序時元素總數(臨時數組元素轉移至原數組時使用)
    //TmpPos 記錄排序元素在新建數組中*對應*位置
    
    LeftEnd = Rpos - 1;
    NumElements = RightEnd - Lpos;
    TmpPos = Lpos;
    
    while(Lpos < LeftEnd && Rpos < RightEnd)
    {
        if(A[Lpos] < A[Rpos])
        {
            TmpArray[TmpPos++] = A[Lpos++];
        }
        else
        {
            TmpArray[TmpPos++] = A[Rpos++];
        }
    }
    
    //Copy rest of each half
    while(Lpos <= LeftEnd)
    {
        TmpArray[TmpPos++] = A[Lpos++];
    }
    while(Rpos <= RightEnd)
    {
        TmpArray[TmpPos++] = A[Rpos++];
    }
    
    for(i = 0 ; i < NumElements ; i++ , RightEnd--)
    {
        A[RightEnd] = TmpArray[RightEnd];
    }
}

  理解描述

  1,遞歸是怎樣實施的?

  答:以8個元素為例,左右各分成4 個元素,先處理左邊4 個,將左邊4個元素重復分隔,直到左邊只剩一個元素,達到基準條件,返回最近主程序中,處理右邊一個元素,達到基準條件,進行合並,此時這兩個元素作為左側,返回最近主程序中,處理右邊兩個元素,繼續先左後右,處理合並,返回最近主程序中,進行合並。此時總體來看,8個元素,左側4個已實施“分治”,接下來繼續進行遞推,過程類似。

  2,小塊元素合並後怎樣存放?

  答:小塊元素在臨時數組*對應位置*進行合並存放,排序後依次放入原數組中,不影響後面元素及其排序。

  分析 

  運行時間:T(N) = O(NlogN)

  應用:很難用於主存排序。首先,合並兩個線性表需要線性附加內存,其次,數據在數組間拷貝需要時間。 

7.7 快速排序

  代碼實現

//驅動例程
void QuickSort(ElementType A[] , int N)
{
    QSort(A , 0 , N - 1);
}

//選取樞紐元
//對三元素排序,最值放兩邊,樞紐元置於 right-1 處(此方法便於設置警戒標誌,防止越界)
ElementType Median3(ElementType A[] , Left , Right)
{
    int Center;
    Center = (Left + Right) / 2;
    if(A[Left] > A[Center])
    {
        Swap(&A[Left] , &A[Center]);
    }
    if(A[Left] > A[Right])
    {
        Swap(&A[Left] , &A[Right]);
    }
    if(A[Center] > A[Right])
    {
        Swap(&A[Center] , &A[Right]);
    }
    Swap(&A[Center] , &A[Right - 1]);
    return A[Right - 1];
}

#define Cutoff (3)
void QSort(ElementType A[] , int Left , int Right)
{
    int i , j;
    ElementType Pivot;
    if(Left + Cutoff <= Right)//數組內大於3個元素時,分割加遞歸
    {
        Pivot = Median3(A , Left , Right);
        i = Left;//因為下方使用的是前自增
        j = Right - 1;
        for( ; ; )
        {
            while(A[++i] < Pivot){}
            while(A[--j] > Pivot){}
            if(i < j)
            {
                Swap(&A[i],&A[j]);
            }
            else
            {
                break;
            }
        }
        Swap(&A[i],&A[Right - 1]);//Restore pivot
        QSort(A , Left , i - 1);
        QSort(A , i + 1 , Right);
    }
    else
    {
       InsertionSort(A + Left , Right - Left + 1); 
    }
}

  理解描述

  與歸並排序類似,區別是選取樞紐元,以樞紐元為界分割為大小兩數組。(適用於大數組)

  選取樞紐元:隨機選取;三數中值分割法(左,右,中心位置裏的中值,同時將三元素排序,便於設置警戒標誌,防止越界)

  分割策略:1,樞紐元與位置right - 1處元素互換

       2,設置i(left),j(right - 1),i 向右滑動直至 i 處元素 >= 樞紐元,j 向左滑動直至 j 處元素 <= 樞紐元,交換i,j處元素

       3,i,j交錯時,i 處元素與樞紐元交換

  分析

  最壞情況分析:樞紐元始終為最小元素,T(N) = O(N^2)

  最好情況分析:樞紐元位於中間,T(N) = O(NlogN)

  平均情況分析:每個文件大小等可能,求出平均分布,T(N) = O(NlogN)

  擴展(選擇問題的線性期望時間算法)

  選擇問題:查找集合S中第k個最小元

  與快速排序類似,區別在於分割後,若 k <= |S1|,則返回quickselect( S1 , k ),若k = |S1| + 1 ,則樞紐元即為所求,否則返回quickselect(S2 , k - |S1| - 1)

  代碼實現

#define Cutoff (3)
void QuickSelect(ElementType A[] , int k , int N)
{
    QSelect(A , k , 0 , N - 1);
}

ElementType Median3(ElementType A[] , Left , Right)
{
    int Center;
    Center = (Left + Right) / 2;
    if(A[Left] > A[Center])
    {
        Swap(&A[Left] , &A[Center]);
    }
    if(A[Left] > A[Right])
    {
        Swap(&A[Left] , &A[Right]);
    }
    if(A[Center] > A[Right])
    {
        Swap(&A[Center] , &A[Right]);
    }
    Swap(&A[Center] , &A[Right - 1]);
    return A[Right - 1];
}

//位置k - 1處即為第k個最小值
void QSelect(ElementType A[] , int k , int Left , int Right)
{
    int i , j;
    ElementType Pivot;
    if(Left + Cutoff <= Right)
    {
        Pivot = Median3(A , Left , Right);
        i = Left;
        j = Right - 1;
        for( ; ; )
        {
            while(A[++i] < Pivot){}
            while(A[--j] > Pivot){}
            if(i < j)
            {
                Swap(&A[i],&A[j]);
            }
            else
            {
                break;
            }
        }
        Swap(&A[i],&A[Right - 1]);//Restore pivot
        if(k <= i)
        {
            QSelect(A , k , Left , i - 1);
        }
        else if(k > i + 1)
        {
            QSelect(A , k - i - 1 , i + 1 , Right);
        }
    }
    else
    {
       InsertionSort(A + Left , Right - Left + 1);
    }
}

7.8 大型結構的排序

  排序時交換整個結構代價是昂貴的,解決辦法是讓輸入數組包含指向結構的指針,通過指針比較關鍵字,必要時交換指針來進行排序。

7.9 排序的一般下界

  引入決策樹,通過只使用比較進行排序的每一種算法都可以用決策樹表示。

  引理7.1:令T是深度為d的二叉樹,則T最多有2^d個樹葉

  引理7.2:具有L片樹葉的二叉樹深度至少是[ logL ]

  定理7.6:只使用元素間比較的任何排序算法在最壞情況下至少需要[ log( N! ) ]次比較

  定理7.7:只使用元素間比較的任何排序算法需要進行Ω(NlogN)次比較

7.10 桶式排序

  以線性時間進行排序,構建數組,大小包含所有待排元素,初始為0,元素出現以1代替。

7.11 外部排序

  簡單算法(該部分內容轉自@Judy518)——二路合並

  例如要對外存中4500個記錄進行歸並,而內存大小只能容納750個記錄,在第一階段,我們可以每次讀取750個記錄進行排序,這樣可以分六次讀取,進行排序,可以得到六 個有序的歸並段,如下圖:

  技術分享圖片

每個歸並段的大小是750個記錄,記住,這些歸並段已經全部寫到臨時緩沖區(由一個可用的磁盤充當)內了,這是第一步的排序結果。

1、將內存空間劃分為三份,每份大小250個記錄,其中兩個用作輸入緩沖區,另外一個用作輸出緩沖區。首先對Segment_1和Segment_2進行歸並,先從每個歸並段中讀取250個記錄到輸入緩沖區,對其歸並,歸並結果放到輸出緩沖區,當輸出緩沖區滿後,將其寫道臨時緩沖區內,如果某個輸入緩沖區空了,則從相應的歸並段中再讀取250個記錄進行繼續歸並,反復以上步驟,直至Segment_1和Segment_2全都排好序,形成一個大小為1500的記錄,然後對Segment_3和Segment_4、Segment_5和Segment_6進行同樣的操作。

2、對歸並好的大小為1500的記錄進行如同步驟1一樣的操作,進行繼續排序,直至最後形成大小為4500的歸並段,至此,排序結束。

  多路合並

  1,2-路合並擴充為k-路合並,區別在於尋找k個元素中最小元過程稍微復雜,可通過優先隊列尋找最小元,進行DeleteMin操作,得到下一個寫到磁盤上的元素

  2,在初始順串構造階段之後,k-路合並所需趟數為[ logk( N / M ) ]。(N為數據總量,M為內存容量)

  多相合並

  k-路合並需要2k盤磁帶,然而使用 k + 1 盤磁帶也有可能完成排序工作,作為例子,這裏闡述只用三盤磁帶(T1,T2,T3)如何完成2-路合並。

  假設T1上有一個輸入文件,它將產生34個順串。

  選擇一:每盤磁帶存入17個,然後合並存放至T1,再將T1前個8順串存至T2,再次合並存至T3,依次類推。這樣每一趟合並附加額外半趟工作。

  選擇二:將34個順串不均衡地分成2份,例如21,13,再次進行合並。此時需考慮順串最初的分配,事實上,若初始順串為斐波那契數,則將其分裂為2個斐波那契數之和,否則需添加啞順串填補磁帶,將其補足為斐波那契數

  替換選擇(該部分內容轉自@kph_Hajash)  

  在標準的外排中,一次讀入內存可容納的 M 個記錄,排序完依次輸出到空磁帶上;但這裏其實有個小技巧,排完序後輸出第一個記錄到磁帶上時,內存讓出了一個記錄的空間,這時我們可以從輸入磁帶取出一個記錄,判斷它是否大於剛輸出的記錄,若是,說明它可以放入當前順串中(順串是從小到大有序),否則,應暫存內存,等下一個順串的構造; 這裏暫存內存書上講是放在最小堆的死區(dead space)。

初始順串的構造詳解,綠色箭頭表示當前輸入狀態,Tbn 表示輸出狀態,內存緩沖表示當前內存中存在的記錄(括號內記錄表示存在最小堆的死區)
技術分享圖片
從上圖可知,與標準順串構造方式生成的 5 個順串相比,替換選擇構造的初始順串記錄數更多,順串數更少,只有 3 個,且前者需要 12 趟完成排序,替換選擇只需 3 趟。

  

數據結構與算法分析——第七章 排序