排序算法能夠分爲內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納所有的排序記錄,在排序過程當中須要訪問外存。html
常見的內部排序算法有:冒泡排序、選擇排序、插入排序、希爾排序、堆排序、快速排序、歸併排序、基數排序等。git
前面三種是簡單排序,後續算法是在前面基礎上進行優化。本文將基於C語言,依次介紹上述八大排序算法。github
主要思想:依次對兩個數比較大小,較大的數冒起來,較小的數壓下來。算法
形象理解:一隊新兵N我的整齊站成一列,教官想讓他們按照身高排好隊,看起來更協調,因而從前走到後走一趟,每次遇到相鄰的兩我的身高不協調時,就讓兩人互換位置。當走完一趟時,個子最高的人就被排到了最後。教官回到前排後發現隊伍仍然不協調,因而又按照原樣走了一趟。這樣循環走了N-1趟以後,教官終於滿意了。(注意:每次走一趟時,以前排到後面的高個子就不參與此次排序了;有時候可能還沒走完N-1趟,教官就發現隊伍已經協調了,因而排序結束。)數組
特色:簡單易懂,排序穩定,但速度慢。數據結構
主要思想:針對冒泡排序,有一個地方能夠優化,即在跑一趟的過程當中,不必兩兩交換,能夠先記下最小值,跑完一趟後直接將最小值換到前面。函數
特色:比冒泡更快一些,但代價是跳躍性交換,排序不穩定。測試
#include <stdio.h> #include <stdlib.h> void Swap_Two(int *p1, int *p2); //交換兩個整數 void Select_Sort(int A[], int N); //選擇排序 //主函數,其他相似 int main() { int N, i; int *p = NULL; //將N個整數存儲至數組中 scanf("%d", &N); //讀入個數N p = (int *)malloc(sizeof(int) * N); //申請數組 for(i=0; i<N; i++){ scanf("%d", &p[i]); } //排序並打印 Select_Sort(p, N); for(i=0; i<N; i++){ printf("%d", p[i]); if(i != N-1) printf(" "); } free(p); return 0; } void Swap_Two(int *p1, int *p2){ //交換兩個長整型數 int temp; temp = *p1; *p1 = *p2; *p2 = temp; return; } void Select_Sort(int A[], int N){ //選擇排序 int i, j, min_idx; for(i=0; i<N-1; i++){ min_idx = i; //初始化最小值索引 for(j=i+1; j<N; j++){ if(A[j] < A[min_idx]) //若待排序列中有比當前最小值還小的,則更新最小值索引 min_idx = j; } if(min_idx != i) //若更新過最小值索引 Swap_Two(&A[i], &A[min_idx]); } return; }
主要思想:過程跟拿牌同樣,依次拿N張牌,每次拿到到牌後,從後往前看,遇到合適位置就插進去。最終手上的牌從小到大。大數據
特色:當數據規模較小或者數據基本有序時,效率較高。優化
void Swap_Two(int *p1, int *p2){ //交換兩個整數 int temp; temp = *p1; *p1 = *p2; *p2 = temp; return; } void Insert_Sort(int A[], int N){ //直插排序 int temp, i, k; for(i=1; i<N; i++){ temp = A[i]; for(k=i-1; k>=0 && temp<A[k]; k--){ A[k+1] = A[k]; } A[k+1] = temp; } return; }
主要思想:設增量序列個數爲k,則進行k輪排序。每一輪中,按照某個增量將數據分割成較小的若干組,每一組內部進行插入排序;各組排序完畢後,減少增量,進行下一輪的內部排序。
特色:針對插入排序的改進,當數據規模較大或無序時也比較高效。精妙之處在於,能夠同時構造出兩個特殊的有利條件(數據量小,基本有序),一個有利條件弱時,另一個有利條件就強。(剛開始時雖然每組有序度低,但其數據量小;隨着每輪的增量逐漸壓縮,雖然各組數據量逐漸變大,但其有序度逐漸增長。)
void Shell_Sort(int A[], int N){ //希爾排序 int k, i, j, p, temp; int t = 0; int D[33]; //假定增量序列不超過2^32 //定義Hibbard增量序列 for(k=1; k<33; k++){ t = 2 * t + 1; //增量序列項 if(t < N){ D[k] = t; }else{ break; } } //進行k-1(增量序列的個數)趟插排 for(p=k-1; p>=1; p--){ for(i=D[p]; i<N; i++){ temp = A[i]; for(j=i-D[p]; j>=0 && temp<A[j]; j-=D[p]){ A[j+D[p]] = A[j]; } A[j+D[p]] = temp; } } return; }
主要思想:將待排數組構建成一個最大堆,將堆頂最大元素換到後面,而後堆容量減1;相似進行N-1次操做便可。
主要思想:分治思想。選一基準元素,依次將剩餘元素中小於該基準元素的值放置其左側,大於等於該基準元素的值放置其右側;而後,取基準元素的前半部分和後半部分分別進行一樣的處理;以此類推,直至各子序列剩餘一個元素時,即排序完成。
注意:對於小規模數據(n<100),快排因爲用了遞歸,其效率可能還不如插排。所以一般能夠定義一個閾值,當遞歸的數據量很小時中止遞歸,直接調用插排。
主要思想:相似兩個有序鏈表的合併,每次兩兩合併相鄰的兩個有序序列,直至整個序列有序。
主要思想:基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。
#define Radix 10 #define MaxDigit 4 typedef struct ENode{ //定義元素結點 int data; struct ENode *next; }*PtrToNode; typedef struct BucketNode{ //定義桶結點結構 PtrToNode head; PtrToNode tail; }*Bucket; void LSDRadix_Sort(int A[], int N){ //基排序 int i, D; //D爲位, d爲第D位上的數字(範圍爲0到9) //定義兩個大小爲Radix的桶,並初始化 Bucket B1 = malloc(Radix * sizeof(struct BucketNode)); Bucket B2 = malloc(Radix * sizeof(struct BucketNode)); for(i=0; i<Radix; i++){ B1[i].head = B1[i].tail = B2[i].head = B2[i].tail = NULL; //注意大bug:B1爲桶指針,可是B1[i]爲結構體 } //將數組中元素按照倒數第D位數字分別掛到桶B1上 D = 1; Transfer_Array_To_Bucket(A, B1, N, D); D++; //開始相互倒騰,每倒騰一趟就往前看一位 while(D <= MaxDigit){ if(D % 2 == 0) Transfer_Bucket_To_Bucket(B1, B2, D); else Transfer_Bucket_To_Bucket(B2, B1, D); D++; } //倒騰結束後將桶中的元素依次倒入數組A中 if(D % 2 == 0) Transfer_Bucket_To_Array(B1, A, N); else Transfer_Bucket_To_Array(B2, A, N); //釋放兩個桶及其元素 Free_Bucket(B1); Free_Bucket(B2); return; } void Transfer_Array_To_Bucket(int A[], Bucket B, int N, int D){ //將數組A中元素按照第D位掛到桶B中 int i, d; PtrToNode temp; for(i=0; i<N; i++){ d = Get_Digit(A[i], D); //獲取A[i]倒數第D位數字 temp = malloc(sizeof(struct ENode)); //建立一個新結點並初始化 temp->data = A[i]; temp->next = NULL; if(B[d].tail == NULL){ //若桶的d位置上爲空 B[d].head = B[d].tail = temp; }else{ B[d].tail->next = temp; B[d].tail = temp; } } return; } void Transfer_Bucket_To_Array(Bucket B, int A[], int N){ //將桶B1中元素依次倒入數組A中,最多不超過N int k, i = 0; PtrToNode p; for(k=0; k<Radix; k++){ p = B[k].head; while(p != NULL){ if(i < N){ A[i++] = p->data; p = p->next; }else{ return; } } } return; } void Transfer_Bucket_To_Bucket(Bucket B1, Bucket B2, int D){ //依次將桶B1中元素按照第D位掛到B2中, int k, d; PtrToNode p, temp; //依次從B1中取下元素結點,根據D位數字掛到B2上 for(k=0; k<Radix; k++){ p = B1[k].head; while(p != NULL){ //從B1中取該元素,並獲取第D位數字 temp = p; p = p->next; d = Get_Digit(temp->data, D); //獲取該結點第D位數字 //將該元素掛到B2上 temp->next = NULL; //確定做爲本次轉移的尾結點 if(B2[d].head == NULL){ //若B2[d]爲空位置 B2[d].head = B2[d].tail = temp; }else{ //若B2[d]不爲空位置,則將temp結點接到尾結點上 B2[d].tail->next = temp; B2[d].tail = temp; } } //取完B1[k]上的全部結點後,設置其首尾結點指針爲空 B1[k].head = B1[k].tail = NULL; } return; } int Get_Digit(int X, int D){ //獲取X倒數第D位數字 int k, i; for(i=0; i<D-1; i++) X = X / Radix; k = X % Radix; return k; }
複雜度與穩定性的比較
算法類別 | 平均時間複雜度 | 最好狀況複雜度 | 最壞狀況複雜度 | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | \(O(n^2)\) | \(O(n)\) | \(O(n^2)\) | \(O(1)\) | 穩定 |
選擇排序 | \(O(n^2)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | 不穩定 |
插入排序 | \(O(n^2)\) | \(O(n)\) | \(O(n^2)\) | \(O(1)\) | 穩定 |
希爾排序 | \(O(n^{1.5})\) | - | - | \(O(1)\) | 不穩定 |
堆排序 | \(O(n log(n))\) | \(O(n log(n))\) | \(O(n log(n))\) | \(O(1)\) | 不穩定 |
快速排序 | \(O(n log(n))\) | \(O(n log(n))\) | \(O(n^2)\) | \(O(1)\) | 不穩定 |
歸併排序 | \(O(n log(n))\) | \(O(n log(n))\) | \(O(n log(n))\) | \(O(n)\) | 穩定 |
基排序 | \(O(d*n)\) | \(O(d*n)\) | \(O(d*n)\) | \(O(n)\) | 穩定 |
如下是基於浙大數據結構課練習題測試(09-排序1 排序 (25 分))。
參考:
https://blog.csdn.net/qq_39207948/article/details/80006224
https://blog.csdn.net/qq_43152052/article/details/100078825
https://www.cnblogs.com/onepixel/articles/7674659.html