[C++]六種常見排序

目錄

希爾排序    快速排序    歸併排序    桶排序    冒泡排序    堆排序


引用:https://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F

           https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F

           https://zh.wikipedia.org/wiki/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F

           https://zh.wikipedia.org/wiki/%E6%A1%B6%E6%8E%92%E5%BA%8F

           https://zh.wikipedia.org/wiki/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F

           https://zh.wikipedia.org/wiki/%E5%A0%86%E6%8E%92%E5%BA%8F


希爾排序

希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。希爾排序是非穩定排序算法。

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
  • 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位

算法實現

原始的算法實現在最壞的情況下需要進行O(n2)的比較和交換。V. Pratt的書[1]對算法進行了少量修改,可以使得性能提升至O(nlog2 n)。這比最好的比較算法的O(n log n)要差一些。

希爾排序通過將比較的全部元素分爲幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。然後算法再取越來越小的步長進行排序,算法的最後一步就是普通的插入排序,但是到了這步,需排序的數據幾乎是已排好的了(此時插入排序較快)。

假設有一個很小的數據在一個已按升序排好序的數組的末端。如果用複雜度爲O(n2)的排序(冒泡排序插入排序),可能會進行n次的比較和交換才能將該數據移至正確位置。而希爾排序會用較大的步長移動數據,所以小數據只需進行少數比較和交換即可到正確位置。

一個更好理解的希爾排序實現:將數組列在一個表中並對列排序(用插入排序)。重複這過程,不過每次用更長的列來進行。最後整個表就只有一列了。將數組轉換至表是爲了更好地理解這算法,算法本身僅僅對原數組進行排序(通過增加索引的步長,例如是用i += step_size而不是i++)。

例如,假設有這樣一組數[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我們以步長爲5開始進行排序,我們可以通過將這列表放在有5列的表中來更好地描述算法,這樣他們就應該看起來是這樣:

13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10

然後我們對每列進行排序:

10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45

將上述四行數字,依序接在一起時我們得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].這時10已經移至正確位置了,然後再以3爲步長進行排序:

10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45

排序之後變爲:

10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94

最後以1步長進行排序(此時就是簡單的插入排序了)。

步長序列

步長的選擇是希爾排序的重要部分。只要最終步長爲1任何步長序列都可以工作。算法最開始以一定的步長進行排序。然後會繼續以一定步長進行排序,最終算法以步長爲1進行排序。當步長爲1時,算法變爲普通插入排序,這就保證了數據一定會被排序。

Donald Shell最初建議步長選擇爲\frac{n}{2}並且對步長取半直到步長達到1。雖然這樣取可以比{\mathcal {O}}(n^{2})類的算法(插入排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。可能希爾排序最重要的地方在於當用較小步長排序後,以前用的較大步長仍然是有序的。比如,如果一個數列以步長5進行了排序然後再以步長3進行排序,那麼該數列不僅是以步長3有序,而且是以步長5有序。如果不是這樣,那麼算法在迭代過程中會打亂以前的順序,那就不會以如此短的時間完成排序了。

步長序列 最壞情況下複雜度
{n/2^i} \mathcal{O}(n^2)
2^k - 1 \mathcal{O}(n^{3/2})
2^i 3^j \mathcal{O}( n\log^2 n )

已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),該序列的項來自9\times 4^{i}-9\times 2^{i}+12^{{i+2}}\times (2^{{i+2}}-3)+1這兩個算式[1]。這項研究也表明“比較在希爾排序中是最主要的操作,而不是交換。”用這樣步長序列的希爾排序比插入排序要快,甚至在小數組中比快速排序堆排序還快,但是在涉及大量數據時希爾排序還是比快速排序慢。

另一個在大數組中表現優異的步長序列是(斐波那契數列除去0和1將剩餘的數以黃金分割比的兩倍的進行運算得到的數列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713,…)[2]

程序代碼

