資料結構第10章 排序

^鳶飛魚躍^發表於2021-01-04

插入排序

每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子表中的適當位置,直到全部記錄插入完成為止。

直接插入排序

整個排序過程為n-1趟插入,即先將序列中第1個記錄看成是一個有序子序列,然後從第2個記錄開始,逐個進行插入,直至整個序列有序。

void InsertSort(SqList &L)
 {     
    int i,j;
    for(i=2;i<=L.length; i++)
    {   //將L.R[i]插入有序子表
        if( L.R[i].key<L.R[i-1].key)
        {     
        L.R[0]=L.R[i]; // 複製為哨兵
        j = i-1;
        do{    
            L.R[j+1]=L.R[j]; // 記錄後移 
            j--;
        }while(L.R[0].key>=L.R[j].key))
        L.R[j+1]=L.R[0]; //插入到正確位置
        }
    }
}

時空複雜度和分析

最好的情況(關鍵字在記錄序列中正序):
“比較”的次數: ∑ i = 1 n − 1 1 = n − 1 \sum_{i=1}^{n-1} 1=n-1 i=1n11=n1
“移動”的次數:0
最壞的情況(關鍵字在記錄序列中逆序有序):
“比較”的次數: ∑ i = 1 n − 1 i = n ( n − 1 ) / 2 \sum_{i=1}^{n-1} i=n(n-1)/2 i=1n1i=n(n1)/2
“移動”的次數: ∑ i = 1 n − 1 ( i + 2 ) = ( n + 4 ) ( n − 1 ) / 2 \sum_{i=1}^{n-1} (i+2)=(n+4)(n-1)/2 i=1n1(i+2)=(n+4)(n1)/2

  1. 改進:在插入第 i(i>1)個記錄時,前面的 i-1 個記錄已經排好序,則在尋找插入位置時,可以用二分查詢來代替順序查詢,從而減少比較次數。這就是折半插入排序。
  2. 直接插入排序在基本有序時,效率較高
  3. 在待排序的記錄個數較少時,效率較高

希爾(Shell)排序

先將整個待排記錄序列分割成若干子序列,各個子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行一次直接插入排序。

將相距d個位置的記錄分為一組, n 個記錄被分成 d 個子序列,d 稱為增量,增量d的值在排序過程中從大到小逐漸縮小,直至最後一趟排序減為 1。所以希爾排序也稱為縮小增量排序。
在這裡插入圖片描述

時空複雜度和分析

時間複雜度平均情況: O ( n 1.3 ) O(n^{1.3}) O(n1.3)

如何選擇最佳d序列,目前尚未解決;但最後一個增量值必須為1
不宜在鏈式儲存結構上實現

交換排序

氣泡排序

void Bubble-sort(SqList &L)
{     
    int i, j, swap;    // 當swap為0則停止排序
    for  ( i=1; i<L.length; i++)   //  i 表示趟數,最多n-1趟 
    {    
        swap=0;                // 開始時元素未交換
        for ( j=1; j<=L.length-i; j++)  
        if (L.R[j].key>L.R[j+1].key)   // 發生逆序
        {     L.R[0]=L.R[j];  L.R[j]=L.R[j+1];   L.R[j+1]=L.R[0];
                swap=1;     
        } // 交換,並標記發生了交換
    if(swap==0)   break;   
    }
}

時空複雜度和分析

最好的情況(關鍵字在記錄序列中正序):
“比較”的次數: ∑ i = 1 n − 1 1 = n − 1 \sum_{i=1}^{n-1} 1=n-1 i=1n11=n1
“移動”的次數:0
最壞的情況(關鍵字在記錄序列中逆序有序):
“比較”的次數: ∑ i = 1 n − 1 i = n ( n − 1 ) / 2 \sum_{i=1}^{n-1} i=n(n-1)/2 i=1n1i=n(n1)/2
“移動”的次數: ∑ i = 1 n − 2 3 ( n − i − 1 ) = 3 n ( n − 1 ) / 2 \sum_{i=1}^{n-2}3 (n-i-1)=3n(n-1)/2 i=1n23(ni1)=3n(n1)/2

改進:在氣泡排序中,記錄的比較和移動是在相鄰單元中進行的,記錄每次交換隻能上移或下移一個單元,因而總的比較次數和移動次數較多。

快速排序

  1. 快速排序首先選一個基準值(即比較的基準),每趟使表的第1個元素放入適當位置(歸位),將表一分為二,前一部分記錄的關鍵碼均小於或等於基準值,後一部分記錄的關鍵碼均大於或等於基準值對;
  2. 子表按遞迴方式繼續這種劃分,直至劃分的子表長為0或1(遞迴出口)。
