數據結構-排序

》排序:使一組任意排列的對象變成一組按關鍵字線性有序的對象。

        排序是計算機內經常進行的一種操作,其目的是將一組「無序」的記錄序列調整爲「有序」的記錄序列。

        -內部排序:整個排序過程不需要訪問外存便能完成。內部排序的過程是一個逐步擴大記錄的有序序列長度的過程。

        -外部排序:參加排序的記錄數量很大,整個序列的排序過程不可能在內存中完成,則稱此類排序問題爲外部排序。

        -數據表(Data List):待排序的數據對象的有限集合。

        -關鍵字(Key):作爲排序依據的數據對象中的屬性域。

        -主關鍵字(Primary Key):是表中的一個或多個字段,它的值用於唯一地標識表中的某一條記錄。

        -排序算法的穩定性:關鍵字相同的數據對象在排序過程中是否保持前後次序不變,不變則表示排序是穩定的,反之是不穩定的。

        -排序的時間開銷:它是衡量算法好壞的最重要的標誌。通常用算法執行中的數據比較次數和數據移動次數來衡量。

        -評價排序算法好壞的標準:時間複雜度和空間複雜度。另外算法本身的複雜程度也是需要考慮的一個因素。排序算法所需要的附加空間一般都不大,矛盾並不突出。而排序是一種經常執行的運算,往往屬於系統的核心部分,因此排序的時間開銷是算法好壞的最重要標誌。

        -插入排序:將無序子序列中的一個或幾個記錄「插入"到有序序列中,從而增加記錄的有序子序列的長度。

        -交換排序:通過「交換」無序序列中的記錄從而得到其中關鍵字最小或最大的記錄,並將其加入到有序子序列中,以此方法增加記錄的有序子序列的長度。

        -選擇排序:從記錄的無序子序列中「選擇」關鍵字最小或最大的記錄,並將它加入到有序子序列中,以此方法增加記錄的有序子序列的長度。

        -歸併排序:通過「歸併」兩個或兩個以上的記錄有序子序列,逐步增加記錄有序序列的長度。

》直接插入排序 (穩定,需要一個記錄的輔助空間)   

        -時間性能分析:

        最好情況下(正序):比較次數>n-1    移動次數>0    時間複雜度爲O(n)。

        最壞情況下(逆序):比較次數>(n+2)(n-1)/2    移動次數>(n+4)(n-1)/2    時間複雜度O(n^2)。

        直接插入排序算法簡單、容易實現,適用於待排序記錄基本有序或待排序記錄較小時。當待排序的記錄個數較多時,大量的比較和移動操作使直接插入排序算法的效率降低。

        -基本思想:

               當插入第i個對象時,前面的V[0],V[1],…,V[i-1]已經排好序,此時,用v[i]的關鍵字與V[i-1], V[i-2],…的關鍵字順序進行比較,找到插入位置即將V[i]插入,原來位置上對象向後順移。

        -一趟插入排序步驟:

                在R[1..i-1]中查找R[i]的插入位置, R[1..j].key≤R[i].key< R[j+1..i-1].key;
                將R[j+1..i-1]中的所有記錄均後移一個位置;
                將R[i] 插入(複製)到R[j+1]的位置上。

                

       - 構造初始的有序序列:將第1個記錄看成是初始有序表,然後從第2個記錄起依次插入到這個有序表中,直到將第n個記錄插入。

        -查找待插入記錄的插入位置:在i-1個記錄的有序區r[1] ~ r[i-1]中插入記錄r[i],首先順序查找r[i]的正確插入位置,然後將r[i]插入到相應位置。

        r[0]=r[i];j=i-1;    //r[0]充當哨兵

        while(r[0<r[j]]){

                 r[j+1]=r[j];    j--;  

        }//while


》表插入排序

        爲了減少在排序過程中進行的「移動」記錄的操作,必須改變排序過程中採用的存儲結構。利用靜態鏈表進行排序,並在排序完成之後,一次性地調整各個記錄相互之間的位置,即將每個記錄都調整到它們所應該在的位置上。


》折半插入排序(穩定,需要初始隊列已經有序)

        因爲 R[1..i-1] 是一個按關鍵字有序的有序序列,則可以利用折半查找實現「在R[1..i-1]中查找R[i]的插入位置」,如此實現的插入排序爲折半插入排序。


