【基本數據結構與經典算法】排序算法 C++版本 總結!

0、算法概述

0.1 算法分類

十種常見排序算法可以分爲兩大類:

  • 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
  • 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。 

0.2 算法複雜度

0.3 相關概念

  • 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
  • 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
  • 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
  • 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數

 

1、冒泡排序(Bubble Sort)

冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。 

1.1 算法描述

  • 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
  • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
  • 針對所有的元素重複以上的步驟,除了最後一個;
  • 重複步驟1~3,直到排序完成。

 1.2 圖片演示

我們先分析第1趟排序
當i=5,j=0時,a[0]<a[1]。此時,不做任何處理!
當i=5,j=1時,a[1]>a[2]。此時,交換a[1]和a[2]的值;交換之後,a[1]=30,a[2]=40。
當i=5,j=2時,a[2]>a[3]。此時,交換a[2]和a[3]的值;交換之後,a[2]=10,a[3]=40。
當i=5,j=3時,a[3]<a[4]。此時,不做任何處理!
當i=5,j=4時,a[4]>a[5]。此時,交換a[4]和a[5]的值;交換之後,a[4]=50,a[3]=60。

於是,第1趟排序完之後,數列{20,40,30,10,60,50}變成了{20,30,10,40,50,60}。此時,數列末尾的值最大。

 

根據這種方法:
第2趟排序完之後,數列中a[5...6]是有序的。
第3趟排序完之後,數列中a[4...6]是有序的。
第4趟排序完之後,數列中a[3...6]是有序的。
第5趟排序完之後,數列中a[1...6]是有序的。

第5趟排序之後,整個數列也就是有序的了。

觀察上面冒泡排序的流程圖,第3趟排序之後,數據已經是有序的了;第4趟和第5趟並沒有進行數據交換。
下面我們對冒泡排序進行優化,使它效率更高一些:添加一個標記,如果一趟遍歷中發生了交換,則標記爲true,否則爲false。如果某一趟沒有發生交換,說明排序已經完成。

冒泡排序時間複雜度

冒泡排序的時間複雜度是O(N2)。
假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?N-1次!因此,冒泡排序的時間複雜度是O(N2)。

冒泡排序穩定性

冒泡排序是穩定的算法,它滿足穩定算法的定義。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!

1.3 代碼實現

/**
 * 冒泡排序:C++
 *
 * @author skywang
 * @date 2014/03/11
 */

#include <iostream>
using namespace std;

/*
 * 冒泡排序
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void bubbleSort1(int* a, int n)
{
    int i,j,tmp;

    for (i=n-1; i>0; i--)
    {
        // 將a[0...i]中最大的數據放在末尾
        for (j=0; j<i; j++)
        {
            if (a[j] > a[j+1])
            {    
                // 交換a[j]和a[j+1]
                tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = tmp;
            }
        }
    }
}

/*
 * 冒泡排序(改進版)
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void bubbleSort2(int* a, int n)
{
    int i,j,tmp;
    int flag;                 // 標記

    for (i=n-1; i>0; i--)
    {
        flag = 0;            // 初始化標記爲0

        // 將a[0...i]中最大的數據放在末尾
        for (j=0; j<i; j++)
        {
            if (a[j] > a[j+1])
            {
                // 交換a[j]和a[j+1]
                tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = tmp;

                flag = 1;    // 若發生交換,則設標記爲1
            }
        }

        if (flag==0)
            break;            // 若沒發生交換,則說明數列已有序。
    }
}

int main()
{
    int i;
    int a[] = {20,40,30,10,60,50};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    bubbleSort1(a, ilen);
    //bubbleSort2(a, ilen);

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

 

2、快速排序(Quick Sort)

快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。

2.1 算法描述

快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述如下:

  • 從數列中挑出一個元素,稱爲 “基準”(pivot);
  • 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
  • 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

2.2 圖片演示

上圖只是給出了第1趟快速排序的流程。在第1趟中,設置x=a[i],即x=30。
(01) 從"右 --> 左"查找小於x的數:找到滿足條件的數a[j]=20,此時j=4;然後將a[j]賦值a[i],此時i=0;接着從左往右遍歷。
(02) 從"左 --> 右"查找大於x的數:找到滿足條件的數a[i]=40,此時i=1;然後將a[i]賦值a[j],此時j=4;接着從右往左遍歷。
(03) 從"右 --> 左"查找小於x的數:找到滿足條件的數a[j]=10,此時j=3;然後將a[j]賦值a[i],此時i=1;接着從左往右遍歷。
(04) 從"左 --> 右"查找大於x的數:找到滿足條件的數a[i]=60,此時i=2;然後將a[i]賦值a[j],此時j=3;接着從右往左遍歷。
(05) 從"右 --> 左"查找小於x的數:沒有找到滿足條件的數。當i>=j時,停止查找;然後將x賦值給a[i]。此趟遍歷結束!

按照同樣的方法,對子數列進行遞歸遍歷。最後得到有序數組!

快速排序穩定性
快速排序是不穩定的算法,它不滿足穩定算法的定義。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!

快速排序時間複雜度
快速排序的時間複雜度在最壞情況下是O(N2),平均的時間複雜度是O(N*lgN)。
這句話很好理解:假設被排序的數列中有N個數。遍歷一次的時間複雜度是O(N),需要遍歷多少次呢?至少lg(N+1)次,最多N次。
(01) 爲什麼最少是lg(N+1)次?快速排序是採用的分治法進行遍歷的,我們將它看作一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的定義,它的深度至少是lg(N+1)。因此,快速排序的遍歷次數最少是lg(N+1)次。
(02) 爲什麼最多是N次?這個應該非常簡單,還是將快速排序看作一棵二叉樹,它的深度最大是N。因此,快讀排序的遍歷次數最多是N次。

2.3 代碼實現 

/**
 * 快速排序:C++
 *
 * @author skywang
 * @date 2014/03/11
 */

