排序演算法
1. 氣泡排序
- 氣泡排序是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
1.1 演算法描述
n
個元素的序列,經過n-1
趟選擇排序得到有序結果。具體演算法描述如下:
- 初始狀態:無序區為
R[1..n]
,有序區為空;
- 每一輪從無序區起點開始,相鄰元素兩兩比較,如果前面的比後面的元素大就交換,直到無序區最後。
- 針對所有的元素重複以上的步驟,每一輪冒泡操作無序區域元素減一,有序區元素減一;
- 重複執行
n-1
輪,序列變為有序。
1.2 動圖演示
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
趟直接選擇排序得到有序結果。具體演算法描述如下:
- 初始狀態:無序區為
R[1..n]
,有序區為空;
- 第
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
個的新無序區;
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 演算法描述
- 從第一個元素開始,該元素可以認為已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟
3
,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟
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
排序
1959
年Shell
發明,第一個突破 \(O(n^2)\) 的排序演算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
4.1 演算法描述
- 選擇一個較大的增量,一般選
gap=n/2
,把下標為i,i+gap,i+2*gap+...+i+n/gap*gap
分為一組,對同組的元素進行插入排序。
- 減小增量為上一個增量的一半,繼續操作
1
。
- 重複以上操作,直到增量為
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 演算法描述
- 把長度為n的輸入序列分成兩個長度為n/2的子序列;
- 對這兩個子序列分別採用歸併排序;
- 將兩個排序好的子序列合併成一個最終的排序序列。
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 演算法描述
- 從數列中挑出一個元素,稱為 基準(
pivot
);
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(
partition
)操作;
- 遞迴地(
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
) 是指利用堆這種資料結構所設計的一種排序演算法。
- 堆是一個近似完全二叉樹的結構,並同時滿足堆的性質:即子結點的鍵值或索引總是小於(或者大於)它的父結點。
- 堆排序可以說是一種利用堆的概念來排序的選擇排序。分為兩種方法:
- 大頂堆:每個結點的值都大於或等於其子結點的值,在堆排序演算法中用於升序排列;
- 小頂堆:每個結點的值都小於或等於其子結點的值,在堆排序演算法中用於降序排列;
- 堆排序的平均時間複雜度為
Ο(nlogn)
。
7.1 演算法描述:
- 建立一個堆
- 把堆首(最大值)和堆尾互換;
- 堆的大小減一,並向下調整堆使之滿足堆的性質
- 重複
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 演算法描述
- 找出待排序的陣列中最大和最小的元素(作為陣列的範圍);
- 統計陣列中每個值為
i
的元素出現的次數,存入陣列C
的第i
項;
- 對所有的計數累加(從
C
中的第一個元素開始,每一項和前一項相加);
- 反向填充目標陣列:將每個元素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 演算法描述
- 設定一個定量的陣列當作空桶子。
- 尋訪序列,並且把專案一個一個放到對應的桶子去。
- 對每個不是空的桶子進行排序。
- 從不是空的桶子裡把專案再放回原來的序列中。
9.2 動圖演示
9.3 時間效率
- 桶排序最好情況下使用線性時間
O(n)
,桶排序的時間複雜度,取決與對各個桶之間資料進行排序的時間複雜度,因為其它部分的時間複雜度都為O(n)
。很顯然,桶劃分的越小,各個桶之間的資料越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
10. 基數排序
- 基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。
10.1 演算法描述
- 取得陣列中的最大數,並取得位數;
arr
為原始陣列,從最低位開始取每個位組成radix
陣列;
- 對
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;
}