[置頂] 常見的排序演算法
描述:
排序演算法可謂資料結構模組中的重中之重,常見的雜湊表,二叉樹,搜尋樹/平衡樹,點陣圖等資料結構只是處理實際問題的抽象方法,實際在處理接受或生成的資料集時,排序演算法顯得尤其重要,排序演算法家族很龐大,其中包括了氣泡排序,選擇排序,插入排序,堆排序,快速排序,歸併排序,基數排序,計數排序,希爾排序,箱排序,樹型排序等眾多演算法,每種排序都有各自的特性,沒有好壞之分,只有在特定的場景使用合適的排序演算法才是上策,單純的來比顯得太過絕對,沒有可比性。因為實際需求及各方面條件的限制使得排序演算法的可選範圍往往只縮小到某一種或某幾種,所以要具體問題具體對待。
常見的排序演算法在此列舉氣泡排序,選擇排序,快速排序,插入排序,堆排序,歸併排序,基數排序,計數排序演算法。
一:氣泡排序
★演算法描述:
氣泡排序(Bubble Sort,臺灣譯為:泡沫排序或氣泡排序)是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
★演算法步驟:
1、比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2、對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
3、針對所有的元素重複以上的步驟,除了最後一個。
4、持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
[cpp] view plain copy print?
- #include<iostream>
- usingnamespace std;
- void Bubble_Sort(int* arr, size_t size)
- {
- for (size_t i = 0; i < size; i++)
- {
- int temp = 0;
- for (size_t j = size - 1; j > 0; j--)
- {
- if (arr[j] < arr[j - 1])
- {
- temp = arr[j];
- arr[j] = arr[j - 1];
- arr[j - 1] = temp;
- }
- }
- }
- }
#include<iostream>
using namespace std;
void Bubble_Sort(int* arr, size_t size)
{
for (size_t i = 0; i < size; i++)
{
int temp = 0;
for (size_t j = size - 1; j > 0; j--)
{
if (arr[j] < arr[j - 1])
{
temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
}
▲注:該排序演算法有很大的優化空間,因為每一趟排序都使有序區增加了一個氣泡,在經過n-1趟排序之後,有序區中就有n-1個氣泡,而無序區中氣泡的重量總是大於等於有序區中氣泡的重量,所以整個氣泡排序過程至多需要進行n-1趟排序。以此本演算法的時間複雜度還是O(n*n),也不能算是一個高效的演算法。細心分析不難發現,若在某一趟排序中未發現氣泡位置的交換,則說明待排序的無序區中所有氣泡均滿足輕者在上,重者在下的原則,因此,氣泡排序過程可在此趟排序後終止。也就是說按照升序或降序排序時,若該序列本身就是一個接近有序的數列,則在某兩個資料項未發生交換時退出,說明此時序列已經有序,不必再依次往後“冒泡”,提高了效率。
★氣泡排序:
二:快速排序
★演算法描述:
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序 n 個專案要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來,且在大部分真實世界的資料,可以決定設計的選擇,減少所需時間的二次方項之可能性。
1、從數列中挑出一個元素,稱為 “基準”(pivot)。
2、重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
3、遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
- #include<iostream>
- usingnamespace std;
- #define SWAP(x,y){int tmp;tmp = x;x = y;y = tmp;}
- int partition(int arr[], int left, int right)
- {
- int i, j, k;
- k = arr[right];
- i = left - 1;
- for (j = left; j < right; j++)
- {
- if (arr[j] <= k)
- {
- i++;
- SWAP(arr[i], arr[j]);
- }
- }
- SWAP(arr[i + 1], arr[right]);
- return i + 1;
- }
- void Quick_Sort(int arr[], int left, int right)
- {
- int q;
- if (left < right)
- {
- q = partition(arr, left, right);
- Quick_Sort(arr, left, q - 1);
- Quick_Sort(arr, q + 1, right);
- }
- }
#include<iostream>
using namespace std;
#define SWAP(x,y){int tmp;tmp = x;x = y;y = tmp;}
int partition(int arr[], int left, int right)
{
int i, j, k;
k = arr[right];
i = left - 1;
for (j = left; j < right; j++)
{
if (arr[j] <= k)
{
i++;
SWAP(arr[i], arr[j]);
}
}
SWAP(arr[i + 1], arr[right]);
return i + 1;
}
void Quick_Sort(int arr[], int left, int right)
{
int q;
if (left < right)
{
q = partition(arr, left, right);
Quick_Sort(arr, left, q - 1);
Quick_Sort(arr, q + 1, right);
}
}
▲注:對於快排演算法來說,其也有改進優化的空間,比如在選取第一個元素作為主元時,該主元的如何選取。對於快速排序演算法的改進主要集中在三個方面:① 選取一個更好的中軸值;② 根據產生的子分割槽大小調整演算法;③不同的劃分分割槽的方法。
★快速排序:
三:插入排序
★演算法描述:
插入排序(Insertion Sort)的演算法描述是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
★演算法步驟:
1、從第一個元素開始,該元素可以認為已經被排序;
2、取出下一個元素,在已經排序的元素序列中從後向前掃描;
3、如果該元素(已排序)大於新元素,將該元素移到下一位置;
4、重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
5、將新元素插入到該位置中;
6、重複步驟2;
[cpp] view plain copy print?- #include<iostream>
- usingnamespace std;
- void Insertsort(int *arr, int size)
- {
- assert(arr);
- for (int i = 1; i < size; i++)
- {
- int index = i;
- int temp = arr[index];
- int end = index - 1;
- while (end >= 0 && temp<arr[end])
- {
- arr[end + 1] = arr[end];
- end--;
- }
- arr[end + 1] = temp;
- }
- }
#include<iostream>
using namespace std;
void Insertsort(int *arr, int size)
{
assert(arr);
for (int i = 1; i < size; i++)
{
int index = i;
int temp = arr[index];
int end = index - 1;
while (end >= 0 && temp<arr[end])
{
arr[end + 1] = arr[end];
end--;
}
arr[end + 1] = temp;
}
}
▲注:插入排序同樣也有改進優化的空間,折半插入排序(binary insertion sort)是對插入排序演算法的一種改進,由於排序演算法過程中,就是不斷的依次將元素插入前面已排好序的序列中。由於前半部分為已排好序的數列,這樣我們不用按順序依次尋找插入點,可以採用折半查詢的方法來加快尋找插入點的速度。折半插入排序演算法是一種穩定的排序演算法,比直接插入演算法明顯減少了關鍵字之間比較的次數,因此速度比直接插入排序演算法快,但記錄移動的次數沒有變,所以折半插入排序演算法的時間複雜度仍然為O(n^2),與直接插入排序演算法相同。
四:選擇排序
★演算法描述:
選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小元素,然後放到排序序列末尾。以此類推,直到所有元素均排序完畢。
[cpp] view plain copy print?- #include<iostream>
- usingnamespace std;
- void SelectionSort(int* arr, size_t size)
- {
- assert(arr);
- int min;
- for (size_t i = 0; i < size; i++)
- {
- min = i;
- for (size_t j = i + 1; j < size; j++)
- if (arr[j] < arr[min])
- min = j;
- swap(arr[i], arr[min]);
- }
- }
#include<iostream>
using namespace std;
void SelectionSort(int* arr, size_t size)
{
assert(arr);
int min;
for (size_t i = 0; i < size; i++)
{
min = i;
for (size_t j = i + 1; j < size; j++)
if (arr[j] < arr[min])
min = j;
swap(arr[i], arr[min]);
}
}
▲注:對於簡單的選擇排序其改進的方法是傳統的簡單選擇排序,每趟迴圈只能確定一個元素排序後的定位,所以可以考慮改進為每趟迴圈確定兩個元素(當前趟最大和最小記錄)的位置,從而減少排序所需的迴圈次數。改進後對n個數據進行排序,最多隻需進行[n/2]趟迴圈即可。
★選擇排序:
五:堆排序
★演算法描述:
堆積排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
★演算法步驟:過程比較複雜,簡單點說就三步:建堆、調堆、排序。
[cpp] view plain copy print?- #include<iostream>
- usingnamespace std;
- void AdjustDown(int *arr, size_t size, int root)//向下調整
- {
- size_t child = 2 * root + 1;
- while (child < size)
- {
- if (child + 1 < size && arr[child + 1] > arr[child])
- {
- ++child;
- }
- if (arr[child] > arr[root])
- {
- swap(arr[child], arr[root]);
- root = child;
- child = 2 * root + 1;
- }
- else
- {
- break;
- }
- }
- }
- void HeapSort(int *arr, size_t size)
- {
- assert(arr);
- for (int i = (size - 2) / 2; i >= 0; i--)
- {
- AdjustDown(arr, size, i);
- }
- for (size_t i = 0; i < size; ++i)
- {
- swap(arr[0], arr[size - 1 - i]);
- AdjustDown(arr, size - 1 - i, 0);
- }
- }
#include<iostream>
using namespace std;
void AdjustDown(int *arr, size_t size, int root)//向下調整
{
size_t child = 2 * root + 1;
while (child < size)
{
if (child + 1 < size && arr[child + 1] > arr[child])
{
++child;
}
if (arr[child] > arr[root])
{
swap(arr[child], arr[root]);
root = child;
child = 2 * root + 1;
}
else
{
break;
}
}
}
void HeapSort(int *arr, size_t size)
{
assert(arr);
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdjustDown(arr, size, i);
}
for (size_t i = 0; i < size; ++i)
{
swap(arr[0], arr[size - 1 - i]);
AdjustDown(arr, size - 1 - i, 0);
}
}
▲注:堆排序的最明顯的優勢在於只需要O(1)的輔助儲存空間,且明顯減少了最大值的多餘比較。客觀的來說,堆排的效能還不錯。
★堆排序:
六:歸併排序
★演算法描述:
歸併排序(Merge sort,臺灣譯作:合併排序)是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。
★演算法步驟:
1、申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列;
2、設定兩個指標,最初位置分別為兩個已經排序序列的起始位置;
3、比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置;
4、重複步驟3直到某一指標達到序列尾;
5、將另一序列剩下的所有元素直接複製到合併序列尾
- #include<iostream>
- usingnamespace std;
- //將有二個有序數列a[first...mid]和a[mid...last]合併。
- void mergearray(int a[], int first, int mid, int last, int temp[])
- {
- int i = first, j = mid + 1;
- int m = mid, n = last;
- int k = 0;
- while (i <= m && j <= n)
- {
- if (a[i] < a[j])
- temp[k++] = a[i++];
- else
- temp[k++] = a[j++];
- }
- while (i <= m)
- temp[k++] = a[i++];
- while (j <= n)
- temp[k++] = a[j++];
- for (i = 0; i < k; i++)
- a[first + i] = temp[i];
- }
- void mergesort(int a[], int first, int last, int temp[])
- {
- if (first < last)
- {
- int mid = (first + last) / 2;
- mergesort(a, first, mid, temp); //左邊有序
- mergesort(a, mid + 1, last, temp); //右邊有序
- mergearray(a, first, mid, last, temp); //再將二個有序數列合併
- }
- }
- bool MergeSort(int a[], int n)
- {
- int *p = newint[n];
- if (p == NULL)
- returnfalse;
- mergesort(a, 0, n - 1, p);
- delete[] p;
- returntrue;
- }
#include<iostream>
using namespace std;
//將有二個有序數列a[first...mid]和a[mid...last]合併。
void mergearray(int a[], int first, int mid, int last, int temp[])
{
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n)
{
if (a[i] < a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
void mergesort(int a[], int first, int last, int temp[])
{
if (first < last)
{
int mid = (first + last) / 2;
mergesort(a, first, mid, temp); //左邊有序
mergesort(a, mid + 1, last, temp); //右邊有序
mergearray(a, first, mid, last, temp); //再將二個有序數列合併
}
}
bool MergeSort(int a[], int n)
{
int *p = new int[n];
if (p == NULL)
return false;
mergesort(a, 0, n - 1, p);
delete[] p;
return true;
}
▲注:對於歸併排序的優化最為簡單的一種方法則為利用插入排序優化歸併排序,在歸併中利用插入排序不僅可以減少遞迴次數,還可以減少記憶體分配次數。
★歸併排序:
七:計數排序
★演算法描述:
計數排序(Counting sort)是一種穩定的排序演算法,和基數排序一樣都是桶排序的變體。計數排序使用一個額外的陣列C,其中第i個元素是待排序陣列A中值小於等於i的元素的個數。然後根據陣列C來將A中的元素排到正確的位置。
★演算法步驟:
1、找出待排序的陣列中最大和最小的元素;
2、統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項;
3、對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
4、反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1;
- void counting_sort(int A[], int length_A, int B[], int k)
- {
- int C[MAX] = { 0 };//C是臨時陣列
- for (int i = 1; i <= length_A; i++)
- C[A[i]]++;
- //此時C[i]包含等於i的元素個數
- for (int i = 1; i <= k; i++)
- C[i] = C[i] + C[i - 1];
- //此時C[i]包含小於或者等於i的元素個數
- for (int i = length_A; i >= 1; i--)//從length_A到1逆序遍歷是為了保證相同元素排序後的相對順序不改變
- { //如果從1到length_A,則相同元素的相對順序會逆序,但結果也是正確的
- B[C[A[i]]] = A[i];
- C[A[i]]--;
- }
- }
void counting_sort(int A[], int length_A, int B[], int k)
{
int C[MAX] = { 0 };//C是臨時陣列
for (int i = 1; i <= length_A; i++)
C[A[i]]++;
//此時C[i]包含等於i的元素個數
for (int i = 1; i <= k; i++)
C[i] = C[i] + C[i - 1];
//此時C[i]包含小於或者等於i的元素個數
for (int i = length_A; i >= 1; i--)//從length_A到1逆序遍歷是為了保證相同元素排序後的相對順序不改變
{ //如果從1到length_A,則相同元素的相對順序會逆序,但結果也是正確的
B[C[A[i]]] = A[i];
C[A[i]]--;
}
}
▲注:計數排序的特性是可以用在基數排序中的演算法來排序資料範圍很大的陣列。尤其當輸入的元素是 n 個 0 到 k 之間的整數時,它的執行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。但其缺點是對於字串型資料無法處理,僅能處理數字集。
八:基數排序
★演算法描述:
基數排序(Radix Sort)是對桶排序的改進和推廣。唯一的區別是基數排序強調多關鍵字,而桶排序沒有這個概念,換句話說基數排序對每一種關鍵字都進行桶排序,而桶排序同一個桶內排序可以任意或直接排好序。
[cpp] view plain copy print?- void radixSort(int data[]) {
- int temp[10][10] = { 0 };
- int order[10] = { 0 };
- int n = 1;
- while (n <= 10) {
- int i;
- for (i = 0; i < 10; i++) {
- int lsd = ((data[i] / n) % 10);
- temp[lsd][order[lsd]] = data[i];
- order[lsd]++;
- }
- // 重新排列
- int k = 0;
- for (i = 0; i < 10; i++) {
- if (order[i] != 0) {
- int j;
- for (j = 0; j < order[i]; j++, k++) {
- data[k] = temp[i][j];
- }
- }
- order[i] = 0;
- }
- n *= 10;
- }
- }
void radixSort(int data[]) {
int temp[10][10] = { 0 };
int order[10] = { 0 };
int n = 1;
while (n <= 10) {
int i;
for (i = 0; i < 10; i++) {
int lsd = ((data[i] / n) % 10);
temp[lsd][order[lsd]] = data[i];
order[lsd]++;
}
// 重新排列
int k = 0;
for (i = 0; i < 10; i++) {
if (order[i] != 0) {
int j;
for (j = 0; j < order[i]; j++, k++) {
data[k] = temp[i][j];
}
}
order[i] = 0;
}
n *= 10;
}
}
▲注:基數排序應用到字串處理的倍增演算法裡面,這個倍增演算法,要反覆的進行排序。如果排序能快一點,程式就能快很多。
※注:上圖列舉出了各種排序演算法的時間複雜度,空間複雜度以及穩定性,並不能籠統的說哪一種演算法好,遇到實際問題時根據具體需要予以選擇恰當的排序演算法,這才是明智之舉。