#include <iostream>
using namespace std;

/*
 * 快速排序
 *
 * 參數說明:
 *     a 		-- 待排序的數組
 *     left 	-- 數組的左邊界(例如,從起始位置開始排序,則left=0)
 *     right 	-- 數組的右邊界(例如,排序截至到數組末尾,則right=a.length-1)
 */
void quickSort(int* a, int left, int right)
{
    if (left < right)
    {
        int i,j,x;

        i = left;
        j = right;
        x = a[i];
        while (i < j)
        {
            while(i < j && a[j] > x)
                j--; // 從右向左找第一個小於x的數
            if(i < j)
                a[i++] = a[j];
            while(i < j && a[i] < x)
                i++; // 從左向右找第一個大於x的數
            if(i < j)
                a[j--] = a[i];
        }
        a[i] = x;
        quickSort(a, left, i-1); /* 遞歸調用 */
        quickSort(a, i+1, right); /* 遞歸調用 */
    }
}

int main()
{
    int i;
    int a[] = {80,70,40,12,33,90};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    quickSort(a, 0, ilen-1);

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

3、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

3.1 算法描述

一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:

  • 從第一個元素開始,該元素可以認爲已經被排序;
  • 取出下一個元素,在已經排序的元素序列中從後向前掃描;
  • 如果該元素(已排序)大於新元素,將該元素移到下一位置;
  • 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  • 將新元素插入到該位置後;
  • 重複步驟2~5。

3.2 圖片演示

直接插入排序時間複雜度
直接插入排序的時間複雜度是O(N2)
假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?N-1!因此,直接插入排序的時間複雜度是O(N2)。

直接插入排序穩定性
直接插入排序是穩定的算法,它滿足穩定算法的定義。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!

3.3 代碼實現 


#include <stdio.h> 
void insert_sort(int a[],int n)
//待排序元素用一個數組a表示,數組有n個元素
{
    int i,j;
    int temp;
    for ( i=1; i<n; i++) //i表示插入次數,共進行n-1次插入
  {
      temp=a[i]; //把待排序元素賦給temp,temp在while循環中並不改變,這樣方便比較,並且它是要插入的元素
      j=i-1;
      //while循環的作用是將比當前元素大的元素都往後移動一個位置
      while ((j>=0)&& (temp<a[j])){
          a[j+1]=a[j];
          j--; // 順序比較和移動,依次將元素後移動一個位置
          }
 
      a[j+1]=temp;//元素後移後要插入的位置就空出了,找到該位置插入
  }
}
int main(){
    int array[]={2, 10, 4, 5, 1, 9};
    int i=0;
    insert_sort(array,6);
    for(;i<=5;i++)
       printf("[%d]",array[i]);
	return 0;
    }

4、希爾排序(Shell Sort)

 希爾排序屬於插入類排序,是將整個有序序列分割成若干小的子序列分別進行插入排序。

 希爾排序最後一趟已經基本有序,比較次數和移動次數更少。

4.1 算法描述

 排序過程:先取一個正整數d1<n,把所有序號相隔d1的數組元素放一組,組內進行直接插入排序;然後取d2<d1,重複上述分組和排序操作;直至di=1,即所有記錄放進一個組中排序爲止。

4.2 圖片演示

 

我們來看下希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2...1},稱爲增量序列。

圖中有10個數,所以第一趟增量就設爲10/2=5;看圖中第一趟排序,顏色相同爲一組,每組進行比較,可以看到,這十個數被分成了五組        [9,4] [1,8] [2,6] [5,3] [7,5],     然後對這五組分別進行直接插入排序

第二趟繼續進行,縮小增量爲5/2=2;這時候可以看到這些數被分成了兩組[4,2,5,8,5] [1,3,9,6,7],繼續分別對這兩組進行直接插入排序。

