【C++】十大經典排序算法詳解


本文譯自《【Python】十大經典排序算法詳解》,由Python譯爲C++。

排序算法總覽

十大排序算法可以分爲比較類排序以及非比較類排序。

  • 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn)O(nlogn),因此也稱爲非線性時間比較類排序。
  • 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
    在這裏插入圖片描述
    排序算法可以分爲內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。常見的內部排序算法有:插入排序、希爾排序、選擇排序、冒泡排序、歸併排序、快速排序、堆排序、基數排序等。用一張表格概括:
排序算法 平均時間複雜度 最好情況 最壞情況 空間複雜度 排序方式 穩定性
冒泡排序 O(n2)O(n^2) O(n)O(n) O(n2)O(n^2) O(1)O(1) In-place 穩定
選擇排序 O(n2)O(n^2) O(n2)O(n^2) O(n2)O(n^2) O(1)O(1) In-place 不穩定
插入排序 O(n2)O(n2) O(n)O(n) O(n2)O(n^2) O(1)O(1) In- place 穩定
希爾排序 O(nlogn)O(n log n) O(nlog2n)O(n log^2 n) O(nlog2n)O(n log^2n) O(1)O(1) In- place 不穩定
歸併排序 O(nlogn)O(n log n) O(nlogn)O(nlogn) O(nlogn)O(nlogn) O(n)O(n) Out-place 穩定
快速排序 O(nlogn)O(n log n) O(nlogn)O(n log n) O(n2)O(n^2) O(logn)O(log n) In-place 不穩定
堆排序 O(nlogn)O(n log n) O(nlogn)O(nlogn) O(nlogn)O(nlogn) O(1)O(1) In-place 不穩定
計數排序 O(n+k)O(n + k) O(n+k)O(n + k) O(n+k)O(n + k) O(k)O(k) Out-place 穩定
桶排序 O(n+k)O(n + k) O(n+k)O(n + k) O(n2)O(n^2) O(n+k)O(n + k) Out- -place 穩定
基數排序 O(nk)O(nk) O(nk)O(nk) O(nk)O(nk) O(n+k)O(n + k) Out-place 穩定

比較類排序算法

交換排序

一. 冒泡排序

算法思想

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

作爲最簡單的排序算法之一,冒泡排序給我的感覺就像 Abandon 在單詞書裏出現的感覺一樣,每次都在第一頁第一位,所以最熟悉。冒泡排序還有一種優化算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升性能來說並沒有什麼太大作用。

算法步驟

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

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

