幾種經典的排序演算法

ZHUO_SIR發表於2018-06-09

綜述

最近複習了各種排序演算法,記錄了一下學習總結和心得,希望對大家能有所幫助。本文介紹了氣泡排序、插入排序、選擇排序、快速排序、歸併排序、堆排序、計數排序、桶排序、基數排序9種經典的排序演算法。針對每種排序演算法分析了演算法的主要思路,每個演算法都附上了虛擬碼和C++實現。

電梯直達

1. 氣泡排序 
2. 插入排序 
3 .選擇排序 
4. 快速排序 
5. 歸併排序 
6. 堆排序 
7. 計數排序 
8. 桶排序 
9. 基數排序

演算法分類

原地排序(in-place):沒有使用輔助資料結構來儲存中間結果的排序**演算法。 
非原地排序(not-in-place / out-of-place):使用了輔助資料結構來儲存中間結果的排序演算法 
穩定排序:數列值(key)相等的元素排序後相對順序維持不變 
不穩定排序:不屬於穩定排序的排序演算法 

演算法複雜度

這裡寫圖片描述 
演算法複雜度參考了Big-O Cheat Sheet

1. 氣泡排序(Bubble Sort)

思路

不斷地遍歷數列,比較相鄰元素,每次把無序部分最大的元素放到最後,遍歷n-1次後,數列就是有序的了。

虛擬碼

BUBBLE_SORT(A, n)
    for( i from 0 to n-2) //遍歷n-1次
        for(j from 0 to n-2-i) //比較無序部分的所有相鄰元素
            if(A[j] > A[j+1]) //如果前面的元素大,放到後面去
                swap(A[i],A[j+1])
                swapped = true
        if(not swapped) //如果以第j個數為起點遍歷,沒有發生交換,說明後面已經有序了
            break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最好情況

輸入數列有序,第一次遍歷結束就會完成排序,時間複雜度最好為Ω(n)

C++實現