第三趟 縮小增量爲 2/2=1;這時候,只需要對這些數字進行微調就可以滿足要求。

 4.3 代碼實現

/**
 * 希爾排序:C++
 *
 * @author skywang
 * @date 2014/03/11
 */

#include <iostream>
using namespace std;

/*
 * 希爾排序
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void shellSort1(int* a, int n)
{
    int i,j,gap;

    // gap爲步長,每次減爲原來的一半。
    for (gap = n / 2; gap > 0; gap /= 2)
    {
        // 共gap個組,對每一組都執行直接插入排序
        for (i = 0 ;i < gap; i++)
        {
            for (j = i + gap; j < n; j += gap)
            {
                // 如果a[j] < a[j-gap],則尋找a[j]位置,並將後面數據的位置都後移。
                if (a[j] < a[j - gap])
                {
                    int tmp = a[j];
                    int k = j - gap;
                    while (k >= 0 && a[k] > tmp)
                    {
                        a[k + gap] = a[k];
                        k -= gap;
                    }
                    a[k + gap] = tmp;
                }
            }
        }

    }
}

/*
 * 對希爾排序中的單個組進行排序
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組總的長度
 *     i -- 組的起始位置
 *     gap -- 組的步長
 *
 *  組是"從i開始,將相隔gap長度的數都取出"所組成的!
 */
void groupSort(int* a, int n, int i,int gap)
{
    int j;

    for (j = i + gap; j < n; j += gap)
    {
        // 如果a[j] < a[j-gap],則尋找a[j]位置,並將後面數據的位置都後移。
        if (a[j] < a[j - gap])
        {
            int tmp = a[j];
            int k = j - gap;
            while (k >= 0 && a[k] > tmp)
            {
                a[k + gap] = a[k];
                k -= gap;
            }
            a[k + gap] = tmp;
        }
    }
}

/*
 * 希爾排序
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void shellSort2(int* a, int n)
{
    int i,gap;

    // gap爲步長,每次減爲原來的一半。
    for (gap = n / 2; gap > 0; gap /= 2)
    {
        // 共gap個組,對每一組都執行直接插入排序
        for (i = 0 ;i < gap; i++)
            groupSort(a, n, i, gap);
    }
}

int main()
{
    int i;
    int a[] = {80,30,60,40,20,10,50,70};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    shellSort1(a, ilen); //直接把插入排序寫進去
    //shellSort2(a, ilen); //把插入排序拎出來方便理解

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

5、選擇排序(Selection Sort)

選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。 (交換)

5.1 算法描述 

選擇排序是蠻力法在排序算法中的一個重要運用,選擇排序開始的時候,我們掃描整個列表,找到它的最小元素然後和第一個元素交換,將最小元素放到它在有序表的最終位置上。然後我們從第二個元素開始掃描列表,找到最後n-1個元素的最小元素,再和第二個元素交換位置,把第二小的元素放在它最終的位置上。如此循環下去,在n-1遍以後,列表就排好序了。

n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體算法描述如下:

  • 初始狀態:無序區爲R[1..n],有序區爲空;
  • 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別爲R[1..i-1]和R(i..n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]和R[i+1..n)分別變爲記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
  • n-1趟結束,數組有序化了。

 5.2 圖片演示

 

排序流程

第1趟:i=0。找出a[1...5]中的最小值a[3]=10,然後將a[0]和a[3]互換。 數列變化:20,40,30,10,60,50 -- > 10,40,30,20,60,50
第2趟:i=1。找出a[2...5]中的最小值a[3]=20,然後將a[1]和a[3]互換。 數列變化:10,40,30,20,60,50 -- > 10,20,30,40,60,50
第3趟:i=2。找出a[3...5]中的最小值,由於該最小值大於a[2],該趟不做任何處理。 
第4趟:i=3。找出a[4...5]中的最小值,由於該最小值大於a[3],該趟不做任何處理。 
第5趟:i=4。交換a[4]和a[5]的數據。 數列變化:10,20,30,40,60,50 -- > 10,20,30,40,50,60

5.3 代碼實現

#include <iostream>
using namespace std;

/*
 * 選擇排序
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void selectSort(int* a, int n)
{
    int i;        // 有序區的末尾位置
    int j;        // 無序區的起始位置
    int min;    // 無序區中最小元素位置

    for(i=0; i<n; i++)
    {
        min=i;

        // 找出"a[i+1] ... a[n]"之間的最小元素,並賦值給min。
        for(j=i+1; j<n; j++)
        {
            if(a[j] < a[min])
                min=j;
        }

        // 若min!=i,則交換 a[i] 和 a[min]。
        // 交換之後,保證了a[0] ... a[i] 之間的元素是有序的。
        // ice:其實還是無序的,可以把if(min != i)去掉,直接寫函數體裏的交換代碼就可以。
        if(min != i)
        {
            int tmp = a[i];
            a[i] = a[min];
            a[min] = tmp;
        }
    }
}

int main()
{
    int i;
    int a[] = {20,40,30,10,60,50};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    selectSort(a, ilen);

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

6、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

升序->大頂堆

降序->小頂堆

參考:https://www.cnblogs.com/lanhaicode/p/10546257.html

6.1 算法描述

  • 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區;
  • 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
  • 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整爲新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。

 6.2 圖片演示

6.3 舉例說明

 給定一個列表array=[16,7,3,20,17,8],對其進行堆排序。

首先根據該數組元素構建一個完全二叉樹,得到

然後需要構造初始堆,則從最後一個非葉節點開始調整,調整過程如下:

第一步: 初始化大頂堆(從最後一個子節點開始往上調整最大堆) 由下往上

20和16交換後導致16不滿足堆的性質,因此需重新調整

這樣就得到了初始堆。

第二步: 堆頂元素R[1]與最後一個元素R[n]交換,交換後堆長度減一

即每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換(交換之後可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整)。有了初始堆之後就可以進行排序了。

第三步: 重新調整堆。此時3位於堆頂不滿堆的性質,則需調整繼續調整(從頂點開始往下調整) 由上往下

重複上面的步驟:

請特別特別注意: 

初始化大頂堆時 是從最後一個非葉子節點開始從下往上調整最大堆。

而堆頂元素(最大數)與堆最後一個數交換後,需再次調整成大頂堆,此時是從上往下調整的。

不管是初始大頂堆的從下往上調整,還是堆頂堆尾元素交換,每次調整都是從父節點、左孩子節點、右孩子節點三者中選擇最大者跟父節點進行交換,交換之後都可能造成被交換的孩子節點不滿足堆的性質,因此每次交換之後要重新對被交換的孩子節點進行調整

堆排序時間複雜度
堆排序的時間複雜度是O(N*lgN)。
假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?
堆排序是採用的二叉堆進行排序的,二叉堆就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的定義,它的深度至少是lg(N+1)。最多是多少呢?由於二叉堆是完全二叉樹,因此,它的深度最多也不會超過lg(2N)。因此,遍歷一趟的時間複雜度是O(N),而遍歷次數介於lg(N+1)和lg(2N)之間;因此得出它的時間複雜度是O(N*lgN)。

堆排序穩定性
堆排序是不穩定的算法,它不滿足穩定算法的定義。它在交換數據的時候,是比較父結點和子節點之間的數據,所以,即便是存在兩個數值相等的兄弟節點,它們的相對順序在排序也可能發生變化。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序算法是穩定的!

6.4 代碼實現

/**
 * 堆排序:C++
 *
 * @author skywang
 * @date 2014/03/11
 */