template <typename _Tp>
void shell_sort(_Tp *arr, int length) {
    int h = 1;
    while (h < length / 3) {
        h = 3 * h + 1;
    }
    while (h >= 1) {
        for (int i = h; i < length; ++i) {
            for (int j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
                swap(arr[j], arr[j - h]);
            };
        };
        h = h / 3;
    };
};

快速排序

快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最早由東尼·霍爾提出。在平均狀況下,排序n個項目要{\displaystyle \ O(n\log n)}大O符號)次比較。在最壞狀況下則需要{\displaystyle O(n^{2})}次比較,但這種狀況並不常見。事實上,快速排序{\displaystyle \Theta (n\log n)}通常明顯比其他算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地達成。

算法實現

快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分爲兩個子序列(sub-lists)。

步驟爲:

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

遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法一定會結束,因爲在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。

正規分析

從一開始快速排序平均需要花費{\displaystyle O(n\log n)}時間的描述並不明顯。但是不難觀察到的是分割運算,數組的元素都會在每次循環中走訪過一次,使用{\displaystyle O(n)}的時間。在使用結合(concatenation)的版本中,這項運算也是{\displaystyle O(n)}

在最好的情況,每次我們運行一次分割,我們會把一個數列分爲兩個幾近相等的片段。這個意思就是每次遞歸調用處理一半大小的數列。因此,在到達大小爲一的數列前,我們只要作\log n次嵌套的調用。這個意思就是調用樹的深度是{\displaystyle O(\log n)}。但是在同一層次結構的兩個程序調用中,不會處理到原來數列的相同部分;因此,程序調用的每一層次結構總共全部僅需要{\displaystyle O(n)}的時間(每個調用有某些共同的額外耗費,但是因爲在每一層次結構僅僅只有{\displaystyle O(n)}個調用,這些被歸納在{\displaystyle O(n)}係數中)。結果是這個算法僅需使用{\displaystyle O(n\log n)}時間。

另外一個方法是爲{\displaystyle T(n)}設立一個遞歸關係式,也就是需要排序大小爲n的數列所需要的時間。在最好的情況下,因爲一個單獨的快速排序調用牽涉了{\displaystyle O(n)}的工作,加上對{\displaystyle n/2}大小之數列的兩個遞歸調用,這個關係式可以是:

{\displaystyle T(n)=O(n)+2T(n/2)}

解決這種關係式類型的標準數學歸納法技巧告訴我們{\displaystyle T(n)=O(n\log n)}

事實上,並不需要把數列如此精確地分割;即使如果每個基準值將元素分開爲99%在一邊和1%在另一邊,調用的深度仍然限制在{\displaystyle 100\log n},所以全部運行時間依然是{\displaystyle O(n\log n)}

然而,在最壞的情況是,兩子數列擁有大各爲1 和{\displaystyle n-1},且調用樹(call tree)變成爲一個n個嵌套(nested)調用的線性連串(chain)。第i 次調用作了{\displaystyle O(n-i)}的工作量,且\sum _{i=0}^{n}(n-i)=O(n^{2})遞歸關係式爲:

{\displaystyle T(n)=O(n)+T(1)+T(n-1)=O(n)+T(n-1)}

這與插入排序選擇排序有相同的關係式,以及它被解爲{\displaystyle T(n)=O(n^{2})}。 

程序代碼

template <typename value_type> void quick_sort(const int left, const int right, value_type *area) {
	int i = left, j = right; value_type pivot = area[(left + right) >> 1];
	while (i <= j) {
		while (area[j] > pivot) --j;
		while (area[i] < pivot) ++i;
		if (i <= j)
			value_swap(area[i++], area[j--]);
	}
	if (i < right) quick_sort(i, right, area);
	if (j > left) quick_sort(left, j, area);
}
template <typename value_type> void value_swap(value_type &element_A, value_type &element_B) {
	value_type element_C;
	element_C = element_A;
	element_A = element_B;
	element_B = element_C;
}

歸併排序