》希爾排序(插入類,不穩定)

        又稱縮小增量排序,在插入排序中,只比較相鄰的結點,一次比較最多把結點移動一個位置。如果對位置間隔較大距離的結點進行比較,使得結點在比較以後能夠一次跨過較大的距離,這樣就可以提高排序的速度。希爾排序開始時增量較大,每個子序列中的記錄個數較少,從而排序速度較快;當增量較小時,雖然每個子序列中記錄個數較多,但整個序列已基本有序,排序速度也較快。
        希爾排序算法的時間性能是所取增量的函數,而到目前爲止尚未有人求得一種最好的增量序列。
        研究表明,希爾排序的時間性能在O(n2)和O(nlog2n)之間。當n在某個特定範圍內,希爾排序所需的比較次數和記錄的移動次數約爲O(n1.3 ) 。

        -基本思想:將整個待排序記錄分割成若干個子序列,在子序列內分別進行直接插入排序,待整個序列中的記錄基本有序時,對全體記錄進行直接插入排序。

        -需要解決的關鍵問題:

        應如何分割待排序記錄,才能保證整個序列逐步向基本有序發展?

            解決方法:將相隔某個「增量」的記錄組成一個子序列。增量應如何取?希爾最早提出的方法是d1=n/2,di+1=di/2。

            算法描述:for(d=n/2;d>=1;d=d/2){以d爲增量,進行組內直接插入排序;}

        子序列內如何進行直接插入排序?

            解決方法:在插入記錄r[i]時,自r[i-d]起往前跳躍式(跳躍幅度爲d)搜索待插入位置,並且r[0]只是暫存單元,不是哨兵。當搜索位置<0,表示插入位置已找到。在搜索過程中,記錄後移也是跳躍d個位置。在整個序列中,前d個記錄分別是d個子序列中的第一個記錄,所以從第d+1個記錄開始進行插入。

            

》冒泡排序(交換類,穩定)

        基本思想:假設待排序的n個對象的序列爲r[0],r[1],..., r[n-1],起始時排序範圍是從r[0]到r[n-1]。在當前的排序範圍之內,自右至左對相鄰的兩個結點依次進行比較,讓值較大的結點往下移(下沉),讓值較小的結點往上移(上冒)。每趟起泡都能保證值最小的結點上移至最左邊,下一遍的排序範圍爲從下一結點到r[n-1]。在整個排序過程中,最多執行(n-1)遍。但執行的遍數可能少於(n-1),這是因爲在執行某一遍的各次比較沒有出現結點交換時,就不用進行下一遍的比較。
        

        實現算法:

                Bubble_Sort(rectype r[]){

                        int i,j,noswap;

                        rectype temp;

                        for(i=0;i<n-2;i++){

                                noswap=TRUE;

                                for(j=n-1;j>=i;j--){

                                      if(r[i+1].key<r[j]){

                                            temp=r[j+1];    r[j+1]=r[j];    r[j]=temp;    noswap=FALSE;

                                       }//if

                                       if(noswap) break;

                                 }//for

                        }//for

                }//Bubble_Sort

        時間性能分析:

            最好情況:初始狀態是遞增有序的,一趟掃描就可完成排序,關鍵字的比較次數爲n-1,沒有記錄移動。

            最壞情況:若初始狀態是反序的,則需要進行n-1趟掃描,每趟掃描要進行n-i次關鍵字的比較,且每次需要移動記錄三次,因此,最大比較次數和移動次數分別爲:(最大比較次數>O(n^2))(最大移動次數>O(n^2))。


》快速排序(交換類,不穩定)

        -基本思想:首先選一個軸值(即比較的基準),通過一趟排序將待排序記錄分割成獨立的兩部分,前一部分記錄的關鍵碼均小於或等於軸值,後一部分記錄的關鍵碼均大於或等於軸值,然後分別對這兩部分重複上述方法,直到整個序列有序。

        -選擇軸值的方法:

                使用第一個記錄的關鍵碼;
                選取序列中間記錄的關鍵碼;
                比較序列中第一個記錄、最後一個記錄和中間記錄的關鍵碼,取關鍵碼居中的作爲軸值並調換到第一個記錄的位置;
                隨機選取軸值。

        -選取不同軸值的效果:決定兩個子序列的長度,子序列的長度最好相等。

        -如何實現一次劃分:

                取第一個記錄的關鍵字值作爲基準,將第一個記錄暫存於temp中,設兩個變量i,j分別指示將要劃分的最左、最右記錄的位置。
                將j指向的記錄關鍵字值與基準值進行比較,如果j指向的記錄關鍵字值大,則j前移一個位置;重複此過程,直到j指向的記錄關鍵字值小於基準值;若i<j,則將j指向的記錄移到i所指位置。
                將i指向的記錄關鍵字值與基準值進行比較,如果i指向的記錄關鍵字值小,則i後移一個位置;重複此過程,直到i指向的記錄關鍵字值大於基準;若i<j,則i指向的記錄移到j所指位置。
                 重複②、③步,直到i=j。

        -如何處理分割得到的兩個待排序子序列:對分割得到的兩個子序列遞歸的執行快速排序。

        -時間性能分析:

                最好情況:每一次劃分對一個記錄定位後,該記錄的左側子表與右側子表的長度相同,爲O(nlog2n)。

                最壞情況:每次劃分只得到一個比上一次劃分少一個記錄的子序列(另一個子序列爲空),爲 O(n^2)。

                平均情況:O(nlog2n)。

  

