十大排序方法

#三玖天下第一#發表於2020-07-06

排序演算法

1. 氣泡排序

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

1.1 演算法描述

  • n個元素的序列,經過n-1趟選擇排序得到有序結果。具體演算法描述如下:
  1. 初始狀態:無序區為R[1..n],有序區為空;
  2. 每一輪從無序區起點開始,相鄰元素兩兩比較,如果前面的比後面的元素大就交換,直到無序區最後。
  3. 針對所有的元素重複以上的步驟,每一輪冒泡操作無序區域元素減一,有序區元素減一;
  4. 重複執行n-1輪,序列變為有序。

1.2 動圖演示

img

1.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Bubble_sort(int a[]){
	for(int i=1;i<n;++i){//經過n-1輪的冒泡操作
		for(int j=1;j<=n-i;++j)//沒操作一輪,待排序的少一個
			if(a[j]>a[j+1])
				std::swap(a[j],a[j+1]);
	}
}
void Solve(){
	Read();
	Bubble_sort(a);
	Print();
}
int main(){
	Solve();
	return 0;
}

2.選擇排序

  • 選擇排序(Selection-sort)是一種簡單直觀的排序演算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

2.1 演算法描述

  • n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。具體演算法描述如下:
    1. 初始狀態:無序區為R[1..n],有序區為空;
    2. i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別為R[1..i-1]R(i..n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]R[i+1..n)分別變為記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
    3. n-1趟結束,陣列有序化了。

2.2 動圖演示

2.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Select_sort(int a[]){
	int k=0;
	for(int i=1;i<n;++i){//n-1輪,每一輪找到一個最小的
		k=i;//k指向未排序的最小
		for(int j=i+1;j<=n;++j)
			if(a[k]>a[j])
				k=j;
		std::swap(a[i],a[k]);
	}
}
void Solve(){
	Read();
	Select_sort(a);
	Print();
}
int main(){
	Solve();
	return 0;
}

3. 插入排序

  • 插入排序(Insertion-Sort)的演算法描述是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。

3.1 演算法描述

  1. 從第一個元素開始,該元素可以認為已經被排序;
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描;
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置;
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  5. 將新元素插入到該位置後;
  6. 重複步驟2~5

3.2 動圖演示

3.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Insert_sort(int a[]){
	for(int i=2;i<=n;++i){
		for(int j=i;j>1 && a[j-1]>a[j];--j)
			std::swap(a[j],a[j-1]);
	}
}
void Solve(){
	Read();
	Insert_sort(a);
	Print();
}
int main(){
	Solve();
	return 0;
}

4. Shell排序

  • 1959Shell發明,第一個突破 \(O(n^2)\) 的排序演算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序

4.1 演算法描述

  1. 選擇一個較大的增量,一般選gap=n/2,把下標為i,i+gap,i+2*gap+...+i+n/gap*gap 分為一組,對同組的元素進行插入排序。
  2. 減小增量為上一個增量的一半,繼續操作1
  3. 重複以上操作,直到增量為1,此時序列變為有序。

4.2 動圖演示

4.3 程式碼演示

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Shell_sort(){
	for(int d=n/2;d>0;d>>=1){//增量選擇,逐漸縮小為原來一半
		for(int i=d+1;i<=n;++i){//對同組元素進行插入排序
			for(int j=i;j-d>0 && a[j]<a[j-d];j-=d)
				std::swap(a[j],a[j-d]);
		}
	}
}
void Solve(){
	Read();
	Shell_sort();
	Print();
}
int main(){
	Solve();
	return 0;
}

4.4 時間效率

  • 希爾排序中對於增量序列的選擇十分重要,直接影響到希爾排序的效能。我們上面選擇的增量序列{n/2,(n/2)/2...1}(希爾增量),其最壞時間複雜度依然為\(O(n^2)\),一些經過優化的增量序列如Hibbard經過複雜證明可使得最壞時間複雜度為\(O(n^{3/2})\)

5. 歸併排序

  • 歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。

5.1 演算法描述

  1. 把長度為n的輸入序列分成兩個長度為n/2的子序列;
  2. 對這兩個子序列分別採用歸併排序;
  3. 將兩個排序好的子序列合併成一個最終的排序序列。

5.2 動圖演示