歸併排序(英語:Merge sort,或mergesort),是創建在歸併操作上的一種有效的排序算法效率{\displaystyle O(n\log n)}大O符號)。1945年由約翰·馮·諾伊曼首次提出。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞歸可以同時進行。

歸併操作

歸併操作(merge),也叫歸併算法,指的是將兩個已經排序的序列合併成一個序列的操作。歸併排序算法依賴歸併操作。

遞歸法(Top-down)

  1. 申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
  4. 重複步驟3直到某一指針到達序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

迭代法(Bottom-up)

原理如下(假設序列共有n個元素):

  1. 將序列每相鄰兩個數字進行歸併操作,形成{\displaystyle ceil(n/2)}個序列,排序後每個序列包含兩/一個元素
  2. 若此時序列數不是1個則將上述序列再次歸併,形成{\displaystyle ceil(n/4)}個序列,每個序列包含四/三個元素
  3. 重複步驟2,直到所有元素排序完畢,即序列數爲1

程序代碼 

template <typename _Tp>
void merge_sort(int start, int end, _Tp *arr) {
	if (start == end) return;
	int mid = (start + end) / 2;
	merge_sort(start, mid, arr);
	merge_sort(mid + 1, end, arr);
	int i = start, j = mid + 1, k = start;
	while (i <= mid && j <= end) if (arr[i] <= arr[j]) rad[k] = arr[i], ++k, ++i; else rad[k] = arr[j], ++k, ++j;
	while (i <= mid) rad[k] = arr[i], ++k, ++i;
	while (j <= end) rad[k] = arr[j], ++k, ++j;
	for (int i = start; i <= end; ++i) arr[i] = rad[i];
};

桶排序

桶排序(Bucket sort)或所謂的箱排序,是一個排序算法,工作的原理是將數組分到有限數量的桶裏。每個桶再個別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間({\displaystyle \Theta (n)}大O符號))。但桶排序並不是比較排序,他不受到{\displaystyle O(n\log n)}下限的影響。

桶排序以下列程序進行:

  1. 設置一個定量的數組當作空桶子。
  2. 尋訪序列,並且把項目一個一個放到對應的桶子去。
  3. 對每個不是空的桶子進行排序。
  4. 從不是空的桶子裏把項目再放回原來的序列中。

程序代碼 

假設數據範圍分佈在[0, 100)之間

桶排序簡單實現方法:

for (int i = 0; i < n; ++i) {
	cin >> x; ++b[x];	
};
for (int i = 0; i < 100; ++i)
	while (b[i] > 0) {
		cout << i << ' '; --b[i];	
	};

桶排序STL實現,每個桶內部用鏈表表示,在數據入桶的同時插入排序。然後把各個桶中的數據合併:

#include <iterator>
#include <iostream>
#include <vector>
using namespace std;
const int BUCKET_NUM = 100;
struct ListNode {
	explicit ListNode(int i = 0) : mData(i), mNext(NULL) {}
	ListNode *mNext;
	int mData;
};
ListNode *insert(ListNode* head, int val) {
	ListNode dummyNode;
	ListNode *newNode = new ListNode(val);
	ListNode *pre, *curr;
	dummyNode.mNext = head;
	pre = &dummyNode;
	curr = head;
	while(NULL != curr && curr->mData <= val) {
		pre = curr;
		curr = curr->mNext;
	};
	newNode->mNext = curr;
	pre->mNext = newNode;
	return dummyNode.mNext;
};
ListNode *Merge(ListNode *head1, ListNode *head2) {
	ListNode dummyNode;
	ListNode *dummy = &dummyNode;
	while (NULL != head1 && NULL != head2) {
		if (head1->mData <= head2->mData) {
			dummy->mNext = head1;
			head1 = head1->mNext;
		} else {
			dummy->mNext = head2;
			head2 = head2->mNext;
		};
		dummy = dummy->mNext;
	};
	if (NULL != head1) dummy->mNext = head1;
	if (NULL != head2) dummy->mNext = head2;	
	return dummyNode.mNext;
};
void BucketSort(int n, int *arr) {
	vector <ListNode*> buckets(BUCKET_NUM, (ListNode*)(0));
	for (int i = 0; i < n; ++i) {
		int index = arr[i] / BUCKET_NUM;
		ListNode *head = buckets.at(index);
		buckets.at(index) = insert(head, arr[i]);
	};
	ListNode *head = buckets.at(0);
	for (int i = 1; i < BUCKET_NUM; ++i) {
		head = Merge(head, buckets.at(i));
	};
	for (int i = 0; i < n; ++i) {
		arr[i] = head->mData;
		head = head->mNext;
	};
};

