一、排序概念
排序稱爲分類,就是把一批任意序列的數據記錄,按關鍵字重新排成一個有序的序列。
排序一般又分爲以下幾種:
(1)穩定排序和不穩定排序
穩定排序:兩個相等的數排序前的位置順序與排序後位置的順序不變。比如排序前:5,2,5。排序後:2,5,5。
不穩定排序:兩個相等的數排序前的位置順序與排序後位置的順序顛倒。比如5,8,5,2,9。選擇排序中:第一個5先跟2發生了交換。
(2)根據存儲設備的不同,分爲內部排序和外部排序
內部排序:數據全部存放在計算機的內存儲器中進行的排序過程,在此期間沒有進行內、外存儲器的數據對象。
外部排序:指待排序記錄的數量很大,以致內存不能依次容納全部記錄,所以排序的過程中,數據的主要部分存在外存儲器上,藉助與內存儲器逐步調整記錄之間的相對位置。在這個過程中,需要不斷地在內、外存儲器之間進行數據地交換。
穩定排序:直接插入排序、基數排序、歸併排序、冒泡排序…
不穩定排序:快速排序、希爾排序、簡單選擇排序、堆排序…
二、排序的分類
1、直接插入排序:
基本思想:每一個待排序的記錄按其排序碼關鍵字的大小插到前面已經排好序的序列。直接插入過程可以理解爲:不斷建立監察哨,不斷更新監察哨的位置
假設一組關鍵字序列爲{48,35,18,45,12,68,33},一共7個記錄
其上就是監察哨地位置變化結果。不斷與監察範圍的元素比較,範圍內的數比監察哨大,就交換。
核心代碼實現:
void insertsort(int a[],int n){///從小到大排序
for(int i=2;i<=n;i++){
a[0]=a[i];///監察哨
int j=i-1;///監察範圍最大值
while(a[0]<a[j]){
a[j+1]=a[j];
j--;
}
a[j+1]=a[0];
}
}///時間複雜度:O(n的平方/2)
2、折半插入排序(二分插入排序):
基本思想:類似二分查找,先取一個序列的中間關鍵字與當前關健字比較,如果相等則查找成功,否則就改變查找區間。但是排序通過查找元素的插入。注意:運用此算法的前提就是一個有序的序列,所以在直接插入排序的基礎上再插入。
核心代碼實現:
void insertsort(int a[],int n){///從小到大排序
int low,high;
for(int i=2;i<=n;i++){
a[0]=a[i];///暫存a[i]
low=1;
high=i-1;
while(low<=high){
int mid=(low+high)/2;
if(a[0]<a[mid]) high=mid-1;
else low=mid+1;
}
for(int j=i-1;j>=high+1;j--) a[j+1]=a[j];
a[high+1]=a[0];
}
}
3、冒泡排序
基本思想:依次比較兩個相鄰的元素,如果(從小到大或從大到小)順序相反就把它們交換。冒泡排序可以理解爲,不斷交換找最大數(冒泡)、縮小範圍繼續找次最小,直到所有元素排序完畢。
比如一個無序序列:45 15 68 2.其冒泡排序的簡易圖如下:
核心代碼實現:
void bubblesort(int a[],int n){
for(int i=1;i<=n-1;i++)///不用遍歷到n,因爲j+1已經訪問了
for(int j=1;j<=n-i;j++)///縮小範圍
if(a[j+1]<a[j]){
int temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}///時間複雜度O(n的平方),
4、簡單選擇排序
基本思想:不斷地找出序列中最小的數,然後與當前比較的數進行交換
核心代碼實現:
void selectsort(int a[],int n){
for(int i=1;i<=n;i++)
{
int t=i;
for(int j=i+1;j<=n;j++)///找出序列中最小的數
if(a[t]>a[j]) t=j;
int temp=a[i];
a[i]=a[t];
a[t]=temp;
}
}
5、希爾排序
希爾排序又稱爲縮小增量排序,先將待排序的記錄分割成爲若干子序列分別進行直接插入排序。
下面是一個序列爲(83、72、47、36、87、30、12、67)希爾排序的實現過程:
畫的再多還是需要動動腦,根據代碼,動筆來捋順一遍這幾幅圖?希望以上圖對大家的理解有所幫助。
核心代碼實現
void shellsort(int a[],int n){///我們不妨回看以下直接插入排序,你會發現相似之處。
int d=n;
d=d/2;
while(d>0){
for(int i=d+1;i<=n;i++){
a[0]=a[i];///暫時記錄下來
int j=i-d;
while(j>=0&&a[0]<a[j]){
a[j+d]=a[j];
j=j-d;
}
a[j+d]=a[0];
}
d=d/2;///縮小增量
}
}
6、快速排序
基本思想:以第一個數爲基本標準,從左往右找比基準數大的數,從右到左找比基準數小的數,然後兩數交換。如果i==j,就找到了基準數要交換的位置。
核心代碼實現:
void quicksort(int a[],int left,int right){
if(left>=right) return;
int i=left;
int j=right;
int x = a[left];///基準數
while(i!=j){
while(i<j&&x<=a[j]) j--;///從右往左
while(i<j&&x>=a[i]) i++;///從左往右
if(i<j){///交換兩數
int t=a[i];
a[i]=a[j];
a[j]=t;
}
}
a[left]=a[i];///交換基準數
a[i]=x;
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
7、堆排序
基本思想:將待排序的記錄序列構造一個堆(大頂堆或小頂堆),此時,選出堆中最小者或者最大者,然後將它從堆中移走(將它與堆數組的末尾元素交換,此時末尾元素就是最大值或最小值),並將剩餘的記錄再調整成堆,這樣又找出了次小(或次大)的記錄,以此類推,直到堆中只有一個記錄爲止。注意:每一個出堆的順序就是一個有序序列。
最大堆:每一個結點大於等於它的左右孩子結點,用於升序序列(找出最大值)
最小堆:每一個結點小於等於它的左右孩子結點,用於降序序列(找出最小值)
假設一個序列爲{16,20,32,28,46,35},此序列構造成小頂堆或者大頂堆如下:
以最大堆進行升序排序的基本思想:
①初始化堆:將數列a[0…n-1]構造成最大堆。
②交換數據:將a[0]和a[n-1]交換,使a[n-1]是a[0…n-1]中的最大值,然後將a[0…n-2]重新調整爲最大堆。以此類推,直到整個數列都是有序的。
假設有下面的一個序列:
其堆排序的執行過程如下:
以此類推…直到整個數列有序。
核心代碼實現:
int swap(int *a,int *b){
int temp=*b;
*b=*a;
*a=temp;
}
void maxheap_down(int a[],int start,int end){///大頂堆向下調整算法
int i=start;
int L=2*i+1;///R==L+1=2*i+2;
int temp=a[i];
for(;L<=end;i=L,L=2*i+1){
if(L<end&&a[L]<a[L+1]) L++;
if(temp>=a[L]) break;
else {
a[i]=a[L];
a[L]=temp;
}
}
}
void heapsort(int a[],int n){
for(int i=n/2-1;i>=0;i--) maxheap_down(a,i,n-1);///初始化堆
for(int i=n-1;i>0;i--) {
swap(&a[0],&a[i]);///交換數據,使數組末尾元素是整個數列最大的。
maxheap_down(a,0,i-1);///依次類推0~i-1範圍內的最大值
}
}
8、二路歸併排序
基本思想:將一個具有n個待排序記錄的序列記錄的序列看成是n個長度爲1的有序列,然後兩兩歸併,得到一個長度爲2的有序序列;若n爲奇數,則最後一個關鍵字不參與歸併,直接進入下一趟歸併。如此重複,直到一個長度爲n的有序序列爲止。
核心代碼實現:
const int maxn=1010;
///將數組a的[L1,R1]與[L2,R2]區間合併爲有序區間
void Merge(int a[],int L1,int R1,int L2,int R2){
int i=L1;
int j=L2;
int t[maxn],k=0;///數組t臨時存放合併後的數組,如果點比較大我們可以定義爲*t,申請空間
while(i<=R1&&j<=R2){
if(a[i]<=a[j]) t[k]=a[i++];
else t[k]=a[j++];
k++;
}
while(i<=R1) t[k++]=a[i++];///將[L1,R1]的剩餘元素加入序列temp
while(j<=R2) t[k++]=a[j++];///將[L2.R2]的剩餘元素加入序列temp
for(i=0;i<k;i++)
a[L1+i]=t[i];///將合併後的序列賦值回數組a
}
void mergesort(int a[],int left,int right){///遞歸實現
if(left<right){
int mid=(left+right)/2;
mergesort(a,left,mid);
mergesort(a,mid+1,right);
Merge(a,left,mid,mid+1,right);
}
}
void mergesort(int *a,int n){///非遞歸實現
for(int step=2;step/2<=n;step*=2)///step爲組內元素。step/2爲左子區間元素個數
for(int i=0;i<n;i+=step){
int mid=i+step/2-1;
///左子區間[i,mid],右子區間爲[mid+1,mid(i+step-1,n-1)],mid(i+step-1,n-1):當i等於n-1
if(mid+1<n) Merge(a,i,mid,mid+1,min(i+step-1,n-1),n);///右子區間存在元素則合併
}
}
9、桶排序
基本思想:把待排序的序列的每個數用一個數組記錄下來,數組的下標就是每個數的值,然後統計每個數出現的個數,然後遍歷。
假設一個序列:5 3 5 2 8
核心算法實現:
///從大到小排序
int book[1001];///數組大小看題目需求
for(int i=0;i<=1000;i++) ///一個待排序的序列每個元素的值的範圍
book[i]=0;
for(int i=1;i<=n;i++){
scanf("%d",&t);
book[t]++;///記錄當前的元素t有幾個
}
for(int i=1000;i>=0;i--)
for(int j=1;j<=book[i];j++) ///元素個數
printf("%d ",i);
///時間複雜度爲O(2*(m+n))
10、基數排序
基本思想:將所有待排序的數統一爲相同得數位長度,數位較短得數前面補0;從最地位開始,依次進行排序;從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列
一個含有10個數的待排序的序列爲{236,63,970,852,109,621,8,63,505,189}。它的基數排序過程如下:(從左到右)
初始狀態–> | 按個位排序–> | 按十位排序–> | 按百位排序 |
---|---|---|---|
236 | 970 | 505 | 008 |
063 | 621 | 008 | 063 |
970 | 852 | 109 | 083 |
852 | 063 | 621 | 109 |
109 | 083 | 236 | 189 |
621 | 505 | 852 | 236 |
008 | 236 | 063 | 505 |
083 | 008 | 970 | 621 |
505 | 109 | 083 | 852 |
189 | 189 | 189 | 970 |
核心算法實現:
int maxbit(int a[],int n){///求數據的最大位數
int d=1;///保存最大位數
int p=10;
for(int i=0;i<n;i++)
while(a[i]>=p){
p*=10;
++d;
}
}
void radixsort(int a[],int n){
int d=maxbit(a,n);
int temp[n];
int count[10];///計數
int k;
int radix=1;
for(int i=1;i<=d;i++){///進行d次排序
for(int j=0;j<10;j++) count[j]=0;///初始化
for(int j=0;j<n;j++) {
k=(a[j]/radix)%10;///取位
count[k]++;
}
for(int j=1;j<10;j++) count[j]=count[j-1]+count[j];
///把temp的位置分給每個桶
for(int j=n-1;j>=0;j--) {
k=(a[j]/radix)%10;///取位
temp[count[k]-1]=a[j];
count[k]--;
}
for(int j=0;j<n;j++) a[j]=temp[j];///把臨時數組的內容複製到a中
radix*=10;
}
}
11、計數排序
計數排序是基於非比較的排序算法。核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。
基本步驟:
①掃描整個序列,獲取最小值min和最大值max
②開闢新空間的數組count,長度爲max-min+1,它是記錄的值是序列中某元素出現的次數
核心算法實現:
void Countsort(int a[],int n){
int max=a[0];
int min=a[0];
for(int i=1;i<n;i++){
if(a[i]>max) max=a[i];
if(a[i]<min) min=a[i];
}
int m=max-min+1;///獲得申請空間大小
int *count=(int *)malloc(sizeof(int)*m);
memset(count,0,sizeof(int)*m);///初始化
for(int i=0;i<n;i++)count[a[i]-min]++;///統計每一個元素的個數
for(int i=1;i<m;i++) count[i]+=count[i-1];///確定元素位置
int *t=(int *)malloc(sizeof(int)*n);
for(int i=n-1;i>=0;i--){
count[a[i]-min]--;
t[count[a[i]-min]]=a[i];
}
for(int i=0;i<n;i++) a[i]=t[i];
free(count);
free(t);
}
三、各排序的時間複雜度分析
排序算法 | 平均複雜度 | 最好情況 | 最壞情況 | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | O(n*n) | O(n) | O(n*n) | O(1) | 穩定 |
選擇排序 | O(n*n) | O(n*n) | O(n*n) | O(1) | 不穩定 |
插入排序 | O(n*n) | O(n) | O(n*n) | O(1) | 穩定 |
希爾排序 | O(n*log n) | O(n*log2 n) | O(n*log2 n) | O(1) | 不穩定 |
歸併排序 | O(n*log n) | O(n*log n) | O(n*log n) | O(n) | 穩定 |
快速排序 | O(n*log n) | O(n*log n) | O(n*n) | O(log n) | 不穩定 |
堆排序 | O(n*log n) | O(n*log n) | O(n*log n) | O(1) | 不穩定 |
計數排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 穩定 |
桶排序 | O(n+k) | O(n+k) | O(n*n) | O(n+k) | 穩定 |
基數排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 穩定 |
四、參考資料
1、《算法筆記》
2、《啊哈算法》
3、《圖論及其應用》
4、公衆號五分鐘學算法
5、《大話數據結構》