1. 程式人生 > >C++ 幾種排序演算法詳解

C++ 幾種排序演算法詳解

排序的演算法有很多種,其關鍵在於根據待排序序列的特性選擇合適的排序方式。下面將介紹不同的排序方式。

基本排序演算法

基本排序演算法主要包括插入排序,快速排序,氣泡排序等三種排序方式,下面將對這三種排序演算法分別進行分析

插入排序

假定待排序陣列序列為:data[5,2,3,8,1],將此陣列從小到大排列

首先對陣列的前面兩個元素進行比較,若data[0]小於data[1]則不進行比較,若data[0]大於data[1],則將data[0]與data[1]互換,即將data[1]插入到合適的位置上去。接下來處理data[2],若data[2]同時小於data[0]與data[1],則將data[2]插入到陣列頭位置上去,那麼data[0]與data[1]都需要向後移動一個位置。若data[2]介於兩者之間,則將data[1]與data[2]交換位置。因此排序的過程就是依次處理陣列元素data[i],並將其插入到合適的位置j上,使0<=j<=i.值得注意的是,在插入範圍內,需要將大於data[i]的所有元素都要移動一個位置。對上述例子進行插入排序的步驟為:

[2,5,3,8,1]

[2,3,5,8,1]

[2,3,5,8,1]

[1,2,3,5,8]

C++實現

void insertionsort(vector<int> &data){
  int n = data.size();
  for(int i = 1; i < n; i++){
    int temp = data[i];
    int j = 0;
    for(j = i; j>0 && temp<data[j-1]; j--){
      data[j] = data[j-1];
    }
    data[j] = temp;
  }
}

時間複雜度與空間複雜度的計算

首先考慮最好的情況,陣列本來不需要排序,那麼僅進行外層的for迴圈即可,此時的執行次數為n-1次,時間複雜度為O(n);

考慮最壞的情況,陣列完全倒序,外層for迴圈依舊要進行n-1次,然而每個第i次外層for迴圈,內層for迴圈也要執行I次,故複雜度為:

void bubblesort(vector<int> &data){
  int n = data.size();
  bool again = true;
  for(int i = 0;  i < n-1 && again; i++){
    for(int j = n-1, again = false; j > i; j--){
      if(data[j] < data[j-1]){
        swap(data[j], data[j-1]);
        again = true;
      }
    }
  }
}

1+2+3+...+n-1 = n(n-1)/2 = O(n^2);

另外,由於插入排序時申請的額外粗存空間與n無關,因此空間複雜度為O(1)。

因此,對於插入排序,時間複雜度為O(1),空間複雜度為O(n^2);

選擇排序

選擇排序的思路是先找到不合適的元素,再把其放在最終的合適位置上去。首先遍歷陣列找到陣列中最小的元素,將此元素與data[0]進行交換,接下來尋找data[1]~data[n-1]中最小的元素,與data[1]進行交換,依次進行完成排序。

例:data[5,2,3,8,1]

data[1,2,3,8,5]

data[1,2,3,8,5]

data[1,2,3,5,8]

C++實現

void selectionsort(vector<int> &data){
  int n = data.size();
  for(int i = 0, j,min; i < n-1; i++){
    for(j = i+1, min = i; j < n; j++)
      if(data[j] < data[min]) min = j;
    swap(data[i], data[min]);
  }
}

時間複雜度與空間複雜度的計算

由於每次選擇都要在data[i]~data[n-1]之間尋找最小的元素,因此無論何種情況,for迴圈的次數都是固定的,第一次進行外部for迴圈是,內部for迴圈執行n-1次,第二次外部for迴圈時內部for迴圈執行n-2次,因此總次數為:

(n-1)+(n-2)+...+2+1 = n(n-1)/2 = O(n^2);

由於也為申請過多的儲存空間,因此空間複雜度為O(1);

因此,選擇排序的時間複雜度為O(n^2),空間複雜度為O(1).

氣泡排序

氣泡排序與上述兩種排序方式在處理方向上有所不同,氣泡排序採用了自底向上的排序方式,首先比較資料data[n-1]和資料data[n-2],若逆序則交換,接著比較data[n-2]與data[n-3],逆序則交換,一直比較到data[1]與data[0],經過這些操作,得以將最小的元素移動到陣列的頭位置。接下來繼續比較,這次比較到data[1],將陣列的次小元素移動到data[1]位置。

例:data[5,2,3,8,1]

data[5,2,3,1,8]

data[5,2,1,3,8]

data[5,1,2,3,8]

data[1,5,2,3,8]

data[1,2,5,3,8]

C++實現

void bubblesort(vector<int> &data){
  int n = data.size();
  bool again = true;
  for(int i = 0;  i < n-1 && again; i++){
    for(int j = n-1, again = false; j > i; j--){
      if(data[j] < data[j-1]){
        swap(data[j], data[j-1]);
        again = true;
      }
    }
  }
}

時間複雜度與空間複雜度

根據上述程式碼可以看出,在最好情況下,陣列已經為正序,則需要的比較次數為n-1,時間複雜度為O(n);