5.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Merge(int l,int mid,int r){//合併操作
	int i=l,j=mid+1,k=0;//i指向前面區間第一個元素,j指向後面區間第一元素
	int b[r-l+2]={0};//b儲存合併的序列
	while(i<=mid && j<=r){//取兩個序列前面的較小者
		if(a[i]<=a[j])b[++k]=a[i++];
		else b[++k]=a[j++];
	}//跳出迴圈兩個序列中有一個為空
	while(i<=mid)//若比較完之後,第一個有序區仍有剩餘
		b[++k]=a[i++];
	while(j<=r)//若比較完之後,第二個有序區仍有剩餘
		b[++k]=a[j++];
	for(i=l,k=1;i<=r;++i,++k)//把合併後的排好序的序列拷貝到陣列a[l,r]
		a[i]=b[k];
}
void Merge_sort(int l,int r){
	if(l<r){//把區間分成兩部分
		int mid=l+(r-l)/2;
		Merge_sort(l,mid);//遞迴左區間
		Merge_sort(mid+1,r);//遞迴右區間
		Merge(l,mid,r);//合併左右兩區間
	}
}
void Solve(){
	Read();
	Merge_sort(1,n);
	Print();
}
int main(){
	Solve();
	return 0;
}

5.4 時間效率

  • 歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的效能不受輸入資料的影響,但表現比選擇排序好的多,因為始終都是\(O(nlogn)\)的時間複雜度。代價是需要額外的記憶體空間。

6. 快速排序

  • 快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。

6.1 演算法描述

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

6.2 動圖演示

6.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
void Quick_sort(int l,int r){
	if(l>=r)return;
	int i=l,j=r,base=a[l];//選左邊界作為基準
	while(i<j){//作指標要小於右指標
		while(i<j && a[j]>base)--j;//先遍歷右邊界
		while(i<j && a[i]<base)++i; 
		if(i<j)std::swap(a[i],a[j]);
	}//跳出迴圈時i==j,此位置為基準最終位置
	std::swap(a[l],a[i]);
	Quick_sort(l,i-1);
	Quick_sort(i+1,r);
}
void Solve(){
	Read();
	Quick_sort(1,n);
	Print();
}
int main(){
	Solve();
	return 0;
}

7. 堆排序

  • 堆排序 (Heapsort) 是指利用堆這種資料結構所設計的一種排序演算法。
  • 堆是一個近似完全二叉樹的結構,並同時滿足堆的性質:即子結點的鍵值或索引總是小於(或者大於)它的父結點。
  • 堆排序可以說是一種利用堆的概念來排序的選擇排序。分為兩種方法:
    1. 大頂堆:每個結點的值都大於或等於其子結點的值,在堆排序演算法中用於升序排列;
    2. 小頂堆:每個結點的值都小於或等於其子結點的值,在堆排序演算法中用於降序排列;
  • 堆排序的平均時間複雜度為 Ο(nlogn)

7.1 演算法描述:

  1. 建立一個堆
  2. 把堆首(最大值)和堆尾互換;
  3. 堆的大小減一,並向下調整堆使之滿足堆的性質
  4. 重複2,3直到只剩一個元素。

7.2 動圖演示

7.3 程式碼實現

#include <cstdio>
#include <cstring>
const int maxn = 10000 + 5;
void swap(int &x,int &y){int t=x;x=y;y=t;}//交換函式
int Heap[maxn],siz=0;
void Push(int x){//向上調整
    Heap[++siz]=x;//把插入的元素x放在陣列最後
    for(int i=siz;i/2>0 && Heap[i]>Heap[i/2];i=i/2)
        swap(Heap[i],Heap[i/2]);
}
void Pop(){//向下調整
    swap(Heap[siz],Heap[1]);siz--;//交換堆頂和堆底,然後直接彈掉堆底
    for(int i=1;2*i<=siz;i*=2){
        int j=2*i;//如果存在右兒子且右兒子大於左兒子j就指向右兒子
        if(j+1<=siz && Heap[j]<Heap[j+1])++j;
        if(Heap[i]<Heap[j])swap(Heap[i],Heap[j]);
        else break;
    }
}
void Solve(){
    int n;scanf("%d",&n);
    for(int i=1;i<=n;++i){//建堆
        int x;scanf("%d",&x);
        Push(x);
    }
    for(int i=1;i<=n;++i){//輸出堆頂並刪除,此乃降序
        printf("%d ",Heap[1]);Pop();
    }
    printf("\n");
    for(int i=1;i<=n;++i)//全部出堆後原陣列為升序
        printf("%d ",Heap[i]);
}
int main(){
    Solve();
    return 0;
}