template <typename T>
//整數或浮點數皆可使用
void bubble_sort(T a[],int len) {
    for(int i=0; i<len-1; i++)
        for(int j=0; j<len-1-i; j++)
        	if(a[j]>a[j+1]) swap(a[j],a[j+1]);
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	bubble_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

二. 快速排序

算法思想

快速排序是由東尼·霍爾所發展的一種排序算法。在平均狀況下,排序 n 個項目要 O(nlogn)Ο(nlogn) 次比較。在最壞狀況下則需要 O(n2)Ο(n^2) 次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他 O(nlogn)Ο(nlogn) 算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。快速排序使用分治法(Divide and conquer)策略來把一個串行(list)分爲兩個子串行(sub-lists)。快速排序又是一種分而治之思想在排序算法上的典型應用。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。

算法步驟

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

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	sort(a,a+n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}
#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void qsort(int a[],int l,int r) {
	if(l>=r) return; //如果左邊索引大於或者等於右邊的索引就代表已經整理完成一個組了
	int i=l,j=r;
	int x=a[l];
	while(i<j) {    /*控制在當組內尋找一遍*/
		while(i<j && x<=a[j]) j--;/*向前尋找*/
        /*而尋找結束的條件就是:
        1.找到一個小於或者大於key的數(大於或小於取決於你想升序還是降序)
        2.沒有符合條件1的,並且i與j的大小沒有反轉*/ 
		a[i]=a[j];
        //找到一個這樣的數後就把它賦給前面的被拿走的i的值(如果第一次循環且key是 a[l],那麼就是給x)
		while(i<j && x>=a[i]) i++;
        /*這是i在當組內向前尋找,同上,不過注意與x的大小關係停止循環和上面相反,
        因爲排序思想是把數往兩邊扔,所以左右兩邊的數大小與x的關係相反*/
		a[j]=a[i];
	}
	a[i]=x; //當在當組內找完一遍以後就把中間數key迴歸
	qsort(a,l,i-1); //最後用同樣的方式對分出來的左邊的小組進行同上的做法
    qsort(a,i+1,r); //用同樣的方式對分出來的右邊的小組進行同上的做法
    //當然最後可能會出現很多分左右,直到每一組的 i=j 爲止
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	qsort(a,0,n-1);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void qsort(int a[],int l,int r){
    if(l>=r) return;
    int i=l,j=r+1;
    int x=a[l];
    while(1) {
        //從左向右找比x大的值
        while(a[++i]<x)
            if(i==r) break;
        //從右向左找比x小的值
        while(a[--j]>x)
            if(j==l) break;
        if(i>=j) break;
        //交換i,j對應的值
        swap(a[i],a[j]);
    }
    //中樞值與j對應值交換
    swap(a[l],a[j]);
    qsort(a,l,j-1);
    qsort(a,j+1,r);
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	qsort(a,0,n-1);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

三. 插入排序

算法思想

插入排序是一種最簡單直觀的排序算法,它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。就像我們鬥地主時,抽牌階段會把抽到的牌插入到相應的位置中去,使手上的牌有序。
插入排序有個小優化叫做折半插入,就是往前尋找插入位置時,因爲前面的數組全部有序,因此我們用二分查找法來尋找插入位置。

算法步驟

將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面,保持相應順序不變,插入排序是一個穩定的排序算法。)

動圖演示

代碼實現

def insertionSort(arr):
    for i in range(1,len(arr)):
        pos, insert_num = 0, arr[i]
        for j in range(i-1,-1,-1):
            if insert_num < arr[j]:
                arr[j+1] = arr[j]
            if insert_num >= arr[j]:
                arr[j+1] = insert_num
                pos = j+1
                break
        if pos == 0:
            arr[0] = insert_num

四. 希爾排序

算法思想

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

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

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

希爾排序的基本思想是:先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,待整個序列中的記錄"基本有序"時,再對全體記錄進行依次直接插入排序。

算法步驟

1.選擇一個增量序列 t1t2tkt_1,t_2,……,t_k,其中 ti>tjt_i > t_j, tk=1t_k = 1
2.按增量序列個數 kk,對序列進行 kk 趟排序;
3.每趟排序,根據對應的增量 tit_i,將待排序列分割成若干長度爲 mm 的子序列,分別對各子表進行直接插入排序。僅增量因子爲 11 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

動圖演示

代碼實現

只需要將插入排序稍微修改一下,就可以得到希爾排序。

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void insertion_sort(int a[],int len){
	for(int i=0; i<len; i++) {
		//將a[i]插入到a[i-1],a[i-2],a[i-3]……之中
		for(int j=i-1; j>=0 && a[j+1]<a[j]; j--)
			swap(a[j],a[j+1]);
	}
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	insertion_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

五. 選擇排序

算法思想

選擇排序是一種簡單直觀的排序算法,無論什麼數據進去都是 O(n²) 的時間複雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間了吧。

算法步驟

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
重複第二步,直到所有元素均排序完畢。

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void select_sort(int a[],int len){
	for(int i=0; i<len; i++) 
		for(int j=i+1; j<len; j++) 
			if(a[j]<a[i]) swap(a[j],a[i]);
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	select_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void select_sort(int a[],int len){
	for(int i=0; i<len; i++) {
		int x=i;
		for(int j=i+1; j<len; j++) 
			if(a[x]>a[j]) x=j;
		if(i!=x) swap(a[x],a[i]);
	}
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	select_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

六. 堆排序

算法思想

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

  • 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列
  • 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列

堆排序的平均時間複雜度爲 Ο(nlogn),利用堆的特性,其實我們可以很方便的得到一個未排序數組中的Top K元素。

算法步驟

1.創建一個堆 H[0……n-1];
2.把堆首(最大值)和堆尾互換;
3.把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置
4.重複步驟 2,直到堆的尺寸爲 1。

動圖演示

代碼實現

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define RI                 register int
#define re(i,a,b)          for(RI i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(a,b)           (((a)>(b)) ? (a):(b))
#define MIN(a,b)           (((a)<(b)) ? (a):(b))

using namespace std;

typedef long long LL;

const int N=1e6+5;

int n;
int a[N];

void max_heapify(int a[],int start,int end) {
	//建立父節點指標和子節點指標
	int dad=start;
	int son=dad<<1+1;
	while(son<=end) { 
		//若子節點指標在範圍內才做比較
		if(son+1<=end && a[son]<a[son+1]) son++;
		//先比較兩個子節點大小,選擇最大的
		if(a[dad]>a[son]) return;//如果父節點大於子節點代表調整完畢,直接跳出函數
			else { //否則交換父子內容再繼續子節點和孫節點比較
				swap(a[dad],a[son]);
				dad=son;
				son=dad<<1+1;
			}
	}
}
 
void heap_sort(int a[],int len) {
	//初始化,i從最後一個父節點開始調整
	for(int i=len>>1-1; i>=0; i--)
		max_heapify(a,i,len-1);
	//先將第一個元素和已經排好的元素前一位做交換,再從新調整(剛調整的元素之前的元素),直到排序完畢
	for(int i=len-1; i>0; i--) {
		swap(a[0],a[i]);
		max_heapify(a,0,i-1);
	}
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	heap_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
    printf("\n");
	return 0;
}

七. 歸併排序

算法思想

歸併排序(Merge sort)是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。作爲一種典型的分而治之思想的算法應用,歸併排序的實現由兩種方法:

自上而下的遞歸(所有遞歸的方法都可以用迭代重寫,所以就有了第 2 種方法);
自下而上的迭代;

算法步驟

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

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000],a1[1000];

void Merge(int a[],int a1[],int l,int mid,int r) {
    int i=l,j=mid+1,k=l;
    while(i<=mid && j<=r) {
        if(a[i]<=a[j]) a1[k++]=a[i++];
        	else a1[k++]=a[j++];
    }
    if(i<=mid) while(i<=mid) a1[k++]=a[i++];
    	else while(j<=r) a1[k++]=a[j++];
    for(int i=l; i<=r; i++)
        a[i]=a1[i];
}

void merge_sort(int a[],int a1[],int l,int r) {
    if(l<r) {
        int mid=(l+r)>>1;
        merge_sort(a,a1,l,mid);
        merge_sort(a,a1,mid+1, r);
        Merge(a,a1,l,mid,r);
    }
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	merge_sort(a,a1,0,n-1);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

多路歸併排序的思路參考上述代碼,無非就是多了幾個有序數組合並而已。

非比較類排序算法

一. 計數排序

算法思想

計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。當輸入的元素是 n 個 0 到 k 之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序0到100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。
通俗地理解,例如有 10 個年齡不同的人,統計出有 8 個人的年齡比 A 小,那 A 的年齡就排在第 9 位,用這個方法可以得到其他每個人的位置,也就排好了序。當然,年齡有重複時需要特殊處理(保證穩定性),這就是爲什麼最後要反向填充目標數組,以及將每個數字的統計減去 1 的原因。

算法步驟

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

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n,k=1e5;
int a[100000],c[100000],ans[100000];

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) {
		scanf("%d",&a[i]);
		c[a[i]]++;
	}
	for(int i=1; i<=k; i++) c[i]+=c[i-1];
	for(int i=n-1; i>=0; i--) ans[--c[a[i]]]=a[i];
	for(int i=0; i<n; i++) printf("%d ",ans[i]);
	printf("\n");
    return 0;
}

二. 桶排序

算法思想

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

在額外空間充足的情況下,儘量增大桶的數量
使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中
同時,對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。

1.什麼時候最快
當輸入的數據可以均勻的分配到每一個桶中。

2.什麼時候最慢
當輸入的數據被分配到了同一個桶中。

示意圖

元素分佈在桶中:
在這裏插入圖片描述
然後,元素在每個桶中排序:
在這裏插入圖片描述

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n,k=1e5;
int c[100000];

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) {
		int x;
		scanf("%d",&x);
		c[x]++;
	}
	for(int i=0; i<=k; i++) {
		while(c[i]>0) printf("%d ",i),c[i]--;
	}
	printf("\n");
    return 0;
}