》直接選擇排序(不穩定)

        -基本思想:在一組對象V[i]到V[n-1]中選擇具有最小關鍵字的對象。若它不是這組對象中的第一個對象,則將它與這組對象中的第一個對象對調。刪除具有最小關鍵字的對象,在剩下的對象中重複第(1)、(2)步,直到剩餘對象只有一個爲止。

        

        -算法實現:

            Select_Sort(rectype r[]){

                    int i,j,k;

                    rectype temp;

                    for(i=0;i<n-1;i++){

                          k=i;

                          for(j=i+1;j<n;j++) if(r[j].key<r[k].key) k=j;

                          if(k!=i){

                               temp=r[i];    r[i]=r[k];    r[k]=temp;

                          }//if

                    }//for

            }//Select_Sort

        -時間性能分析:

            無論初始狀態如何,在第i趟排序中選擇最小關鍵字的記錄,需做n-i次比較,因此總的比較次數爲O(n^2)。

            當文件爲正序時,移動次數爲0,文件初態爲反序時,每趟排序均要執行交換操作,總的移動次數取最大值3(n-1)。

》堆排序(選擇類,不穩定)

        -基本思想:直接選擇排序的改進,研究如何減少關鍵碼間的比較次數。若能利用每趟比較後的結果,也就是在找出鍵值最小記錄的同時,也找出鍵值較小的記錄,則可減少後面的選擇中所用的比較次數,從而提高整個排序過程的效率。

        -堆的定義:

        

         在完全二叉樹上,雙親和左右孩子之間的編號就是i和2i、2i+1的關係。因此一個序列可以和一棵完全二叉樹對應起來,用雙親其左、右孩子之間的關係可以直觀的分析是否符合堆的特性。

         

        -建堆的過程:對原始序列建堆過程,就是一個反覆進行篩選的過程。通過對應的完全二叉樹分析:對n個結點的完全二叉樹,可以認爲:以葉子爲根的子樹(只有它自己)已滿足堆特性,因此從最後一個分支結點開始,把每棵子樹調整爲堆,直到根結點爲止,整棵樹成爲堆。

        設初始排序序列:30  24  85 16  36 53 91 47 ,建成大頂堆。

        

        -輸出堆元素:輸出堆頂元素後,將堆底元素送入堆頂(或將堆頂元素與堆底元素交換),堆可能被破壞。破壞的情況僅是根結點和其左右孩子之間可能不滿足堆的特性,而其左右子樹仍然是局部的堆。在這種情況下,將其R1 … Ri整理成堆。(i=n-1..1)

        

        -怎樣將剩餘的n-1個元素按其關鍵碼調整爲一個新堆:

            假設有一個大根堆,當輸出堆頂元素(根結點)後,以堆中最後一個元素替代它。此時根結點的左子樹和右子樹均爲堆,則只需自上而下進行調整即可
             首先將堆頂元素與其左、右子樹根結點的值進行比較,如果堆頂元素比它的兩個子結點都大,則已經是堆;否則,讓堆頂元素與其中較大的孩子結點交換,先讓堆頂滿足堆的性質。

             可能因爲交換,使交換後的結點爲根的子樹不再滿足堆的性質,則重複向下調整,當調整使新的更小子樹依舊滿足堆的性質時,重新建堆的過程結束。這種自上而下的建堆過程稱爲結點向下的「篩選」。

  

        -時間性能分析:

            對深度爲 k 的堆,「篩選」所需進行的關鍵字比較的次數至多爲2(k-1);

            對 n 個關鍵字,建成深度爲h=log2n+1的堆,所需進行的關鍵字比較的次數至多4n;

            調整「堆頂」 n-1 次,總共進行的關鍵字比較的次數不超過2(log2(n-1)+log2(n-2)+ …+log22)< 2n(log2n) ,因此,堆的時間複雜度O(nlogn)。