最壞情況下,陣列為倒序,需要的比較次數為n-1+n-2+...+2+1 = n(n-1)/2 = O(n^2);

因此,氣泡排序的時間複雜度為O(n^2), 空間複雜度為(1);

高效排序演算法

高效排序演算法主要有希爾排序,堆排序,快速排序,歸併排序,基數排序。

希爾排序

希爾排序演算法的主要思想是將待排序陣列拆分為幾個陣列分別進行排序,然後對這幾個陣列再進行排序,經驗證使用這種辦法的時間複雜度要小於O(n^2).首先每隔hi個數取一個元素,對這些元素進行插入排序,然後每隔h(i-1)個數取一個元素,依次排序直到h1 = 1;

通過例子解讀希爾排序:data[10,8,6,20,4,3,22,1,0,15,16]

首先每個5個元素取一個元素,從第一個元素開始取,對元素[10,3,16]排序為[3,10,16], 原陣列變為:

data[3,8,1,0,4,10,22,,6,20,15,16]

繼續取5個元素,從第二個元素開始取,對元素[8,22]排序,原陣列變為

data[3,8,1,0,4,10,22,,6,20,15,16],依次從第三個元素開始取,一直到如下情況:

data[3,8,1,0,4,10,22,,6,20,15,16],5-排序完成,h減小,開始進行3-排序。

3-排序與5-排序操作相同,一直進行到1-排序,希爾排序完成。

如何選定h的值呢,通常情況下我們有:
h(1) = 1    h(i+1)=3*h(i)+1;

直到h(i+2)>=n時停止得到h,然後依次上式遞減h來進行希爾排序。

C++實現

void shellsort(vector<int> &data){
  int n = data.size();
  int h = 0, i = 0;
  vector<int> increments;
  for(h=1; h < n; ){
    increments.push_back(h);
    h = 3*h+1;
  }

  for(i = increments.size()-1; i >= 0; i--){
    h = increments[i];
    for(int hCnt = h; hCnt < 2*h; hCnt++){
      //插入排序部分
      for(int j = hCnt; j < n; j = j+h){
        int temp = data[j];
        int k = 0;
        for(k = j; k-h>0 && temp < data[k-h]; k = k-h){
          data[k] = data[k-h];
        }
        data[k] = temp;
      }
    }
  }
}

注意上述程式碼插入部分與上面提到的插入排序幾乎一致,區別僅在於遞增的個數。其中,2*h代表了遞增條件為h時,根據h取得的第二個元素的極限位置,即上述樣例中藍色部分。

時間複雜度與空間複雜度

希爾排序最優情況下時間複雜度為O(n^1.3),最壞情況下時間複雜度為O(n^2),空間複雜度為O(1).(不做證明)

堆排序

堆排序可以看作是一個選擇排序的逆過程,依次找到陣列中最大的元素放置於陣列末端,區別在於尋找最大元素的方式,堆排序顧名思義,是將陣列表示為堆的形式然後進行排序,那什麼是堆呢?

堆是一種特殊型別的二叉樹,堆主要具有兩個性質,一是每個節點的值大於等於每個子節點的值,二是該二叉樹完全平衡,最後一層的葉子都位於最左側的位置。上述表示的是最大堆,最小堆與上面的表示正好相反,最大堆的主要性質如下:

  1. 索引為i的左孩子的索引是(2*i+1)
  2. 索引為i的右孩子的索引是(2*i+2)
  3. 索引為i的父節點的索引為floor((i-1)/2)

使用堆排序的步驟如下:

  1. 將陣列初始化為堆
  2. 交換a[0]與a[n-1]
  3. 挑戰剩下的a[0]~a[n-2]為最大堆
  4. 重複2~3步

初始化堆

假設排序前陣列為:data[20,30,90,40,70,110,60,10,100,50,80],n=11進行排序.

初始化i=(11-2)/2, i=4.data[20,30,90,40,70,110,60,10,100,50,80], 根據性質計算得到,data[4]的左孩子為data[9]=50, data[4]的右孩子為data[10]=80.故將data[10]與data[4]交換.由於2*10+1超出陣列範圍,故不再進行交換。

i=i-1=4-1=3. data[20,30,90,40,80,110,60,10,100,50,70], data[3]的左孩子為data[7]=10, data[3]的右孩子為data[8]=100,故將data[3]與data[8]交換,由於2*8+1超出陣列範圍,故不再進行交換。

i=i-1=3-1=2. data[20,30,90,100,80,110,60,10,40,50,70] , data[2]的左孩子為data[5]=110, data[2]的右孩子為data[6]=60,故將data[2]與data[5]進行交換,由於2*6+1超出陣列範圍,故不再進行交換。

i=i-1=1. data[20,30,110,100,80,90,60,10,40,50,70], data[1]的左孩子為data[3]=100,data[2]的右孩子為data[4]=80,故將data[1]與data[3]進行交換,data[20,100,110,30,80,90,60,10,40,50,70], 由於2*3+1=7未超出陣列範圍,故計算data[3]的左右孩子,data[3]的左孩子data[7]=10, data[3]的右孩子data[8]=40,故將data[3]與data[8]進行交換,得到data[20,100,110,40,80,90,60,10,30,50,70].由於8*3+1超出陣列範圍,故不再進行更新。