void Quick_Sort(SqList &L,int s,int t)  /* 對R[s]到R[t]的元素進行排序 */
{    if (s<t)     //至少有兩個元素
	{    int i=Partition(L,s,t);
           Quick_Sort(L,s,i-1);
	      Quick_Sort(L,i+1,t);	
     }
}
int Partition(SqList &L,int low,int high)
{	L.R[0]= L.R[low]; /* 暫存基準值元素到R[0]中*/
	while(low<high)  /* 從表的兩端交替地向中間掃描 */
	{    while( low<high&&L.R[high].key>=L.R[0].key )high--;
	      if(low<high)  {L.R[low]= L.R[high]; low++; }
   while( low<high&&L.R[low].key<L.R[0].key ) low++;
	   if (low<high) {L.R[high]= L.R[low]; high--; }
	}
	 L.R[low]= L.R[0];  /* 將基準值元素放到其最終位置 */
	return low;  /* 返回基準值元素所在的位置*/
}

時空複雜度和分析

在這裡插入圖片描述

最好情況時間複雜度為 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n),空間複雜度為 O ( l o g 2 n ) O(log_2n) O(log2n)
最壞情況時間複雜度為 O ( n 2 ) O(n^2) O(n2),空間複雜度為 O ( n ) O(n) O(n)
平均時間複雜度為 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
穩定性:不穩定。

選擇排序

簡單選擇排序(或稱直接選擇排序)

每一趟在後面 n-i+1箇中選出關鍵碼最小的物件, 作為有序序列的第 i 個記錄。

在這裡插入圖片描述

時空複雜度和分析

最好情況時間複雜度為 O ( n 2 ) O(n^2) O(n2)
最壞情況時間複雜度為 O ( n 2 ) O(n^2) O(n2)
空間複雜度為 O ( 1 ) O(1) O(1)

樹形選擇排序(錦標賽排序)

簡單選擇排序慢的原因?
用直接選擇排序從n個記錄中選出關鍵字值最小的記錄要做n-1次比較,然後從其餘n-1個記錄中選出最小者要作n-2次比較。顯然,相鄰兩趟中某些比較是重複的。
樹形選擇排序:首先對n個關鍵字進行兩兩比較,然後在其⌈?/2⌉個較小者之間再進行兩兩比較,如此重複,直至選出最小關鍵字為止。這個過程可用一棵有n個葉結點的完全二叉樹表示。

在這裡插入圖片描述

堆排序(Heap Sort)

堆的概念