》歸併排序

       基本思想:將兩個或兩個以上的有序子序列「歸併」 爲一個有序序列。在內部排序中,通常採用的是2-路歸併排序。即:將兩個位置相鄰的記錄有序子序列。

       算法實現:

            //將有序的記錄序列sr[i..m]和sr[m+1..n]歸併爲有序的記錄序列tr[i..n]

            void Merge(rcdtype sr[],rcdtype &tr[],int i,int m,int n){

                    for(j=m+1,k=i;i<=m&&j<=n;k++){

                            if(sr[i].key<=sr[j].key)tr[k]=sr[i++];    //將sr中記錄由小到大地併入tr

                            else tr[k]=sr[j++];

                    }//for

            }//Merge

        -時間性能分析:

             對n個記錄進行歸併排序的時間複雜度爲O(nlogn)。即:每一趟歸併的時間複雜度爲O(n),總共需進行log2n趟。


》基數排序(多關鍵字排序)

            基數排序是一種藉助「多關鍵字排序」的思想來實現「單關鍵字排序」的內部排序算法。

            -多關鍵字排序:

                    n個記錄的序列{ R1, R2, …,Rn}對關鍵字 (Ki0,Ki1,…,Kid-1) 有序是指:對於序列中任意兩個記錄 Ri 和 Rj(1≤i<j≤n) 都滿足下列(詞典)有序關係:(Ki0, Ki1,…,Kid-1)< (Kj0, Kj1,…,Kjd-1);其中: K0被稱爲 「最主」位關鍵字,Kd-1  被稱爲 「最次」位關鍵字。

            -最高位優先MSD法:

                    先對K0進行排序,並按 K0 的不同值將記錄序列分成若干子序列之後,分別對 K1 進行排序,...…, 依次類推,直至最後對最次位關鍵字排序完成爲止。

            -最低位優先LSD法:

                    先對 Kd-1 進行排序,然後對 Kd-2   進行排序,依次類推,直至對最主位關鍵字 K0 排序完成爲止。

            //當對多關鍵字的記錄序列進行LSD方法排序時,必須採用穩定的排序方法。

            //排序過程中不需要根據 「前一個」關鍵字的排序結果,將記錄序列分割成若干個(「前一個」關鍵字不同的)子序列。

             

》各種排序方法綜合比較

        除基數排序外,其它方法都是基於「比較關鍵字」進行排序的排序方法。這類排序法可能達到的最快的時間複雜度爲O(nlogn)。(基數排序不是基於「比較關鍵字」的排序方法,所以它不受這個限制。)

        -平均的時間性能:

                時間複雜度爲 O(nlogn):快速排序、堆排序和歸併排序

                時間複雜度爲 O(n2):直接插入排序、冒泡排序和簡單選擇排序

                時間複雜度爲 O(n):基數排序

        -當待排記錄序列按關鍵字順序有序時:

                直接插入排序和冒泡排序能達到O(n)的時間複雜度,快速排序的時間性能蛻化爲O(n2) 。

        -簡單選擇排序、堆排序和歸併排序的時間性能不隨記錄序列中關鍵字的分佈而改變。

        -空間性能:

             歸併排序所需輔助空間最多,其空間複雜度爲 O(n);鏈式基數排序需附設隊列首尾指針,則空間複雜度爲 O(rd)。


》外部排序

        待排序的記錄數量很大,不能一次裝入內存,否則無法利用前幾節討論的排序方法 (否則將引起頻繁訪問內存);

對外存中數據的讀/寫是以「數據塊」爲單位進行的;

        讀/寫外存中一個「數據塊」的數據所需要的時間爲:

                  TI/O = tseek +tla + n´ twm

        其中 tseek 爲尋查時間(查找該數據塊所在磁道); tla  爲等待(延遲)時間;  n´ twm 爲傳輸數據塊中n個記錄的時間。

        -外部排序的基本過程:

               按可用內存大小,利用內部排序方法,構造若干( 記錄的) 有序子序列,通常稱外存中這些記錄有序子序列爲「歸併段」;通過「歸併」,逐步擴大 (記錄的)有序子序列的長度,直至外存中整個記錄序列按關鍵字有序爲止。

  

        外排總的時間還應包括內部排序所需時間和逐趟歸併時進行內部歸併的時間,顯然,除去內部排序的因素外,外部排序的時間取決於逐趟歸併所需進行的「趟數」。

        例如,若對上述例子採用5-路歸併,則只需進行2趟歸併,總的訪問外存的次數將壓縮到100+2*100=300次。

        一般情況下,假設待排記錄序列含 m 個初始歸併段,外排時採用 k-路歸併,則歸併趟數爲logkm,顯然,隨之k的增大歸併的趟數將減少,因此對外排而言,通常採用多路歸併。k 的大小可選,但需綜合考慮各種因素。