各種常用的排序算法
0、算法概述
0.1 算法分類
十種常見排序算法可以分爲兩大類:
- 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
- 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
0.2 算法複雜度
0.3 相關概念
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
- 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 空間複雜度:是指算法在計算機
內執行時所需存儲空間的度量,它也是數據規模n的函數。
1、冒泡排序(Bubble Sort)
冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因爲越小的元素會經由交換慢慢“浮”到數列的頂端。
1.1 算法描述
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
1.2 動圖演示
1.3 代碼實現
C/C++實現:
//冒泡排序
void bubble_sort(int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
int i,j,tem;
for (i=0; i<n; i++)
for (j=0; j<n-i; j++) //如果左邊大於右邊則將他們調換位置
if (a[j] > a[j+1])tem=a[j],a[j]=a[j+1],a[j+1]=tem;
}
Java實現:
//冒泡排序算法
private static void bubble_sort(int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
for (int i=0; i<n; i++)
for (int j=0; j<n-i; j++) //如果左邊大於右邊則將他們調換位置
if (a[j] > a[j+1]) {
int tem=a[j];a[j]=a[j+1];a[j+1]=tem;
}
}
2、選擇排序(Selection Sort)
選擇排序(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 代碼實現
C/C++實現:
//選擇排序
void selection_sort (int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
for(int i=0;i<=n;i++)
for(int j=i+1;j<=n;j++)
if(a[i]>a[j]){int tem=a[i];a[i]=a[j];a[j]=tem;}
}
Java 實現:
//選擇排序
public static void selection_sort (int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
for(int i=0;i<=n;i++) //每次選出第i小的數
for(int j=i+1;j<=n;j++)//與後面沒排序的依次比較 選出當前最小的數
if(a[i]>a[j]){int tem=a[i];a[i]=a[j];a[j]=tem;}
}
2.4 算法分析
表現最穩定的排序算法之一,因爲無論什麼數據進去都是O(n2)的時間複雜度,所以用到它的時候,數據規模越小越好。唯一的好處可能就是不佔用額外的內存空間了吧。理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。
3、插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
3.1 算法描述
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:
- 從第一個元素開始,該元素可以認爲已經被排序;
- 取出下一個元素,在已經排序的元素序列中從後向前掃描;
- 如果該元素(已排序)大於新元素,將該元素移到下一位置;
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- 將新元素插入到該位置後;
- 重複步驟2~5。
3.2 動圖演示
3.2 代碼實現
C/C++ 實現
//插入排序
void insertion_sort (int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
int i,j,tem;//tem 中間臨時變量
for (i=1; i<=n; i++) {
//當前i與前(i-1)個做比較如果當前i元素小於前(i-1)個,則第(i-1)個向挪一位
for (tem=a[i],j=i-1;j>=0&&tem<a[j];j--)a[j+1]=a[j];
a[j+1]=tem;//確定當前i應該到的位置
}
}
Java 實現
//插入排序算法:
private static void insertion_sort (int a[], int n) {//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
int i,j,tem;//中間臨時變量
for (i=1; i<=n; i++) {
//當前i與前(i-1)個做比較如果當前i元素小於前(i-1)個,則第(i-1)個向挪一位
for (tem=a[i],j=i-1;j>=0&&tem<a[j];j--)a[j+1]=a[j];
a[j+1]=tem;//確定當前i應該到的位置
}
}
3.4 算法分析
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
4、希爾排序(Shell Sort)
1959年Shell發明,第一個突破O(n2)的排序算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
4.1 算法描述
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
4.2 動圖演示
4.3 代碼實現
C/C++ 實現:
//希爾排序
void shell_sort(int a[],int n){//n爲a[]的實際長度-1,例如a[4]={3,2,9,10} n爲3
for(int d=n/2;d>0;d/=2)//d 爲增量
for(int i=d;i<=n;i++){
int j=i;
int tem=a[j];//j-d 代表 當前j的前一個元素(當前同一個分組)
while(j-d>=0&&a[j-d]>tem) a[j]=a[j-d],j=j-d;// 插入排序的方法
a[j]=tem;
}
}
Java實現:
//希爾排序
private static void shell_sort(int a[],int n){
for(int d=n/2;d>0;d/=2)//d 爲增量
for(int i=d;i<=n;i++){
int j=i;
int tem=a[j];//j-d 代表 當前j的前一個元素(當前同一個分組)
while(j-d>=0&&a[j-d]>tem) {a[j]=a[j-d];j=j-d;}// 插入排序的方法
a[j]=tem;
}
}
4.4 算法分析
希爾排序的核心在於間隔序列的設定。既可以提前設定好間隔序列,也可以動態的定義間隔序列。動態定義間隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
5、歸併排序(Merge Sort)
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
5.1 算法描述
- 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
- 對這兩個子序列分別採用歸併排序;
- 將兩個排序好的子序列合併成一個最終的排序序列。
5.2 動圖演示
5.3 代碼實現
C/C++實現:
//歸併排序
void merge_core(int a[],int l,int mid,int r){//合併數組
int k=0,i=l,j=mid+1;
int *tem=(int*)malloc(sizeof(int)*(r-l+1));//開臨時中間數組
// 執行合併 誰小誰先進tem 兩個組比大小
while(i<=mid||j<=r)tem[k++]=((a[i]<a[j]&&i<=mid)||j>r)?a[i++]:a[j++];
while(k--)a[r--]=tem[k]; free(tem);//清除空間
}
void merge_sort(int a[],int l,int r){
if(r>l){
int mid=(l+r)>>1;//取中間 一分爲二
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
merge_core(a,l,mid,r);//合併
}
}
Java實現:
//歸併排序
private static void mergeSort(int a[],int l,int r){
if(r>l){
int mid=(l+r)>>1;//取中間 一分爲二
mergeSort(a,l,mid);
mergeSort(a,mid+1,r);
mergeCore(a,l,mid,r);//合併
}
}
private static void mergeCore(int a[],int l,int mid,int r){//合併數組
int k=0,i=l,j=mid+1;
int tem[]=new int[r-l+2];//開臨時中間數組
// 執行合併 誰小誰先進 兩個組比大小
while(i<=mid||j<=r)tem[k++]=(j>r||(i<=mid&&a[i]<a[j]))?a[i++]:a[j++];
while(k!=0)a[r--]=tem[--k];
}
5.4 算法分析
歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是O(nlogn)的時間複雜度。代價是需要額外的內存空間。
6、快速排序(Quick Sort)
快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
6.1 算法描述
快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述如下:
- 從數列中挑出一個元素,稱爲 “基準”(pivot);
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作;
- 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
6.2 動圖演示
6.3 代碼實現
C/C++實現:
int mp(int a[], int l, int r) {
int p = a[r];
while (l<r) {
while (l<r && p>a[l])l++;//從左向右尋找比標準p大的數
if(l<r) a[r--]=a[l];//找到了就放到最右邊,比標準p大就應該在右邊
while (l<r && p<a[r])r--;//從右向左尋找比標準p小的數
if(l<r) a[l++]=a[r];//找到就放到最左邊,比標準p大就應該在左邊
}
a[r]=p;
return r;
}
//快速排序
void quick_sort (int a[], int l, int r) {//r爲a[]的實際長度-1,例如a[4]={3,2,9,10} l爲0 r爲3
if (l < r) {
int q = mp(a, l, r);//找到標準應該在的位置
quick_sort(a, l, q-1);//根據標準的位置分爲左邊部分
quick_sort(a, q+1, r);//根據標準的位置分爲右邊部分
}
}
Java實現:
//快速排序
private static void quickSort(int[] a, int l, int r) {//r爲a[]的實際長度-1,例如a[4]={3,2,9,10} l爲0 r爲3
if(l<r) {
int p=mpSort(a,l,r);//找到標準應該在的位置
quickSort(a,l,p-1);//根據標準的位置分爲左邊部分
quickSort(a,p+1,r);//根據標準的位置分爲右邊部分
}
}
private static int mpSort(int[] a, int l, int r) {
int p=a[r];
while(l<r) {
while(l<r&&p>a[l])l++;//從左向右尋找比標準p大的數
if(l<r)a[r--]=a[l];//找到了就放到最右邊,比標準p大就應該在右邊
while(l<r&&p<a[r])r--;//從右向左尋找比標準p小的數
if(l<r)a[l++]=a[r];//找到就放到最左邊,比標準p大就應該在左邊
} a[l]=p;
return l;
}
7、堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
7.1 算法描述
- 將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區;
- 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
- 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整爲新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。
7.2 動圖演示
7.3 代碼實現
C/C++ 實現:
//大頂堆
void max_heapify(int a[], int l, int r) {
int f= l;//建立父節點指標和子節點指標
int s= f*2+1;
while (s <=r) { //若子節點指標在範圍內才做比較
if (s+1<=r&&a[s]<a[s+1])s++;//先比較兩個子節點大小,選擇最大的
if (a[f]<a[s]){//如果父節點小於子節點代表需調整,交換父子內容再繼續子節點和孫節點比較
int tem=a[f];a[f]=a[s];a[s]=tem;
f=s;
s=f*2+1;//交換父子內容再繼續子節點和孫節點比較
}else return;//調整好了直接跳出
}
}
//堆排序
void heap_sort(int a[], int n) {
for (int i=n/2; i>= 0; i--)max_heapify(a,i,n);//i從最後一個父節點開始調整
//先將第一個元素和已排好元素前一位做交換,再重新調整,直到排序完畢
for (int i=n; i>0;i--) {
int tem=a[0];a[0]=a[i];a[i]=tem;
max_heapify(a,0,i-1);
}
}
Java 實現:
//堆排序
private static void heapSort(int a[], int n) {
for (int i=n/2; i>= 0; i--)maxHeapify(a,i,n);//i從最後一個父節點開始調整
//先將第一個元素和已排好元素前一位做交換,再重新調整,直到排序完畢
for (int i=n; i>0;i--) {
int tem=a[0];a[0]=a[i];a[i]=tem;
maxHeapify(a,0,i-1);
}
}
//大頂堆
private static void maxHeapify(int a[], int l, int r) {
int f= l;//建立父節點指標和子節點指標
int s= f*2+1;
while (s <=r) { //若子節點指標在範圍內才做比較
if (s+1<=r&&a[s]<a[s+1])s++;//先比較兩個子節點大小,選擇最大的
if (a[f]<a[s]){//如果父節點小於子節點代表需調整,交換父子內容再繼續子節點和孫節點比較
int tem=a[f];a[f]=a[s];a[s]=tem;
f=s;
s=f*2+1;//交換父子內容再繼續子節點和孫節點比較
}else return;//調整好了直接跳出
}
}
8、計數排序(Counting Sort)
計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。
8.1 算法描述
- 找出待排序的數組中最大和最小的元素;
- 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項;
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
- 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1。
8.2 動圖演示
8.3 代碼實現
C/C++實現:
//計數排序
void counting_sort(int a[],int n){
int max_num=0,k=0; //獲取最大值
for(int i=1;i<=n;i++)if(a[i]>a[max_num])max_num=i;max_num=a[max_num];
int count[max_num+1]; memset(count,0,sizeof(count));//初始化計數數組
for(int i=0;i<=n;i++)count[a[i]]++;//開始計數
for(int i=0;i<=max_num;i++){ //排序
while(count[i]--)a[k++]=i;//到正確的位置
}
}
Java 實現:
private static void counting_sort(int a[],int n){
int max_num=0,k=0; //獲取最大值
for(int i=1;i<=n;i++)if(a[i]>a[max_num])max_num=i;
max_num=a[max_num];
int []count=new int[max_num+1]; //初始化計數數組
for(int i=0;i<=n;i++)count[a[i]]++;//開始計數
for(int i=0;i<=max_num;i++){ //排序
while(0<count[i]--)a[k++]=i;//到正確的位置
}
}
8.4 算法分析
計數排序是一個穩定的排序算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k),空間複雜度也是O(n+k),其排序速度快於任何比較排序算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法。
9、桶排序(Bucket Sort)
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的確定。桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排)。
9.1 算法描述
- 設置一個定量的數組當作空桶;
- 遍歷輸入數據,並且把數據一個一個放到對應的桶裏去;
- 對每個不是空的桶進行排序;
- 從不是空的桶裏把排好序的數據拼接起來。
9.2 動圖演示
9.3 代碼實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
9.4 算法分析
桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
10、基數排序(Radix Sort)
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。
10.1 算法描述
- 取得數組中的最大數,並取得位數;
- arr爲原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
10.2 動圖演示
10.3 代碼實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
10.4 算法分析
基數排序基於分別排序,分別收集,所以是穩定的。但基數排序的性能比桶排序要略差,每一次關鍵字的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的關鍵字序列又需要O(n)的時間複雜度。假如待排數據可以分爲d個關鍵字,則基數排序的時間複雜度將是O(d*2n) ,當然d要遠遠小於n,因此基本上還是線性級別的。
基數排序的空間複雜度爲O(n+k),其中k爲桶的數量。一般來說n>>k,因此額外空間需要大概n個左右。