現今社會,資料量不斷地增加,因此排序演算法的重要性日益提升,從大數據排序到網路搜尋引擎排序,排序演算法在我們日常生活中扮演了重要的角色。
本文將介紹10種常見的排序演算法,來和我一起學習掌握排序演算法的原理及實作過程吧!
下文介紹的 10 種排序演算法都會附上寫好的函式 這裡提供測試模版 在區域中加上各種排序演算法函式即可運作 可 EOF 輸入
#include <bits/stdc++.h>
using namespace std;
vector<int> v;
// 可以將底下的某個排序演算法函式複製起來放進來這個區域
// ----------------------------------------------
// ----------------------------------------------
int main(void)
{
int n;
while (cin >> n) v.emplace_back(n);
// 這行放入某個排序演算法函式名稱用來呼叫 例如:BubbleSort();
// ------------------------------------------
// ------------------------------------------
for (auto &i : v) cout << v[i] << " ";
return 0;
}
氣泡排序是一種簡單的排序演算法,它的原理是依序比較相鄰的兩個元素,如果前一個元素大於後一個元素,就交換它們的位置,重複進行直到排序完成。因為排序過程中較大的元素像氣泡一樣慢慢浮到數列的右端,所以叫氣泡排序。
void BubbleSort()
{
int n = v.size();
for (int i=n; i>1; --i)
{
for (int j=0; j<i-1; ++j)
{
if (v[j] > v[j+1])
{
swap(v[j],v[j+1]);
}
}
}
}
外層 for 控制輪數,每輪內層 for 會比較相鄰的元素並交換它們的位置,如果沒有發生交換,就代表數列已經排好序了,可以提前結束,這樣可以減少後面發生不必要的比較
void BubbleSort()
{
int n = v.size();
for (int i=n; i>1; --i)
{
bool check = false;
for (int j=0; j<i-1; ++j)
{
if (v[j] > v[j+1])
{
swap(v[j],v[j+1]);
check = true;
}
}
if (!check) break;
}
}
時間複雜度:
每一輪排序都需要進行
次比較,而最多需要進行 輪排序。
因此,總的比較次數為
取最高次項所以時間複雜度為
氣泡排序演算法是一種簡單但效率較低的排序演算法,通常只適用於小規模數據的排序。
選擇排序是一種簡單的排序演算法,它的原理是選擇最小的元素,與第一個元素交換位置,然後在剩下的元素中選擇最小的元素,與第二個元素交換位置,以此類推,重複直到排序完成。
void SelectionSort()
{
int n = v.size();
for (int i=0; i<n-1;++i)
{
int mini = i;
for (int j=i; j<n; ++j)
{
if (v[j] < v[mini])
{
mini = j;
}
}
swap(v[i],v[mini]);
}
}
時間複雜度:
選擇排序的核心操作是選擇最小元素,將其與當前位置交換,而選擇最小元素需要在未排序的序列中進行線性搜索,因此需要執行
次循環和 $n4 次內層循環,時間複雜度為 。
選擇排序演算法是一種簡單但效率較低的排序演算法,通常只適用於小規模數據的排序,且較不穩定,如當元素相等時,彼此順序還是會改變。
將未排序的數據依次插入已排序序列中,形成新的已排序序列。
void InsertSort()
{
int n = v.size();
for (int i=1; i<n;++i)
{
int temp = v[i], j = i-1;
while (j >= 0 && temp < v[j])
{
v[j+1] = v[j];
j--;
}
v[j+1] = temp;
}
}
要遍歷
個元素,每次要往前掃瞄並搬動元素
插入排序演算法是一種簡單但效率較低的排序演算法,通常只適用於小規模數據的排序。
合併排序 Merge Sort 是一種基於分治法的排序演算法,也是一種比較經典且常用的排序演算法之一。
合併排序的主要流程包括分解、排序和合併三個步驟。首先將要排序的序列分成兩部分,分別對這兩部分進行排序,最後將排好序的兩個部分合併起來即可得到排序後的序列。在排序的過程中,通過遞歸地將序列分解成小問題,再利用合併操作將小問題的解合併成原問題的解。
呼叫時請用 MergeSort(0,v.size());
void merge(int l, int r)
{
int mid = (l+r)/2, tmp[r-l+1];
for(int i=l,j=mid+1,k=0; i<=mid || j<=r; ++k)
{
if((v[i] <= v[j] && i <= mid) || j > r) tmp[k] = v[i++];
else tmp[k] = v[j++];
}
for(int i=l,j=0; i<=r; ++i,++j) v[i] = tmp[j];
}
void MergeSort(int l, int r)
{
if(l == r) return;
int mid = (l+r)/2;
MergeSort(l,mid); MergeSort(mid+1,r);
merge(l,r);
}
平均時間複雜度:
在平均情況下,合併排序的時間複雜度為
。這是因為在合併排序的過程中,每個數據都需要進行一次比較,而且每個數據都需要被移動到新數組中的正確位置,這樣每個數據都需要進行 級別的操作。
空間複雜度:
合併排序需要額外的存儲空間來存儲排序過程中的數據,這些存儲空間的大小與待排序數列的大小相同。
合併排序是一種高效穩定的排序算法,通過分治的思想,先拆分問題再合併解決。其時間複雜度為
快速排序 Quick Sort 是一種常見的分治演算法,被認為是最快的排序演算法之一。它是選擇一個基準元素,通常是中間點,通過一趟排序將待排序列分為兩部分,其中一部分的所有元素都比基準元素小,另一部分的所有元素都比基準元素大,然後再按照此方法對這兩部分分別進行快速排序,直到整個序列有序。
本動畫以左邊當基準點
呼叫時請用 QuickSort(0,v.size());
void QuickSort(int l, int r)
{
if (l >= r) return;
int i = l, j = r, pivot = v[(l + r) / 2];
while (i <= j)
{
while (v[i] < pivot) i++;
while (v[j] > pivot) j--;
if (i <= j)
{
swap(v[i], v[j]);
i++; j--;
}
}
QuickSort(l, j); QuickSort(i, r);
}
平均時間複雜度:
在平均情況下,每次切分都能將數列分為近似相等的兩個子數列,快速排序的時間複雜度為
。
最壞時間複雜度:
當數列已經排好序或接近排好序時,選擇第一個或最後一個元素作為基準元素,時間複雜度會退化為
。
快速排序演算法是透過分治,達成高效率的排序演算法。它可以在短時間內對大型數據進行排序。儘管最壞情況下的時間複雜度較高,但在大多數情況下,它的表現都很優秀。
堆積排序 Heap Sort 是一種使用二元樹 Binary Tree 資料結構的排序演算法。
堆可以看作是一個完全二元樹,它具有以下兩個性質:
#include<bits/stdc++.h>
using namespace std;
void heapify(int arr[], int n, int i) {
int largest = i; // 初始化最大值為根節點
int left = 2 * i + 1; // 找到左子節點的索引
int right = 2 * i + 2; // 找到右子節點的索引
if (left < n && arr[left] > arr[largest]) { // 如果左子節點的值比最大值還要大
largest = left; // 更新最大值的索引為左子節點的索引
}
if (right < n && arr[right] > arr[largest]) { // 如果右子節點的值比最大值還要大
largest = right; // 更新最大值的索引為右子節點的索引
}
if (largest != i) { // 如果最大值不是根節點
swap(arr[i], arr[largest]); // 把最大值交換到根節點
heapify(arr, n, largest); // 遞迴對以最大值為根的子樹進行heapify操作
}
}
void heapSort(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) { // 建立最大堆
heapify(arr, n, i);
}
for (int i = n - 1; i > 0; i--) { // 進行堆排序
swap(arr[0], arr[i]); // 把最大值與根節點交換
heapify(arr, i, 0); // 對剩餘元素進行heapify操作
}
}
int main() { // 測試堆積排序
int n; // 宣告變數 n 為數列長度
cout << "請輸入要排序的數列長度:";
cin >> n;
int arr[n]; // 宣告陣列 arr 用以儲存待排序數列
cout << "請依序輸入數列元素:\n";
for (int i = 0; i < n; i++) { // 重複 n 次輸入數值存入陣列 arr 中
cin >> arr[i];
}
heapSort(arr, n); // 呼叫堆積排序函式對 arr 進行排序
cout << "排序後的數列:\n";
for (int i = 0; i < n; i++) { // 利用迴圈依序輸出排序後的陣列元素
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
時間複雜度:O(nlogn)
在堆積排序中,排序的主要操作是下潛,即將堆頂元素下潛到合適的位置,這個操作的時間複雜度是 O(logn)。在排序過程中,需要執行 n 次下潛操作,因此排序的時間複雜度為 O(nlogn)。
空間複雜度:O(1)
由於堆積排序是一種原地排序算法,因此它的空間複雜度是 O(1),即不需要額外的空間。在堆排序中,只需要用到常數個變量作為中間變量,不需要額外的數組或其他數據結構。
堆排序是一種高效的排序算法,它具有良好的時間複雜度和空間複雜度,並且它只需要一個輔助空間來存儲堆,可以實現原地排序,因此堆排序在排序大數據時非常有效。但是在實際應用中,由於堆排序的常數因子比較大,因此實際運行速度可能不如快速排序和插入排序等算法。
希爾排序(Shell Sort)是一種插入排序的改進版,其基本思想是先將待排序的序列按照一定間隔分成幾個子序列,然後對每個子序列進行插入排序。接著逐步縮小間隔,重複進行上述操作,直到間隔縮小到1時,最後對整個序列進行一次插入排序,完成排序。
希爾排序的主要優點是在比較次數和移動次數上都有所改進。因為希爾排序采用分組的方式進行插入排序,每次排序可以使得一定程度上有序,因此在進行後面的排序時就可以利用前面排序時建立的有序性,減少比較次數和移動次數。此外,希爾排序不需要額外的內存空間,適合在內存較小的情況下進行排序。
#include<bits/stdc++.h>
using namespace std;
void shellSort(int arr[], int n) {
// 初始化增量gap,設為n/2、n/4、n/8、...直到1
for (int gap = n / 2; gap > 0; gap /= 2) {
// 以gap為間隔,對每個子序列進行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j;
// 在子序列中進行插入排序
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
int main() { // 測試希爾排序
int n; // 宣告變數 n 為數列長度
cout << "請輸入要排序的數列長度:";
cin >> n;
int arr[n]; // 宣告陣列 arr 用以儲存待排序數列
cout << "請依序輸入數列元素:\n";
for (int i = 0; i < n; i++) { // 重複 n 次輸入數值存入陣列 arr 中
cin >> arr[i];
}
shellSort(arr, n); // 呼叫希爾排序函式對 arr 進行排序
cout << "排序後的數列:\n";
for (int i = 0; i < n; i++) { // 利用迴圈依序輸出排序後的陣列元素
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
時間複雜度:
希爾排序的時間複雜度取決於子序列的間隔序列(Increment sequence),一般會使用Hibbard增量序列(Hibbard's increment sequence),其公式為:h_k = 2^k - 1,其中k為子序列的索引,h_k為對應的增量。
空間複雜度:O(1)
希爾排序是一種原地排序算法,只需要一個輔助變量來進行元素交換,因此空間複雜度為O(1)。
希爾排序是一種高效的排序算法,它通常比傳統的插入排序要快很多,特別是對於大型數據集。希爾排序采用分組的方式進行插入排序,每次排序可以使得一定程度上有序,因此在進行後面的排序時就可以利用前面排序時建立的有序性,減少比較次數和移動次數。
計數排序(Counting Sort)是一種線性時間的排序算法,它可以用於排序一定範圍內的整數。計數排序的核心思想是先統計每個元素出現的次數,然後根據元素出現的次數,將元素排列成有序序列。
#include<bits/stdc++.h>
using namespace std;
// 定義計數排序函數
void countingSort(vector<int>& arr) {
int n = arr.size();
int max_val = 100; // 假設元素的範圍為[0,100]
// 計算元素出現的次數,初始化計數數組為0
vector<int> count(max_val + 1, 0);
for (int i = 0; i < n; i++) {
count[arr[i]]++; // 計算arr[i]出現的次數
}
// 累加前面所有元素的值,得到每個元素在有序序列中的位置
for (int i = 1; i <= max_val; i++) {
count[i] += count[i - 1]; // 累加前面所有元素的值
}
// 根據計數數組和有序序列的位置信息,將元素依次放入有序序列中
vector<int> result(n, 0);
for (int i = n - 1; i >= 0; i--) {
result[count[arr[i]] - 1] = arr[i]; // 將arr[i]放到有序序列的對應位置上
count[arr[i]]--; // 將計數數組中對應元素的值減1
}
// 將結果返回到原數組中
for (int i = 0; i < n; i++) {
arr[i] = result[i];
}
}
// 測試計數排序函數
int main() {
int n;
cout << "請輸入數組的大小:";
cin >> n;
cout << "請輸入" << n << "個整數,範圍為[0,100]:";
vector<int> arr(n); // 定義一個整數向量
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
countingSort(arr); // 呼叫計數排序函式對 arr 進行排序
cout << "排序後的數組:";
for (auto num : arr) {
cout << num << " ";
}
return 0;
}
時間複雜度:O(n+k)
計數排序的時間複雜度可以分為兩部分:計數過程和排序過程。首先是計數過程,需要對整個序列進行一次遍歷,把每個元素出現的次數記錄在計數數組中。由於計數數組的大小等於待排序序列的範圍,因此計數過程的時間複雜度為 O(n+k),其中 n 是序列的長度,k 是序列中元素的範圍。接下來是排序過程,需要遍歷待排序序列,根據計數數組中的信息將每個元素放置到排序好的位置上。由於只需要遍歷一次待排序序列,因此排序過程的時間複雜度為 O(n)。因此,計數排序的時間複雜度為 O(n+k),其中 n 為待排序元素的數量,k 為待排序元素的最大值。需要注意的是,當範圍 k 比較大時,計數排序的效率可能會比較低。
空間複雜度:O(k)
計數排序的空間複雜度主要取決於計數數組的大小 k。因此,計數排序的空間複雜度為 O(k)。需要注意的是,當範圍 k 比較大時,計數排序的空間複雜度也會相應增加。
計數排序是一種高效的排序算法,適用於元素範圍較小的場景,在各種應用中都有著廣泛的應用,例如對於年齡、成績等數值型數據的排序。
儘管它的時間複雜度比其他常用排序算法(如快速排序和合併排序)更小,但是它的應用受到了很大的限制,因為它需要在內存中創建一個大小為k的計數數組,如果k太大,計數數組將占用大量內存。此外,計數排序也不適用於具有負值元素的數組。
桶排序(Bucket Sort)是一種非常簡單的排序演算法,它的基本思想是將要排序的資料分為幾個桶,每個桶裡的資料都有一定的範圍。然後,對每個桶中的資料進行排序,最後按照桶的順序將所有桶中的資料合併起來。
#include<bits/stdc++.h>
using namespace std;
void bucketSort(vector<int>& arr) {
int n = arr.size();
// 建立桶(bucket)的數量,這裡以10個桶作為範例
const int bucket_num = 10;
vector<vector<int>> buckets(bucket_num);
// 將數據分配到對應的桶中
for (int i = 0; i < n; i++) {
int index = arr[i] / bucket_num;
buckets[index].push_back(arr[i]);
}
// 對每個桶中的數據進行排序
for (int i = 0; i < bucket_num; i++) {
sort(buckets[i].begin(), buckets[i].end());
}
// 將排序後的數據依次放回原數組中
int k = 0;
for (int i = 0; i < bucket_num; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[k] = buckets[i][j];
k++;
}
}
}
// 測試桶排序函數
int main() {
int n;
cout << "請輸入數列的大小: ";
cin >> n;
vector<int> arr(n);
cout << "請輸入" << n << "個整數: ";
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
bucketSort(arr); // 呼叫桶排序函式對 arr 進行排序
cout << "排序後的數列: ";
for (auto num : arr) {
cout << num << " ";
}
return 0;
}
時間複雜度:O(n+k)
計數排序的時間複雜度可以分為兩部分:計數過程和排序過程。首先是計數過程,需要對整個序列進行一次遍歷,把每個元素出現的次數記錄在計數數組中。由於計數數組的大小等於待排序序列的範圍,因此計數過程的時間複雜度為 O(n+k),其中 n 是序列的長度,k 是序列中元素的範圍。接下來是排序過程,需要遍歷待排序序列,根據計數數組中的信息將每個元素放置到排序好的位置上。由於只需要遍歷一次待排序序列,因此排序過程的時間複雜度為 O(n)。因此,計數排序的時間複雜度為 O(n+k),其中 n 為待排序元素的數量,k 為待排序元素的最大值。需要注意的是,當範圍 k 比較大時,計數排序的效率可能會比較低。
空間複雜度:O(n+k)
桶排序的空間複雜度取決於桶的數量和每個桶內部元素的個數。由於每個桶內部的元素個數都不超過n/k,因此每個桶所需的空間是O(n/k)。總空間複雜度就是O(n + k)。如果k接近n,則空間複雜度就會接近O(n)。需要注意的是,當k比較大時,可能會出現空間浪費的情況,因此需要根據具體情況來選擇適當的桶數量。
桶排序是一種簡單但有效的線性時間複雜度排序算法,優點是簡單易懂,而且比較容易實現。桶排序在數據分佈比較集中的情況下效果較好,但當數據分佈比較分散時,則會產生較多的桶(bucket)。適用於待排序數據分布範圍有限的情況。
基數排序是一種非比較排序算法,適用於整數排序。基本思想是根據排序元素的位數,將整數按照位數從低到高或者從高到低進行排序,可以使用桶排序或計數排序等算法來實現。它的排序過程是先從最低有效位開始,依次對每一位進行排序,直到最高有效位。
例如,將一個整數序列按照個位、十位、百位的順序來排序。首先,按照個位進行排序,將序列中所有數字根據個位數分成10個桶,分別把它們放進對應的桶中。然後,按照桶的順序把數字放回原序列中。接下來,再按照十位進行排序,以此類推,直到按照最高有效位進行排序為止。
#include<bits/stdc++.h>
using namespace std;
void radix_sort(vector<int>& arr) {
int max = *max_element(arr.begin(), arr.end()); // 找出最大值
int digit = 1;
vector<int> tmp(arr.size()); // 建立暫存的 vector
while (max / digit > 0) {
vector<int> count(10); // 計數排序用的計數陣列
// 計算每個桶子中的元素個數
for (int i = 0; i < arr.size(); i++) {
int bucket = (arr[i] / digit) % 10;
count[bucket]++;
}
// 計算每個桶子中元素在暫存陣列中的結束位置
for (int i = 1; i < count.size(); i++) {
count[i] += count[i-1];
}
// 把元素放入暫存陣列
for (int i = arr.size() - 1; i >= 0; i--) {
int bucket = (arr[i] / digit) % 10;
tmp[count[bucket] - 1] = arr[i];
count[bucket]--;
}
// 把暫存陣列中的元素放回原陣列
for (int i = 0; i < arr.size(); i++) {
arr[i] = tmp[i];
}
digit *= 10; // 到下一個數位
}
}
// 測試基數排序函數
int main() {
int n;
cout << "請輸入數組的大小:";
cin >> n;
cout << "請輸入" << n << "個整數:";
vector<int> arr(n); // 定義一個整數向量
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
radix_sort(arr); // 呼叫基數排序函式對 arr 進行排序
cout << "排序後的數組:";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
return 0;
}
時間複雜度:O(d(n+k))
其中 d 為最大位數,n 為數組大小,k 為桶子數量,通常為 10(代表數字 0~9)。因為每一位數都要進行一次計數排序,計數排序的時間複雜度為 O(n+k),所以時間複雜度為 O(d(n+k))。
空間複雜度:O(n+k)
基數排序的空間複雜度主要由暫存陣列和計數陣列決定,因此空間複雜度為 O(n+k)。
總結來說,基數排序是一種穩定性較好且時間複雜度為線性的排序算法,但對於數字位數較大的情況下,其空間複雜度較高,可能需要額外的存儲空間。基數排序的優點是能夠處理不同長度的數字,且在數字大小範圍有限的情況下,表現優於快速排序和堆排序。但是,它需要額外的空間儲存桶,且當數字大小範圍非常大時,需要大量的額外空間,並且其時間複雜度也會增加。
排序演算法 | 最差時間複雜度 | 平均時間複雜度 | 最佳時間複雜度 | 空間複雜度 | 方式 | 穩定度 |
---|---|---|---|---|---|---|
氣泡排序 | In-place | ✅ | ||||
選擇排序 | In-place | ❌ | ||||
插入排序 | In-place | ✅ | ||||
合併排序 | Out-place | ✅ | ||||
快速排序 | In-place | ❌ | ||||
堆積排序 | In-place | ❌ | ||||
希爾排序 | In-place | ❌ | ||||
計數排序 | Out-place | ✅ | ||||
桶排序 | Out-place | ❌ | ||||
基數排序 | Out-place | ✅ |
排序演算法 | 主要特點 | 優點 | 缺點 |
---|---|---|---|
氣泡排序 | 一種簡單的交換排序演算法,每次將相鄰的元素進行比較和交換 | 實現簡單,程式易懂 | 時間複雜度較高,效率低 |
選擇排序 | 每次選出最小(大)的元素放到已排序序列的末尾 | 實現簡單,程式易懂,穩定 | 時間複雜度較高,效率低 |
插入排序 | 將未排序元素逐個插入到已排序的序列中,從後往前比較 | 實現簡單,對小規模資料排序效率高 | 時間複雜度較高,對大規模資料排序效率較低 |
合併排序 | 分治策略,將序列遞迴划分為子序列,然後將子序列合併 | 時間複雜度較低,效率較高,穩定 | 需要較大的輔助空間 |
快速排序 | 分治策略,選定一個基準元素,將序列分為左右兩部分,遞迴排序 | 時間複雜度較低,效率較高,適用於大規模資料排序 | 不穩定,最壞情況下時間複雜度較高 |
堆積排序 | 將序列構建成大根堆(小根堆),每次將堆頂元素與末尾元素交換,重新調整堆 | 時間複雜度較低,效率較高,適用於大規模資料排序 | 不穩定 |
希爾排序 | 插入排序的改進版本,設定一個增量,將序列劃分為若干子序列進行排序 | 對於中等規模資料排序效率較高 | 不穩定 |
計數排序 | 統計序列中各元素的出現次數,根據出現次數和元素值的關係排序 | 時間複雜度較低,適用於數據範圍較小的整數排序 | 對於數據範圍較大的情況需要較大的輔助空間 |
桶排序 | 將元素劃分到不同的桶中,對每個桶中的元素進行排序,最後合併 | 適用於元素值分佈較均勻的情況,時間複雜度較低 | 對於元素值分佈不均勻的情況效率較低 |
基數排序 | 按照元素的位數進行排序,從低位到高位進行排序,每一位使用穩定排序演算法進行排序 | 適用於大規模資料排序且穩定,可以處理多關鍵字排序 | 需要額外的記憶體空間且時間複雜度高,效率較低 |
在本文中,我們介紹了 10 種常見的排序演算法,每種演算法都有其優點和缺點。在實際應用中,需要根據具體的情況選擇最適合的排序演算法。如果你想學習排序演算法,我建議利用本文章及網路上各種資源,理解各個演算法中的原理,並且嘗試自己實作出這些演算法。通過不斷的練習,你將能更深入地理解這些排序演算法的原理和應用,或許能夠應用來解決現實中的問題,希望此篇文章能讓你有所收穫!
此篇文章因內容繁多,所以在整理資料及撰寫上,可能會有些錯誤,也請大家多留言或善用右側聊天室提出問題,我會馬上勘誤修正,謝謝。