n個元素的序列 { k 1 , k 2 , … … , k n } \{ k_1, k_2 ,…… , k_n \} {k1k2kn},當且僅當滿足下面條件(以大根堆為例)稱之為堆。
{ k i ≥ k 2 i k i ≥ k 2 i + 1 \left\{\begin{array}{l} k _{ i } \geq k _{2 i } \\ k _{ i } \geq k _{2 i +1}\end{array}\right. {kik2ikik2i+1
( i = 1 , 2 , … , ⌊ n / 2 ⌋ ) (i=1,2, \ldots,\lfloor n / 2\rfloor) (i=1,2,,n/2)
若將此關鍵字序列按順序組成一棵完全二叉樹,則堆可以如下定義:
或每個結點的值都大於或等於其左右孩子結點的值(稱為大根堆或大頂堆)。

堆排序流程:

  1. 將無序序列建成一個堆;
  2. 輸出堆頂的最小(大)值;
  3. 使剩餘的n-1個元素又調整成一個堆,返回步驟2;

篩選或調整演算法

對一棵左右子樹均為堆的完全二叉樹,“調整”根結點使整個二叉樹也成為一個堆。

  1. s指向當前根節點,將根節點存入暫存區tmp;
  2. i指向s孩子中key較大的;
  3. 若tmp.key>=i.key,s=tmp,退出;否則s=i,返回步驟2。
void HeapAdjust(SqList &L,int s,int m)
{    //假設R[s+1..m]已經是堆,將R[s..m]調整為以R[s]為根的大根堆
     RecType tmp=L.R[s];
     for(int i=2*s ; i<=m ; i*=2)      //沿key較大的孩子結點向下篩選
     {     if(i<m && L.R[i].key<L.R[i+1].key) i++; //i為key較大的記錄的下標
           if( tmp.key>=L.R[i].key )       
                 break;//雙親大:不再調整,temp應插在位置s上
           L.R[s]=L.R[i];//將R[j]調整到雙親結點位置上
           s=i;       	//修改s值,以便繼續向下篩選
      }
      L.R[s]=tmp;       //插入
}

無序序列建成一個初始堆

for (i=n/2;i>=1;i--)    
     HeapAdjust(L.R,i,n);

在這裡插入圖片描述

堆排序演算法

void HeapSort(SqList &L)
{     int i;  RecType tmp;
      for (i=n/2;i>=1;i--) 	//迴圈建立初始堆
             HeapAdjust(L.R,i,n); 
      for (i=n; i>=2; i--)	//進行n-1次迴圈,完成堆排序
      {     temp=L.R[1];       	//堆頂歸位,R[1]  R[i]
            L.R[1]=L.R[i]; 
            L.R[i]=tmp;
            HeapAdjust(L.R,1,i-1);//調整剩餘記錄,篩選R[1]結點,得到i-1個結點的堆
      }
}

時空複雜度和分析

設有n個記錄的初始序列對應的完全二叉樹的深度為 h = ⌊ log ⁡ 2 n ⌋ + 1 h =\left\lfloor\log _{2} n\right\rfloor+1 h=log2n+1,每個非終端結點都要自上而下進行“篩選”。由於第i層上的結點數小於等於 2 i − 1 2^{i-1} 2i1,且第i層結點最大下移的深度為h-i,每下移一層要做兩次比較,所以建初堆時關鍵字總的比較次數為
∑ i = h − 1 1 2 i − 1 ⋅ 2 ( h − i ) ≤ 4 n \sum_{i=h-1}^{1} 2^{i-1} \cdot 2(h-i) \leq 4 n i=h112i12(hi)4n
調整“堆頂”要做n-1 次“篩選”,每次“篩選”都要將根結點下移到合適的位置,比較2(h-1)次。n 個關鍵字的完全二叉樹的深度為 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2n\rfloor+1 log2n+1,則重建堆時關鍵字總的比較次數不超過:
2 ( log ⁡ 2 ( n − 1 ) ⌋ + ⌊ log ⁡ 2 ( n − 2 ) ⌋ + … + log ⁡ 2 2 ) < 2 n ( log ⁡ 2 n ⌋ ) \left.\left.2\left(\log _{2}( n -1)\right\rfloor+\left\lfloor\log _{2}( n -2)\right\rfloor+\ldots+\log _{2} 2\right)<2 n \left(\log _{2} n \right\rfloor\right) 2(log2(n1)+log2(n2)++log22)<2n(log2n)
因此,堆排序的時間複雜度為 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
空間複雜度為 O ( 1 ) O(1) O(1)
穩定性:不穩定。

歸併排序

2-路歸併排序

  1. 初始序列看成n個有序子序列,每個子序列長度為1,兩兩合併,得到 ⌊ n / 2 ⌋ \lfloor n/2\rfloor n/2 個長度為2或1的有序子序列;
  2. 再兩兩合併,重複直至得到一個長度為n的有序序列為止。
void Merge( SqList &L,int low,int mid,int high) 
{     
    SqList  L1; L1.length = high-low+1;     
    int i=low, j=mid+1, k=0;//k是L1.R的下標,i、j分別為第1、2路的下標
    while ( i<=mid && j<=high ) 
         if (L.R[i].key<=L.R[j].key) //將關鍵字值小的記錄放入L1中
                  {    L1.R[k]=L.R[i];  i++;k++;     } 
         else   {    L1.R[k]=L.R[j];  j++;k++;   }  
    while (i<=mid)         //如果第1路還有剩餘記錄,將其餘下部分複製到L1
    {      L1.R[k]=L.R[i];  i++;k++;   }
    while (j<=high)        //如果第2路還有剩餘記錄,將其餘下部分複製到L1
    {      L1.R[k]=L.R[j];  j++;k++;  }
    for (k=0,i=low;i<=high; k++,i++) 
        L.R[i]=L1.R[k];//將歸併後的記錄複製回L中
} 

void MergePass(SqList &L,int m)
{   for (int i=0;i+2*m<L.length;i=i+2*m) //歸併長為m的兩相鄰子表               
        Merge(L,i,i+m-1,i+2*m-1);
    if (i+m-1<L.length) 	 //還剩下兩個子表,第1段長度為m,第2段長度小於m
        Merge(L,i,i+m-1,L.length-1);  	//歸併剩餘的這兩個子表
}

時空複雜度和分析

最好情況時間複雜度為 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
最壞情況時間複雜度為 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
空間複雜度為 O ( n ) O(n) O(n)
穩定性:穩定。

排序小結

為避免順序儲存時大量移動記錄的時間開銷,可考慮用連結串列作為儲存結構:直接插入排序、歸併排序、基數排序

不宜採用連結串列作為儲存結構:折半插入排序、希爾排序、快速排序、堆排序

排序演算法選擇規則

  1. n較大時
    (1)分佈隨機,穩定性不做要求,則採用快速排序
    (2)記憶體允許,要求排序穩定時,則採用歸併排序
    (3)可能會出現正序或逆序,穩定性不做要求,則採用堆排序或歸併排序
  2. n較小時
    (1)基本有序,則採用直接插入排序
    (2)分佈隨機,則採用簡單選擇排序,若排序碼不接近逆序,也可以採用直接插入排序

相關文章