#include <iostream>
using namespace std;

/* 
 * (最大)堆的向下調整算法
 *
 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N爲數組下標索引值,如數組中第1個數對應的N爲0。
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     start -- 被下調節點的起始位置(一般爲0,表示從第1個開始)
 *     end   -- 截至範圍(一般爲數組中最後一個元素的索引)
 */
void maxHeapDown(int* a, int start, int end)
{
    int c = start;            // 當前(current)節點的位置
    int l = 2*c + 1;        // 左(left)孩子的位置
    int tmp = a[c];            // 當前(current)節點的大小
    for (; l <= end; c=l,l=2*l+1)
    {
        // "l"是左孩子,"l+1"是右孩子
        if ( l < end && a[l] < a[l+1])
            l++;        // 左右兩孩子中選擇較大者,即m_heap[l+1]
        if (tmp >= a[l])
            break;        // 調整結束
        else            // 交換值
        {
            a[c] = a[l];
            a[l]= tmp;
        }
    }
}

/*
 * 堆排序(從小到大)
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void heapSortAsc(int* a, int n)
{
    int i,tmp;

    // 從(n/2-1) --> 0逐次遍歷。遍歷之後,得到的數組實際上是一個(最大)二叉堆。
    for (i = n / 2 - 1; i >= 0; i--)
        maxHeapDown(a, i, n-1);

    // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素
    for (i = n - 1; i > 0; i--)
    {
        // 交換a[0]和a[i]。交換後,a[i]是a[0...i]中最大的。
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。
        // 即,保證a[i-1]是a[0...i-1]中的最大值。
        maxHeapDown(a, 0, i-1);
    }
}

/* 
 * (最小)堆的向下調整算法
 *
 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
 *     其中,N爲數組下標索引值,如數組中第1個數對應的N爲0。
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     start -- 被下調節點的起始位置(一般爲0,表示從第1個開始)
 *     end   -- 截至範圍(一般爲數組中最後一個元素的索引)
 */