三. 基數排序

算法思想

基數排序是一種非比較型整數排序算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

基數排序 vs 計數排序 vs 桶排序

基數排序有兩種方法:

這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

基數排序:根據鍵值的每位數字來分配桶;
計數排序:每個桶只存儲單一鍵值;
桶排序:每個桶存儲一定範圍的數值;

動圖演示

代碼實現

#include <bits/stdc++.h>
using namespace std;

int n;
int a[1000];

void radix_sort(int a[],int size) {   
    int n;  
    for(int i=1; i<=100; i=i*10) {
        int tmp[1000][10];
        memset(tmp,0,sizeof(tmp));
        //建立一個1000行,10列的數組,每一列分別代表0~9位數,1000行代表能存放的總個數
        for(int j=0;j<size;j++) {
            n=(a[j]/i)%10;
            tmp[j][n]=a[j];
        }
        int k=0;
        for(int p=0; p<10; p++)
            for(int q=0; q<size; q++) {
                if(tmp[q][p]!=0)
                    a[k++]=tmp[q][p];
            }
    }
}

int main() {
	scanf("%d",&n);
	for(int i=0; i<n; i++) scanf("%d",&a[i]);
	radix_sort(a,n);
	for(int i=0; i<n; i++) printf("%d ",a[i]);
	printf("\n");
    return 0;
}

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