i=i-1=0. data[20,100,110,40,80,90,60,10,30,50,70], data[0]的左孩子為data[1]=100, data[0]的右孩子為data[2]=110, 故將data[0]與data[2]進行交換,得到data[110,100,20,40,80,90,60,10,30,50,70],由於2*2+1=5未超出陣列範圍,故計算data[2]的左右孩子,data[2]的左孩子data[5]=90, data[2]的右孩子data[6]=60,故將data[2]與data[5]進行交換,得到data[110,100,90,40,80,20,60,10,30,50,70].

排序,交換資料

首先將data[]中的第一個元素與最後一個元素交換,得到data[70,100,90,40,80,20,60,10,30,50,110], 此時最大的元素已經移動到陣列末端。對剩下的陣列部分初始化堆data[70,100,90,40,80,20,60,10,30,50],根據上式i=0的情況初始化堆。然後繼續進行排序交換步驟,直到所有元素排序完成。

C++實現

void moveDown(vector<int> &a, int start, int end){
  int c = start;
  int l = start*2+1;
  for(; l <= end; c=l,l=2*l+1){
    if(l<end && a[l]<a[l+1])
      l++;
    if(a[c]>a[l])
      break;
    else{
      swap(a[c],a[l]);
    }
  }
}

void heapsort(vector<int> &a, int n){
  int i = (n-2)/2;
  //陣列初始化為堆
  for(int j = i; j >= 0; j--){
    moveDown(a,j,n-1);
  }

  for(int k = n-1; k>0; k--){
    swap(a[0],a[k]);
    moveDown(a, 0, k-1);
  }

}

時間複雜度與空間複雜度的計算

堆排序的時間複雜度為O(nlogn), 空間複雜度為O(1),為不穩定排序。

快速排序

快速排序的核心思想是在陣列中找到一個元素,根據這個元素對陣列進行劃分,繼續在劃分完成的陣列內尋找元素,根據此元素將陣列拆分,快速排序是一個遞迴的過程。劃分一次的步驟如下:data[8,5,4,7,6,1,6,3,8,12,10]

1.選定基準,基準可以任意選取陣列內元素內容,一般選取陣列首個元素或者中間元素。

2.從左端開始遍歷陣列,一直遍歷到陣列元素小於基準時暫停遍歷,開始從頭開始遍歷陣列元素,一直到遍歷到 大於基準時暫停遍歷,此時交換左右兩端遍歷到的陣列元素,持續上述過程直到i == j。交換i處與基準處的元素內容。隨後在i處將陣列拆分為兩個。

3.在上述步驟拆分得到的兩個陣列內依次使用1,2步驟。

C++實現

void quiksort(vector<int> &a, int left, int right){
  if(left > right)
    return;
  int i = left, j = right;
  int mid = (left+right)/2;
  int base = a[mid];
  while(i != j){
    while(a[j]>base && i < j)
      j--;
    while(a[i]<=base && i < j)
      i++;
    if(i<j)
      swap(a[i],a[j]);
  }
  swap(a[mid],a[j]);
  quiksort(a,left,i-1);
  quiksort(a,i+1,right);
}

時間複雜度與空間複雜度

快速排序的時間複雜度為O(n^2), 空間複雜度為O(nlogn);

歸併排序

歸併排序與快速排序類似,都是先保證子序列有序,然後對各子序列進行排序,歸併排序的例子如下data[9,6,7,22,20,33,16,20].

1. 劃分子區間,劃分到子區間內元素小於兩個即停止劃分。

2. 歸併過程類似於將兩個有序數組合併為一個有序陣列,具體見程式碼

C++實現

void merge(vector<int>& nums, int l1, int r1, int l2, int r2 ) {
  int i = l1;                                               //左半部分起始位置
  int j = l2;                                               //右半部分起始位置
  int n = (r1 - l1 + 1) + (r2 - l2 + 1);                    //要合併的元素個數
  vector<int> temp(n);                                      //輔助陣列
  int k = 0;	                                          //輔助陣列其起始位置
  while (i <= r1&&j <= r2) {                                //挑選兩部分中最小的元素放入輔助陣列中
    if (nums[i] < nums[j])
      temp[k++] = nums[i++];
    else
      temp[k++] = nums[j++];
  }

  //如果還有剩餘,直接放入到輔助陣列中
  while (i <= r1)
    temp[k++] = nums[i++];
  while (j <= r2)
    temp[k++] = nums[j++];
  //更新原始陣列元素
  for (int i = 0; i < n;i++)
  {
    nums[l1 + i] = temp[i];
  }
}



/*二路歸併排序(遞迴實現)*/

void MergeSort(vector<int>& nums,int start, int end) {
  if (start < end) {
    int mid = (start + end)/2;
    MergeSort(nums, start, mid);
    MergeSort(nums, mid + 1, end);
    merge(nums, start, mid, mid + 1, end);
  }
}

時間複雜度與空間複雜度

歸併排序的時間複雜度為O(nlogn),空間複雜度為O(1).為穩定排序