void bubbleSort(vector<int> &arr)
{
    for(int i = 0; i < arr.size() - 1; i++)
    {
        bool swapped = false;
        for(int j = 0; j < arr.size() - 1 - i; j++)
        {
            if(arr[j] > arr[j + 1])
            {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        if(!swapped)
        {
            break;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

2. 插入排序(Insertion Sort)

思路

把數列分為有序和無序部分,每次從無序部分拿出第一個元素,然後從後向前掃描有序部分,找到相應位置並插入,具體來說就是對於比當前元素大的元素,往後移動一位。直到找到比當前元素小的,在該元素後面插入當前元素

虛擬碼

INSERTION_SORT(A,n)
    for(i from 1 to n-1) //從1開始遍歷無序陣列
        temp = A[i] //取出當前元素
            j = i-1 
                while(j >= 0 and temp < A[j]) //比temp大的元素後移
            A[j+1] = A[j]
            j -= 1
        arr[j+1] = temp;  //temp 放入第0個或者第一個不比temp大的元素
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

最好情況

輸入數列有序,每次插入都是直接插在了有序部分的後面,時間複雜度最好為Ω(n)

C++實現

void insertionSort(vector<int> &arr)
{
    for(int i=1; i<arr.size(); i++)
    {
        int temp = arr[i];
        int j = i -1;
        while(j >=0 && temp < arr[j])
        {
            arr[j+1] = arr[j];
            j--;
        }
        arr[j+1] = temp;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3. 選擇排序(Selection Sort)

思路

數列分為有序部分和無序部分,重複下列過程n次:找到無序部分中最小的數,放到有序部分的最後面(即和無序部分的第一個置換)

虛擬碼

SELECTION_SORT(A, n)
    for(i from 0 to n-2) //i指向無序部分的開頭,n-2為倒數第二個元素的索引
        for(j from i to n-1) // 找到無序部分最小的元素
            minLoc = findMin()
        swap(A[i],A[minLoc]) //最小的元素置換到i位置上(加入了有序部分)
  • 1
  • 2
  • 3
  • 4
  • 5

C++實現

void selectionSort(vector<int> &arr)
{
    for(int i=0; i<arr.size()-1; i++)
    {
        int min = INT_MAX;
        int minLoc = -1;
        for(int j=i; j<arr.size(); j++)
        {
            if(arr[j] < min)
            {
                min = arr[j];
                minLoc = j;
            }
        }
        arr[minLoc] = arr[i];
        arr[i] = min;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

4. 快速排序 (Quick Sort)

思路

使用分治演算法,每次以pivot為基準將數列分成兩部分,左邊的都小於等於基準,右邊的都大於基準,然後分別遞迴地對左右兩部分進行快速排序(終止條件是元素個數為1個或者0個)。演算法的核心在於分割槽(把數列分成兩部分) 
分割槽時,從數列中選擇一個元素作為pivot(一般選最後一個,翻譯為“基準”或者“哨兵”),使用兩個指標,第一個指標始終指向左邊部分的結尾(初始位置為-1),第二個指標用於遍歷數列(初始位置為0),發現小於等於pivot的就和右部分第一個數字互換(相當於把數加入了左邊部分),比pivot大的數就跳過(相當於把數加入了右邊部分)

虛擬碼

QUICK_SORT(A,head,tail) //輸入數列A,[head, tail),不包含tail
    if(tail - head > 1) //元素個數低於1個,有序,停止遞迴
        pivot = PARTITION(A,head,tail) //分割槽,獲得pivot索引
        QUICK_SORT(A,head,pivot)//遞迴
        QUICK_SORT(A,pivot+1,tail)//遞迴,pivot已經在正確的位置上了,不參與後續排序

PARTITION(A,head,tail) //分割槽,[head,tail)
    i = head - 1 //i初始化為head-1,代表著左半邊現在沒有元素
    pivot = A[tail-1] //選擇最後一個元素作為pivot
    for(j from head to tail-2) //遍歷全部元素(除了最後一個 tail-1)
        if(A[j] <= pivot)//發現小於等於pivot的元素,置換(大於的話,j就直接後移了)
            i += 1 //i此時指向了大於pivot的區的第一個元素
            swap(A[i],A[j])
    swap(A[i+1],A[tail-1]) //最後把pivot放到中間位置
    return i+1 //返回pivot
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

最壞情況

  • 輸入的數列是有序數列,這樣每次分割槽選到的pivot都是當前最大值,每次分割槽的結果都是左邊n-1個數,右邊0個數,需要進行n-1分割槽,遞迴深度為n,時間複雜度為O(n^2)
  • 輸入的數列是逆序數列,與上面的情況類似,時間複雜也也為O(n^2)

最好情況

每次分割槽的結果都是均勻的分成了左右兩部分,那麼時間複雜度就是Θ(n log(n))

C++實現

void quickSort(vector<int> &arr)
{
    quickSort(arr, 0, arr.size());
}

void quickSort(vector<int> &arr, int head, int tail)
{
    if(tail - head > 1)
    {
        int pivot = partition(arr, head, tail);
        quickSort(arr, head, pivot);
        quickSort(arr, pivot+1, tail);
    }
}


int partition(vector<int> &arr, int head, int tail)
{
    int i = head - 1;
    int pivot = arr[tail - 1];
    for(int j = head; j < tail - 1; j++)
    {
        //這裡不能用<,陣列為[3,3]這樣時,i沒有移動過,一直為-1,quickSort在右半部分永久進行遞迴[0,2)
        if(arr[j] <= pivot)
        {
            i++;
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    arr[tail - 1] = arr[i + 1];
    arr[i + 1] = pivot;
    return i + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

5. 歸併排序(Merge Sort)

思路

使用分治演算法,把數列均勻地分成左右兩部分,分別進行歸併排序(遞迴終止條件為只有一個或者0個元素),然後將左右兩個有序數列合併到一起。

虛擬碼

MERGE_SORT(A,head,tail)
    if(tail - head < 2) //元素個數小於2個就停止了
        return
    mid = (head + tail)/2 
    MERGE_SORT(A,head,mid) //左邊歸併排序
    MERGE_SORT(A,mid,tail) //右邊歸併排序
    copy A[head,mid) to B //複製A的左半部分到B,B有序
    copy A[mid,tail) to C //複製A的右半部分到C,C有序
    merge B,C to A //合併B和C兩個有序數列,將結果放在A中
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

C++實現

void mergeSort(vector<int> &arr)
{
    mergeSort(arr, 0, arr.size());
}

void mergeSort(vector<int> &arr, int head, int tail)
{
    if(tail - head < 2)
    {
        return;
    }
    int mid = (head + tail) / 2;
    mergeSort(arr, head, mid);
    mergeSort(arr, mid, tail);
    vector<int> left(arr.begin() + head, arr.begin() + mid);
    vector<int> right(arr.begin() + mid, arr.begin() + tail);
    int i = 0, j = 0, k = head;
    while(i < left.size() && j < right.size() && k < tail) //這裡k判斷條件是小於tail,不是arr.size()!!
    {
        if(left[i] < right[j])
        {
            arr[k++] = left[i++];
        } else
        {
            arr[k++] = right[j++];
        }
    }
    if(i == left.size())
    {
        while(j < right.size() && k < tail)
        {
            arr[k++] = right[j++];
        }
    } else if(j == right.size())
    {
        while(i < left.size() && k < tail)
        {
            arr[k++] = left[i++];
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

6. 堆排序(Heap Sort)

思路

構建大頂堆(降序構建小頂堆),然後交換root節點和最後一個節點,此時將堆的大小減1,並利用堆化演算法把堆重新調整為大頂堆,重複上述過程,直到大小為1,堆排序完成,因為每次都是把堆當前最大的數放到堆後面,所以數列最終變成有序了。 
堆化演算法針對root的左右孩子均為大頂堆,但是root自己可能比左右孩子小的情況,演算法比較root和左右孩子,選擇最大的和root進行置換(如果root最大就不用置換了),置換後以被置換的孩子為root繼續執行堆化演算法,直到當前root比左右孩子大了或者已經是葉子節點了。 
這是一個大頂堆

虛擬碼

//堆化演算法,左右孩子均為大頂堆,root可能比左右孩子小,違反大頂堆性質
MAX_HEAPIFY(A,i) //i是當前root的索引
    left = i*2+1
    right = i*2 + 2
    //找到左、右孩子,root中的最大值
    max = i
    if(left < heapSize and A[left] > A[i])
        max = left;
    if(right <headpSize and A[right] > A[max])
        max = right
    if(max != i) //最大值不是root節點,交換之,並繼續堆化被破壞的子堆
        swap(A[i],A[max])
        MAX_HEAPIFY(A,max)

//自底向上構建大頂堆
BUILD_MAX_HEAP(A,n)
    //從最後一個父節點開始(n-1)為最後一個元素的索引,自底向上執行堆化演算法
    for(i from ((n-1)-1)/2 to 0)
        MAX_HEAPIFY(A,i)

///堆排序演算法,不斷把root置換到堆的後面,heapSize減一併執行堆化演算法
HEAP_SORT(A,n)
    BUILD_MAX_HEAP(A,n)
    for(i from n-1 to 1)
        swap(A[i],A[0])
        heapSize -= 1
        MAX_HEAPIFY(A,0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

C++實現

void heapSort(vector<int> &arr)
{
    buildMaxHeap(arr);
    int heapSize = arr.size();
    for(int i = arr.size() - 1; i > 0; i--)
    {
        int temp = arr[i];
        arr[i] = arr[0];
        arr[0] = temp;
        heapSize--;
        maxHeapify(arr,heapSize,0);
    }
}

void buildMaxHeap(vector<int> &arr)
{
    for(int i = ((arr.size() - 1) - 1) / 2; i >= 0; i--)
    {
        maxHeapify(arr,arr.size(),i);
    }
}

void maxHeapify(vector<int> &arr, int heapSize, int root)
{
    int left = root * 2 + 1;
    int right = root * 2 + 2;
    int max = root;
    if(left < heapSize && arr[left] > arr[max])
    {
        max = left;
    }
    if(right < heapSize && arr[right] > arr[max])
    {
        max = right;
    }
    if(max != root)
    {
        int temp = arr[max];
        arr[max] = arr[root];
        arr[root] = temp;
        maxHeapify(arr,heapSize,max);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

7. 計數排序(Counting Sort)

思路

計數排序是一種非比較排序,對數列中每個元素X,通過統計數列中小於等於X的元素個數來計算X所處的位置進行排序。使用陣列統計元素個數, counts[i]記錄的是小於等於 i 的元素個數。

虛擬碼

COUNTING_SORT(A,n)
    for(i from 0 to n-1) //計數
        counts[A[i]]++
    for(i from 1 to n-1) //累加,以便進行反向填充
        counts[i] += counts[i-1] 
    for(i from n-1 to 0) //反向填充是為了保證排序是穩定的
        B[counts[A[i]]-1] = A[i]
        counts[A[i]] -= 1
    A = B
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

C++實現:使用陣列計數

void countingSort(vector<int> &arr)
{
    int max = 0;
    for(int i = 0; i < arr.size(); i++)
    {
        if(arr[i] > max)
        {
            max = arr[i];
        }
    }
    vector<int> counts(max + 1, 0);
    for(int i = 0; i < arr.size(); i++)
    {
        counts[arr[i]]++;
    }
    for(int i = 1; i <= max; i++)
    {
        counts[i] += counts[i - 1];
    }
    vector<int> tempArr(arr.size(), -1);
    for(int i = arr.size() - 1; i >= 0; i--)
    {
        tempArr[counts[arr[i]] - 1] = arr[i];
        counts[arr[i]]--;
    }
    arr = move(tempArr);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

8. 桶排序(Bucket Sort)

思路

把數列中元素的值範圍分割成多個長度相等的區間,稱為桶,把元素按值所在區間分別放到不同的桶中。然後桶內分別進行排序(比如使用插入排序),最終獲得有序數列

虛擬碼

#下面的假設值範圍為0~999,桶數目BNUM為100
INDEX_OF(j)
    return j/10 //這裡根據實際的資料情況和執行環境可以調整桶的分配方式
BUCKET_SORT(A,n)
    list B //桶們
    for(i from 0 to n-1) //分桶
        insert A[i] to B[INDEX_OF[A[i]]]
    for(i from 0 to BUMN-1) 
        INSERTION_SORT(B[i]) //分別桶內排序
    A = B[0] + B[1] + ... + B[BNUM-1] //按序連線各個桶
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

C++實現

#define BUCKETS_NUM 100
int indexOf(int num)
{
    return num/10;
}

void bucketSort(vector<int> &arr)
{
    vector<vector<int>> buckets(BUCKETS_NUM);
    for(int i=0; i<arr.size(); i++)
    {
        buckets[indexOf(arr[i])].push_back(arr[i]);
    }
    for(int i=0; i<BUCKETS_NUM; i++)
    {
        insertionSort(buckets[i]);
    }

    int k =0;
    for(int i=0; i<BUCKETS_NUM; i++)
    {
        for(int j=0; j<buckets[i].size(); j++)
        {
            arr[k++] = buckets[i][j];
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

9. 基數排序(Radix Sort)

思路

把元素按照位置分割成不同的數字,從最低位部分開始,一直到最高位部分,分別對每部分進行入桶操作,入桶時桶內元素的相對順序不變,然後將桶按順序連線起來,進行下一部分的入桶排序。對整數來說,位數較短的前面補0,下面敘述假定數列元素都是非負整數

為什麼多次入桶後數列就有序了

因為進行高位入桶時是按序入桶的,所以高位相同的數字,低位的順序仍然保留下來了。只有高位不同的數字,低位的順序才會被打亂,高位不同肯定是按照高位的順序排的,所以打亂沒有影響。 
下面舉個例子說明一下。 
假設待排序數列為 01, 88, 13, 78, 56, 79, 07 , 28, 76 這裡為了方便理解,位數不夠的前面已經補0了。 
第一次按照最低位(個位數)入桶

桶編號0123456789
- 01 13  56077879
-      76 88 
-        28 

將桶按順序連線起來,形成新的數列01, 13, 56, 76, 07, 78, 88, 28, 79

第二次按照次最低位(十位數)入桶

桶編號0123456789
 011328  56 7688 
 07      78  
        79  

將桶按順序連線起來,得到新的數列01, 07, 13, 28, 56, 76, 78, 79, 88,排序完成

為什麼要從最低位開始

以非負整數為例,每次從最低位開始的話,每次入桶都有10個桶。如果從最高位開始的話,第一次10個桶,第二次如果還是10個桶,那麼如果低位不同,高位的相對順序就會被打亂,這顯然是錯誤的。那麼為了保證高位的順序不被打亂。就必須要在高位的桶內進行排序,即每個桶裡面要再分10個桶。第二位總共需要10*10=100個桶。以此類推n位就需要10^個桶,雖然也可以實現,但是開銷太大,所以不從最高位開始。

虛擬碼

//以對非負整數進行基數排序為例
RADIX_SORT(A,n)
    for(i from d-1 to 0) //d是數字的位數,進行d次入桶(排序)
        for(j from 0 to n-1) 
            put A[j] into B[D[A[j]]] //D[x]是x第i位的值。
        A = B[0] + B[1] + ... + B[9] //按序連線桶
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

C++實現

void radixSort(vector<int> &arr)
{
    vector<vector<int>> buckets(10);
    int radix = 1;
    for(int i=0; i<10; i++) //INT_MAX為10位數,所以最多進行10次入桶
    {
        for(int j = 0; j < arr.size(); j++)
        {
            buckets[(arr[j] / radix) % 10].push_back(arr[j]);
        }
        int k = 0;
        for(int i = 0; i < 10; i++)
        {
            for(int j = 0; j < buckets[i].size(); j++)
            {
                arr[k++] = buckets[i][j];
            }
            if(buckets[i].size() == arr.size())//全部在一個桶裡了,提前結束
            {
                return;
            }
            buckets[i].clear();
        }
        radix*=10;
    }
}

相關文章