void minHeapDown(int* a, int start, int end)
{
    int c = start;            // 當前(current)節點的位置
    int l = 2*c + 1;        // 左(left)孩子的位置
    int tmp = a[c];            // 當前(current)節點的大小
    for (; l <= end; c=l,l=2*l+1)
    {
        // "l"是左孩子,"l+1"是右孩子
        if ( l < end && a[l] > a[l+1])
            l++;        // 左右兩孩子中選擇較小者
        if (tmp <= a[l])
            break;        // 調整結束
        else            // 交換值
        {
            a[c] = a[l];
            a[l]= tmp;
        }
    }
}

/*
 * 堆排序(從大到小)
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     n -- 數組的長度
 */
void heapSortDesc(int* a, int n)
{
    int i,tmp;

    // 從(n/2-1) --> 0逐次遍歷每。遍歷之後,得到的數組實際上是一個最小堆。
    for (i = n / 2 - 1; i >= 0; i--)
        minHeapDown(a, i, n-1);

    // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素
    for (i = n - 1; i > 0; i--)
    {
        // 交換a[0]和a[i]。交換後,a[i]是a[0...i]中最小的。
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        // 調整a[0...i-1],使得a[0...i-1]仍然是一個最小堆。
        // 即,保證a[i-1]是a[0...i-1]中的最小值。
        minHeapDown(a, 0, i-1);
    }
}