冒泡排序

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

冒泡排序對n個項目需要O(n^{2})的比較次數,且可以原地排序。儘管這個算法是最簡單瞭解和實現的排序算法之一,但它對於包含大量的元素的數列排序是很沒有效率的。

冒泡排序是與插入排序擁有相等的運行時間,但是兩種算法在需要的交換次數卻很大地不同。在最壞的情況,冒泡排序需要O(n^{2})次交換,而插入排序只要最多O(n)交換。冒泡排序的實現(類似下面)通常會對已經排序好的數列拙劣地運行(O(n^{2})),而插入排序在這個例子只需要O(n)個運算。因此很多現代的算法教科書避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在內部循環第一次運行時,使用一個旗標來表示有無需要交換的可能,也可以把最優情況下的複雜度降低到O(n)。在這個情況,已經排序好的數列就無交換的需要。若在每次走訪數列時,把走訪順序反過來,也可以稍微地改進效率。有時候稱爲雞尾酒排序,因爲算法會從數列的一端到另一端之間穿梭往返。

冒泡排序算法的運作如下:

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
  3. 針對所有的元素重複以上的步驟,除了最後一個。
  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

由於它的簡潔,冒泡排序通常被用來對於程序設計入門的學生介紹算法的概念。

冒泡排序

Bubble sort animation.gif

使用冒泡排序爲一列數字進行排序的過程

分類 排序算法 數據結構 數組 最壞時間複雜度 O(n^{2}) 最優時間複雜度 O(n) 平均時間複雜度 O(n^{2}) 最壞空間複雜度 總共O(n),需要輔助空間O(1)

程序代碼

template <typename _Tp>
void bubble_sort(int start, int end, _Tp *arr) {
    for (int i = start; i < end - 1; ++i)
        for (int j = start; j < end - 1 - i; ++j)
            if (arr[j] > arr[j + 1])
                swap(arr[j], arr[j + 1]);
};

堆排序

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

概述

若以升序排序說明,把數組轉換成最大堆積(Max-Heap Heap),這是一種滿足最大堆積性質(Max-Heap Property)的二叉樹:對於除了根之外的每個節點i, A[parent(i)] ≥ A[i]。

重複從最大堆積取出數值最大的結點(把根結點和最後一個結點交換,把交換後的最後一個結點移出堆),並讓殘餘的堆積維持最大堆積性質。

堆節點的訪問

通常堆是通過一維數組來實現的。在數組起始位置爲0的情形中:

  • 父節點i的左子節點在位置{\displaystyle (2i+1)};
  • 父節點i的右子節點在位置{\displaystyle (2i+2)};
  • 子節點i的父節點在位置{\displaystyle floor((i-1)/2)};

堆的操作

在堆的數據結構中,堆中的最大值總是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。堆中定義以下幾種操作:

  • 最大堆調整(Max Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
  • 創建最大堆(Build Max Heap):將堆中的所有數據重新排序
  • 堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算

 程序代碼

void max_heapify(int arr[], int start, int end) {
    int dad = start;
    int son = dad * 2 + 1;
    while (son <= end) {
        if (son + 1 <= end && arr[son] < arr[son + 1])
            son++;
        if (arr[dad] > arr[son])
            return;
        else {
            swap(arr[dad], arr[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len) {
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);
    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}

 

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