8. 計數排序

  • 計數排序不是基於比較的排序演算法,其核心在於將輸入的資料值轉化為儲存在額外開闢的陣列空間中。 作為一種線性時間複雜度的排序,計數排序要求輸入的資料必須是有確定範圍的整數

8.1 演算法描述

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

8.2 動圖演示

8.3 程式碼實現

#include <bits/stdc++.h>
const int maxn=1e5+5,Inf=2147483647;
int n,a[maxn],b[maxn];
void Read(){
    scanf("%d",&n);
    srand(time(0));
    for(int i=1;i<=n;++i)
        a[i]=rand()%100000;
}
void Print(){
    for(int i=1;i<=n;++i)
        printf("%d ",a[i]);
}
void Counting_sort(){
    int Min=Inf,Max=-Inf;
    for(int i=1;i<=n;++i){
        b[a[i]]++;//值當下標,並計算個數
        Min=std::min(Min,a[i]);//
        Max=std::max(Max,a[i]);
    }
    int cnt=0;
    for(int i=Min;i<=Max;++i)
        while(b[i])a[++cnt]=i,b[i]--;
}
void Solve(){
    Read();
    Counting_sort();
    Print();
}
int main(){
    Solve();
    return 0;
}

9. 桶排序

  • 桶排序是將待排序集合中處於同一個值域的元素存入同一個桶中,也就是根據元素值特性將集合拆分為多個區域,則拆分後形成的多個桶,從值域上看是處於有序狀態的。對每個桶中元素進行排序,則所有桶中元素構成的集合是已排序的。

9.1 演算法描述

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

9.2 動圖演示

9.3 時間效率

  • 桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間資料進行排序的時間複雜度,因為其它部分的時間複雜度都為O(n)。很顯然,桶劃分的越小,各個桶之間的資料越少,排序所用的時間也會越少。但相應的空間消耗就會增大。

10. 基數排序

  • 基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。

10.1 演算法描述

  1. 取得陣列中的最大數,並取得位數;
  2. arr為原始陣列,從最低位開始取每個位組成radix陣列;
  3. radix進行計數排序(利用計數排序適用於小範圍數的特點);

10.2 動圖演示

程式碼實現

#include <bits/stdc++.h>
const int maxn=1e4+5,Inf=2147483647;
int a[maxn];
int n;
void Read(){
	scanf("%d",&n);
	srand(time(0));
	for(int i=1;i<=n;++i)
		a[i]=rand()%10000;
}
void Print(){
	for(int i=1;i<=n;++i)
		printf("%d ",a[i]);
}
int Get_max(int a[],int n){//求陣列的最大值
	int Max=a[1];
	for(int i=1;i<=n;++i)
		Max=std::max(Max,a[i]);
	return Max;
}
/*
 * 引數說明:
 *     a -- 陣列
 *     n -- 陣列長度
 *     exp -- 指數。對陣列a按照該指數進行排序。
 *
 * 例如,對於陣列a={50, 3, 542, 745, 2014, 154, 63, 616};
 *    (01) 當exp=1表示按照"個位"對陣列a進行排序
 *    (02) 當exp=10表示按照"十位"對陣列a進行排序
 *    (03) 當exp=100表示按照"百位"對陣列a進行排序
 *    ...
 */
void Count_sort(int a[],int n,int exp){
	int b[n+5],buckets[10]={0};
    // b儲存"被排序資料"的臨時陣列
	for(int i=1;i<=n;++i)// 將資料出現的次數儲存在buckets[]中
		buckets[(a[i]/exp)%10]++;
	for(int i=1;i<10;++i)// 更改buckets[i]。目的是讓更改後的buckets[i]的值,是該資料在b[]中的位置。
		buckets[i]+=buckets[i-1];
	for(int i=n;i>0;--i){// 將資料儲存到臨時陣列b[]中
		b[buckets[(a[i]/exp)%10]]=a[i];
		buckets[(a[i]/exp)%10]--;
	}
	for(int i=0;i<=n;++i)// 將排序好的資料賦值給a[]
		a[i]=b[i];
}
void Radix_sort(int a[],int n){
	int Max=Get_max(a,n);// 陣列a中的最大值
	for(int i=1;Max/i>0;i*=10)// 從個位開始,對陣列a按"指數"進行排序
		Count_sort(a,n,i);
}
void Solve(){
	Read();
	Radix_sort(a,n);
	Print();
}
int main(){
	Solve();
	return 0;
}
摘自奧賽教練老姚的部落格

相關文章