int main()
{
    int i;
    int a[] = {20,30,90,40,70,110,60,10,100,50,80};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    heapSortAsc(a, ilen);            // 升序排列
    //heapSortDesc(a, ilen);        // 降序排列

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

7、歸併排序(Merge Sort)

歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。 

7.1 算法描述

  • 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
  • 對這兩個子序列分別採用歸併排序;
  • 將兩個排序好的子序列合併成一個最終的排序序列。

7.2 圖片演示

 

歸併排序(MERGE-SORT)是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題(divide)成一些小的問題然後遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。

分而治之

   可以看到這種結構很像一棵完全二叉樹,本文的歸併排序我們採用遞歸去實現(也可採用迭代的方式去實現)。階段可以理解爲就是遞歸拆分子序列的過程,遞歸深度爲log2n。

合併相鄰有序子序列

  再來看看階段,我們需要將兩個已經有序的子序列合併成一個有序序列,比如上圖中的最後一次合併,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合併爲最終序列[1,2,3,4,5,6,7,8],來看下實現步驟。

7.3 代碼實現

/**
 * 歸併排序:C++
 *
 * @author skywang
 * @date 2014/03/12
 */

#include <iostream>
using namespace std;

/*
 * 將一個數組中的兩個相鄰有序區間合併成一個
 *
 * 參數說明:
 *     a -- 包含兩個有序區間的數組
 *     start -- 第1個有序區間的起始地址。
 *     mid   -- 第1個有序區間的結束地址。也是第2個有序區間的起始地址。
 *     end   -- 第2個有序區間的結束地址。
 */
void merge(int* a, int start, int mid, int end)
{
    int *tmp = new int[end-start+1];    // tmp是彙總2個有序區的臨時區域
    int i = start;            // 第1個有序區的索引
    int j = mid + 1;        // 第2個有序區的索引
    int k = 0;                // 臨時區域的索引

    while(i <= mid && j <= end)
    {
        if (a[i] <= a[j])
            tmp[k++] = a[i++];
        else
            tmp[k++] = a[j++];
    }

    while(i <= mid)
        tmp[k++] = a[i++];

    while(j <= end)
        tmp[k++] = a[j++];

    // 將排序後的元素,全部都整合到數組a中。
    for (i = 0; i < k; i++)
        a[start + i] = tmp[i];

    delete[] tmp;
}

/*
 * 歸併排序(從上往下)
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     start -- 數組的起始地址
 *     endi -- 數組的結束地址
 */
void mergeSortUp2Down(int* a, int start, int end)
{
    if(a==NULL || start >= end)
        return ;

    int mid = (end + start)/2;
    mergeSortUp2Down(a, start, mid); // 遞歸排序a[start...mid]
    mergeSortUp2Down(a, mid+1, end); // 遞歸排序a[mid+1...end]

    // a[start...mid] 和 a[mid...end]是兩個有序空間,
    // 將它們排序成一個有序空間a[start...end]
    merge(a, start, mid, end);
}


/*
 * 對數組a做若干次合併:數組a的總長度爲len,將它分爲若干個長度爲gap的子數組;
 *             將"每2個相鄰的子數組" 進行合併排序。
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     len -- 數組的長度
 *     gap -- 子數組的長度
 */
void mergeGroups(int* a, int len, int gap)
{
    int i;
    int twolen = 2 * gap;    // 兩個相鄰的子數組的長度

    // 將"每2個相鄰的子數組" 進行合併排序。
    for(i = 0; i+2*gap-1 < len; i+=(2*gap))
    {
        merge(a, i, i+gap-1, i+2*gap-1);
    }

    // 若 i+gap-1 < len-1,則剩餘一個子數組沒有配對。
    // 將該子數組合併到已排序的數組中。
    if ( i+gap-1 < len-1)
    {
        merge(a, i, i + gap - 1, len - 1);
    }
}

/*
 * 歸併排序(從下往上)
 *
 * 參數說明:
 *     a -- 待排序的數組
 *     len -- 數組的長度
 */
void mergeSortDown2Up(int* a, int len)
{
    int n;

    if (a==NULL || len<=0)
        return ;

    for(n = 1; n < len; n*=2)
        mergeGroups(a, len, n);
}

int main()
{
    int i;
    int a[] = {80,30,60,40,20,10,50,70};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    # 我們以上圖片展示的就是從上往下的思路, 從下往上的先不要看了...
    mergeSortUp2Down(a, 0, ilen-1);        // 歸併排序(從上往下)
    //mergeSortDown2Up(a, ilen);            // 歸併排序(從下往上)

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

8、計數排序(Counting Sort)

計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

8.1 算法描述

  • 找出待排序的數組中最大和最小的元素;
  • 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項;
  • 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
  • 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。

8.2 圖片演示

è¿éåå¾çæè¿°

 8.3 舉例說明

 

  • 假設有 8 個考生,他們的分數範圍爲 [0, 5]。這 8 個考生的成績我們放在一個數組中,A[8] = {2, 5, 3, 0, 2, 3, 0, 3}。
  • 我們用大小爲 6 的數組代表 6 個桶來統計考生的成績分佈情況,其中下標表示考生的分數,數組內的值表示這個分數的考生個數。我們遍歷一遍數組後,就可以得到 C[6] = {2, 0, 2, 3, 0, 1,},得 0 分的共有 2 人,得 3 分的共有 3 人。
  • 如下所示,成績爲 3 的考生共有 3 個,小於 3 分的考生共有 4 個,所以在排好序的數據 R[8] 中,3 的位置應該爲 4,5 和 6 。
  • 而我們怎麼得到這個位置呢?只需要對 C[6] 數組按順序累計求和即可,這時候,C[6] 數組中的每個值就都表示小於等於它的值的個數了。

  • 接下來,我們從後到前依次掃描數組 A[8]。當掃描到 3 時,我們取出 C[3] 的值 7,說明小於等於 3 的個數爲 7 個,那麼 3 就應該放在數組 R[8] 的第 7 個位置,也就是下標爲 6 的地方,然後把 C[3] 相應地減去 1。當我們再次遇到 3 的時候,這時候小於等於 3 的元素個數就少了一個。
  • 之所以要從後到前依次掃描數組,是因爲這樣之前相同的元素就仍然會保持相同的順序,可以保證排序算法的穩定性
  • 當我們掃描完整個數組 A[8] 時,數組 R[8] 中的數據也就從小到大排好序了,其詳細過程可參考下圖。

 

  8.4 代碼實現

 注意:以下代碼只是找了最大值出來作爲作爲臨時計數數組的長度。實際中爲了避免空間浪費,可以用max-min+1的長度來作爲數組長度,當然下標也要相應改變。

另外,

  • 計數排序只適用於數據範圍不大的場景中,如果數據範圍比排序的數據大很多,就不適合用計數排序了。
  • 計數排序能給非負整數排序,如果數據是其他類型的,需要將其在不改變相對大小的情況下,轉化爲非負整數。比如數據有一位小數,我們需要將數據都乘以 10;數據範圍爲 [-1000, 1000],我們需要對每個數據加 1000。
#include <iostream>
using namespace std;
// 假設數組中存儲的都是非負整數
void Counting_Sort(int data[], int n)
{
    if (n <= 1)
    {
        return;
    }

    // 尋找數組的最大值
    int max = data[0];
    for (int i = 1; i < n; i++)
    {
        if (data[i] > max)
        {
            max = data[i];
        }
    }

    // 定義一個計數數組, 統計每個元素的個數
    int c[max+1] = {0};
    for (int i = 0; i < n; i++)
    {
        c[data[i]]++;
    }

    // 對計數數組累計求和
    for (int i = 1; i <= max; i++)
    {
        c[i] = c[i] + c[i-1];
    }

    // 臨時存放排好序的數據
    int r[n] = {0};
    // 倒序遍歷數組,將元素放入正確的位置
    for (int i = n-1; i >= 0; i--)
    {
        r[c[data[i]] - 1] = data[i];
        c[data[i]]--;
    }

    for (int i = 0; i < n; i++)
    {
        data[i] = r[i];
    }

}

int main()
{
    int i;
    int a[] = {3,4,1,2,2,3,5,5,6,7};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    Counting_Sort(a, ilen);

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

9、桶排序(Bucket Sort)

桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。

9.1 算法描述

  • 設置一個定量的數組當作空桶;
  • 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
  • 對每個不是空的桶進行排序;
  • 從不是空的桶裏把排好序的數據拼接起來。

第三步,對不是空的桶進行排序:可以給每個桶做個標記,有數據的置1,沒數據的置0,遍歷每個桶,只對有數據的(標記爲1)的做排序。這個排序就直接用桶排序吧(相當於每個桶1個數據)...

9.2 舉例說明

桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。爲了使桶排序更加高效,我們需要做到這兩點:

  1. 在額外空間充足的情況下,儘量增大桶的數量
  2. 使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中

同時,對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。

1. 什麼時候最快

當輸入的數據可以均勻的分配到每一個桶中。

2. 什麼時候最慢

當輸入的數據被分配到了同一個桶中。

3. 示意圖

元素分佈在桶中:

然後,元素在每個桶中排序:

如圖:我們要對29,25,3,49,9,37,21,43進行從小到打的排序,根據桶排序的定義,我們要把數組分成大小相等的幾個桶,圖中分爲5個桶,每個桶的長度均爲0~9。然後我們把數組裏面的數分配到這5個桶裏面,然後這樣我們就把數據分成了5份,然後每個桶在進行排序(可以冒泡、桶等排序),最後把桶的數據拼一起就出來了最後的結果。

4.代碼demo

def bucketSort(arr):
    maximum, minimum = max(arr), min(arr)
    bucketArr = [[] for i in range(maximum // 10 - minimum // 10 + 1)]  # set the map rule and apply for space
    for i in arr:  # map every element in array to the corresponding bucket
        index = i // 10 - minimum // 10
        bucketArr[index].append(i)
    arr.clear()
    for i in bucketArr:
        heapSort(i)   # sort the elements in every bucket
        arr.extend(i)  # move the sorted elements in bucket to array

第一個循環作用爲將待排序集合中元素移動到對應的桶中,複雜度爲 $O(N)$;第二個循環的作用爲對每個桶中元素進行排序,並移動回初始集合中,若桶個數爲 $M$,平均每個桶中元素個數爲 $\frac NM$,則複雜度爲 $O(M*\frac NMlog_2{\frac NM}+N)=O(N+N(log_2N-log_2M))$。當 $M==N$ 時,即桶排序向計數排序方式演化,則堆排序不發揮作用,複雜度爲 $O(N)$,只需要將元素移動回初始集合即可。當 $M==1$ 時,即桶排序向比較性質排序算法演化,對集合進行堆排序,並將元素移動回初始集合,複雜度爲 $O(N+Nlog_2N)$。

5.算法分析

由算法過程可知,桶排序的時間複雜度爲 $O(N+N(log_2N-log_2M))$,其中 $M$ 表示桶的個數。由於需要申請額外的空間來保存元素,並申請額外的數組來存儲每個桶,所以空間複雜度爲 $O(N+M)$。算法的穩定性取決於對桶中元素排序時選擇的排序算法。由桶排序的過程可知,當待排序集合中存在元素值相差較大時,對映射規則的選擇是一個挑戰,可能導致元素集中分佈在某一個桶中或者絕大多數桶是空桶的現象,對算法的時間複雜度或空間複雜度有較大影響,所以同計數排序一樣,桶排序適用於元素值分佈較爲集中的序列。

9.3 代碼實現

上面實現的Python版是比較複雜的桶排序。

簡單的直接就是一個數據值一個桶,類似於計數排序。

舉個例子:

期末考試完了老師要將同學們的分數按照從高到低排序。小哼的班上只有5個同學,這5個同學分別考了5分、3分、5分、2分和8分,哎考的真是慘不忍睹(滿分是10分)。接下來將分數進行從大到小排序,排序後是8 5 5 3 2。

這個題裏最小分數是0,最大分數是10。所以需要申請一個大小爲11的數組。

首先我們需要申請一個大小爲11的數組int a[11]。OK現在你已經有了11個變量,編號從a[0]~a[10]。剛開始的時候,我們將a[0]~a[10]都初始化爲0,表示這些分數還都沒有人得過。例如a[0]等於0就表示目前還沒有人得過0分,同理a[1]等於0就表示目前還沒有人得過1分……a[10]等於0就表示目前還沒有人得過10分。

a[0]~a[10]中的數值其實就是0分到10分每個分數出現的次數。接下來,我們只需要將出現過的分數打印出來就可以了,出現幾次就打印幾次,具體如下。

  a[0]爲0,表示“0”沒有出現過,不打印。

  a[1]爲0,表示“1”沒有出現過,不打印。

  a[2]爲1,表示“2”出現過1次,打印2。

  a[3]爲1,表示“3”出現過1次,打印3。

  a[4]爲0,表示“4”沒有出現過,不打印。

  a[5]爲2,表示“5”出現過2次,打印5 5。

  a[6]爲0,表示“6”沒有出現過,不打印。

  a[7]爲0,表示“7”沒有出現過,不打印。

  a[8]爲1,表示“8”出現過1次,打印8。

  a[9]爲0,表示“9”沒有出現過,不打印。

  a[10]爲0,表示“10”沒有出現過,不打印。

  最終屏幕輸出“2 3 5 5 8”,完整的代碼如下:

#include <stdio.h>
int main()
{
    int a[11],i,j,t;
    for(i=0;i<=10;i++)
        a[i]=0;  //初始化爲0
                 
    for(i=1;i<=5;i++)  //循環讀入5個數
    {
        scanf("%d",&t);  //把每一個數讀到變量t中
        a[t]++;  //進行計數
    }
    for(i=0;i<=10;i++)  //依次判斷a[0]~a[10]
        for(j=1;j<=a[i];j++)  //出現了幾次就打印幾次
            printf("%d ",i);
    getchar();getchar();
    //這裏的getchar();用來暫停程序,以便查看程序輸出的內容
    //也可以用system("pause");等來代替
    return 0;
}

這裏實現的是從小到大排序。但是我們要求是從大到小排序,這該怎麼辦呢?

其實很簡單。只需要將for(i=0;i<=10;i++)改爲for(i=10;i>=0;i--)就OK啦。

10、基數排序(Radix Sort)

基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。

10.1 算法描述

基數排序(Radix Sort)是桶排序的擴展,它的基本思想是:將整數按位數切割成不同的數字,然後按每個位數分別比較。
具體做法是:將所有待比較數值統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。

  • 取得數組中的最大數,並取得位數;
  • arr爲原始數組,從最低位開始取每個位組成radix數組;
  • 對radix進行計數排序(利用計數排序適用於小範圍數的特點);

10.2 圖片演示

10.3 舉例說明

通過基數排序對數組{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意圖如下:

在上圖中,首先將所有待比較樹脂統一爲統一位數長度,接着從最低位開始,依次進行排序。
1. 按照個位數進行排序。
2. 按照十位數進行排序。
3. 按照百位數進行排序。
排序後,數列就變成了一個有序序列。

10.4 代碼實現

/**
 * 基數排序:C++
 *
 * @author skywang
 * @date 2014/03/15
 */

#include<iostream>
using namespace std;

/*
 * 獲取數組a中最大值
 *
 * 參數說明:
 *     a -- 數組
 *     n -- 數組長度
 */
int getMax(int a[], int n)
{
    int i, max;

    max = a[0];
    for (i = 1; i < n; i++)
        if (a[i] > max)
            max = a[i];
    return max;
}

/*
 * 對數組按照"某個位數"進行排序(桶排序)
 *
 * 參數說明:
 *     a -- 數組
 *     n -- 數組長度
 *     exp -- 指數。對數組a按照該指數進行排序。
 *
 * 例如,對於數組a={50, 3, 542, 745, 2014, 154, 63, 616};
 *    (01) 當exp=1表示按照"個位"對數組a進行排序
 *    (02) 當exp=10表示按照"十位"對數組a進行排序
 *    (03) 當exp=100表示按照"百位"對數組a進行排序
 *    ...
 */
void countSort(int a[], int n, int exp)
{
    int output[n];             // 存儲"被排序數據"的臨時數組
    int i, buckets[10] = {0};

    // 將數據出現的次數存儲在buckets[]中
    for (i = 0; i < n; i++)
        buckets[ (a[i]/exp)%10 ]++;

    // 更改buckets[i]。目的是讓更改後的buckets[i]的值,是該數據在output[]中的位置。
    for (i = 1; i < 10; i++)
        buckets[i] += buckets[i - 1];

    // 將數據存儲到臨時數組output[]中
    for (i = n - 1; i >= 0; i--)
    {
        output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
        buckets[ (a[i]/exp)%10 ]--;
    }

    // 將排序好的數據賦值給a[]
    for (i = 0; i < n; i++)
        a[i] = output[i];
}

/*
 * 基數排序
 *
 * 參數說明:
 *     a -- 數組
 *     n -- 數組長度
 */
void radixSort(int a[], int n)
{
    int exp;    // 指數。當對數組按各位進行排序時,exp=1;按十位進行排序時,exp=10;...
    int max = getMax(a, n);    // 數組a中的最大值

    // 從個位開始,對數組a按"指數"進行排序
    for (exp = 1; max/exp > 0; exp *= 10)
        countSort(a, n, exp);
}

int main()
{
    int i;
    int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
    int ilen = (sizeof(a)) / (sizeof(a[0]));

    cout << "before sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    radixSort(a, ilen);    // 基數排序

    cout << "after  sort:";
    for (i=0; i<ilen; i++)
        cout << a[i] << " ";
    cout << endl;

    return 0;
}

參考:

由於時間問題,先在網上找的一些素材整理而成,有時間會自己重新畫圖整理一遍。感謝互聯網,感謝這些出處的大神!

https://www.cnblogs.com/onepixel/articles/7674659.html

https://www.cnblogs.com/skywang12345/p/3602162.html (這位博友的文章很有料...)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章