介紹
排序是數據處理中一種很重要也很常用的運算,一般情況下,排序操作在數據處理過程中要花費許多時間,爲了提高計算機的運行效率,我們提出並不斷改進各種各樣的排序算法,這些算法也從不同角度展示了算法設計的重要原則和技巧。
分類
複雜度
排序方法 | 時間複雜度(平均) | 時間複雜度(最壞) | 時間複雜度(最好) | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | 穩定 | ||||
選擇排序 | 不穩定 | ||||
插入排序 | 穩定 | ||||
希爾排序 | 不穩定 | ||||
快速排序 | 不穩定 | ||||
歸併排序 | 穩定 | ||||
堆排序 | 不穩定 | ||||
基數排序 | 穩定 |
代碼
除了一個swap和test輔助函數,其餘的是八種算法的函數全家桶,不方便看的話可以回到我前面的章節,不過沒有講原理,只有代碼。
#include<stdlib.h>
#include<stdio.h>
#include<time.h>
#include<sys/timeb.h>
//#define MAX 250000 //這個數字棧就溢出了
#define MAX 2500000
/*
n種排序方法,都是從大到小排列
沒有太參考別人的程序,大部分是根據定義琢磨的
如有疏漏,懇請批評指教
*/
//輔助函數:交換兩個變量
void swap(int*a,int*p)
{
int temp = *a;
*a = *p;
*p = temp;
}
//---------------自己實現的各種排序算法----------------
//冒泡排序
//兩兩對比,把最大的一直挪到最後面
//非常不怎樣的一個算法,但確是大部分高校教的第一個算法
void bubbleSort(int* arr,int len)
{
int i,j;
//這個是上界
for(i=0;i<len;i++)
{
//一般導數第i+1個以後都已經排好了
for(j=1;j<len-i;j++)
{
//大的一直冒泡到最後面去
if(arr[j-1]>arr[j])
{
swap(&arr[j-1],&arr[j]);
}
}
}
}
//選擇排序
//把最小的一個個放到第一第二第三個
//已放好的叫有序區,沒放好的是無序區,有序區一旦放好就不會變了
void selectSort(int*arr,int len)
{
//i大循環,j大小對比
int i,j;
int temp,index;//臨時對照變量
for(i=0;i<len;i++)
{
//先假設無序區第一個是最小
temp = arr[i];
index = i;
for(j=i+1;j<len;j++)
{
//找到無序區真正最小的
if(arr[j]<temp)
{
temp = arr[j];
index = j;
}
}
//如果第一個就是最小的,就不用進行什麼交換
if(index==i)
continue;
//不然就交換無序區第一個數和無序區最小的數
swap(&arr[i],&arr[index]);
}
}
//插入排序
//從第1個數開始,往後開始遍歷,第i個數一定要放到使得前i個數都變成有序的。
void insertSort(int* arr,int len)
{
int i,j;
for(i=0;i<len;i++)
{
for(j=i;j>0;j--)
{
if(arr[j]<arr[j-1])
{
swap(&arr[j],&arr[j-1]);
}else
{
//已經不比那個元素小了就提前退出
break;
}
}
}
}
//希爾排序
//插入排序的加強版,不是一次性進行插入,而是分成一撥撥來進行
//比如奇數下標的爲一撥,偶數下標的爲一撥,然後再對分好的兩撥進行插入排序
//也就是一開始是隔一定step>1進行插入排序,最後的step=1
//這個步長的變動方式有多種,可是是 step:=step/3+1
//對大量的數據,排序效率明顯比插入排序高
void shellSort(int* arr,int len)
{
int step = len;
//do while比較好,保證step爲1還能再排一次
do
{
//這句一定要放這裏,不然步長爲1就跳出去了,最後一次無法排序
step = step/3 +1;
int i,j,k;
//分撥排序,一共有step撥
for(i=0;i<step;i++)
{
for(j=i;j<len;j+=step)
{
for(k=j;k>i;k-=step)
{
if(arr[k]<arr[k-step])
{
swap(&arr[k],&arr[k-step]);
}else
{
break;
}
}
}
}
}while(step>1);
}
//快速排序
//顧名思義,排得真的很快。用遞歸思想
//每次找一個基準數,以其爲參考點,比它小的放左邊,大的放右邊(這兩堆內部可能是無序的)
//再把分好的兩堆各自找個基準數,按前面的步驟再來,直至數據不能再分,排序完畢
//基準先挖出來,有i,j兩個指針,一開始j往左挪,如果遇到比基準小的,填到基準位置
//之後換i往後挪,遇到比基準大的,就放到j的那個坑。全部跑完後,基準丟到最後剩出來的那個坑。
void quickSort(int* arr,int start,int end)
{
//遞歸最重要的就是設置退出條件,如下
if(start>=end)
{
return;
}
int i = start;
int j = end;
int temp = arr[i];
//如果右指針j一直沒小於左指針i,一直跑
while(i<j)
{
//先從右邊找比基準小的,找到和基準交換,但要保留j值
while(i<j)
{
if(arr[j]<temp)
{
swap(&arr[j],&arr[i]);
break;
}
j--;
}
//右邊找到一個比基準小的之後,輪到左邊找比基準大的,然後和上面空出的j位置交換
while(i<j)
{
if(arr[i]>temp)
{
swap(&arr[j],&arr[i]);
break;
}
i++;
}
}
//排左半區
quickSort(arr,start,i-1);
//排右半區
quickSort(arr,i+1,end);
}
//歸併排序
//本質上是把兩個已經排好的序列合併成一個
//如果對一個隨機序列的兩兩元素來看,那麼每個元素都是排好的序列
//可以把一個數組拆分成前後兩半來做這件事
//這個算法需要額外的輔助空間,用來存放歸併好的結果
void mergeSort(int* arr,int start,int end)
{
if(start>=end)
{
return;
}
int i = start;
int mid = (start+end)/2;
int j = mid + 1;
mergeSort(arr,i,mid);
mergeSort(arr,j,end);
//合併
//其實我覺得不用這個額外的空間也行,兩個子序列再排一次能減少空間,不過速度肯定會有影響
int* temp = (int*)malloc((end-start+1)*sizeof(int));
int index = 0;
//開始對比兩個子序列,頭部最小的那個數放到新空間
while(i<=mid&&j<=end)
{
if(arr[i]<=arr[j])
{
temp[index++] = arr[i++];
}else
{
temp[index++] = arr[j++];
}
}
//總有一個序列是還沒有放完的,這裏再遍歷一下沒放完的
while(i<=mid)
{
temp[index++] = arr[i++];
}
while(j<=end)
{
temp[index++] = arr[j++];
}
//排完再把新空間的元素放回舊空間
int k = start;
for(k;k<=end;k++)
{
//哎,temp的下標寫錯,排查了一個鐘,真菜
arr[k] = temp[k-start];
}
free(temp);
}
//堆排序
/*
這個堆是數據結構堆,不是內存malloc相關的那個堆--我曾經理解錯n久
根節點比孩子節點都大的叫大頂堆(包括子樹的根),比孩子節點小的叫小頂堆
從小到大排序用的是大頂堆,所以要先構造這種堆,然後把這個根的最大元素交換到最尾巴去
每次拿走一個最大元素,待排序的隊列就慢慢變短
主要步驟1,初始化構造這種大頂堆,把堆頂最大的數放到最尾巴,數列長度減少1,再次構建大頂堆
2,這時候只有堆頂元素不滿足大頂堆,那麼其實只要從堆頂元素開始慢慢微調而已,沒必要再完全重新建堆,想要也可以,不過很浪費時間
理解起來確實很難,涉及到完全二叉樹
孩子節點i的爸爸是i/2,爸爸節點的兒子是2i和2i+1。
第一次初始化之後充分利用子樹已經是大頂堆
*/
void adjust(int* arr,int len,int index)
{
//調整函數,把孩子、父親中的最大值放到父親節點
//index爲待調整節點下標,一開始設它最大
int max = index;
int left = 2*index+1;//左孩子
int right = 2*index+2;//右孩子
if(left<len && arr[left] > arr[max])
{
max = left;
}
if(right<len && arr[right] > arr[max])
{
max = right;
}
//如果父親節點不是最大
if(max!=index)
{
//一旦上層節點影響了某個孩子節點,還要觀察以這個孩子節點爲父節點的子樹是不是也不是大頂堆了
swap(&arr[index],&arr[max]);
//因爲發生了交換,還要繼續調整受到影響的孩子節點
//***************************************
adjust(arr,len,max);//這句話非常非常關鍵
//***************************************
/*
只有父親和孩子節點發生了交換,纔有繼續調整孩子的必要,如果無腦在不是這裏面遞歸,堆排序的效果不會比冒泡好到哪去
而如果寫在了這裏面,雖然還是pk不過快排,但好歹和快排的差距只縮小到個位數倍數的量級(小數據量的時候)
堆排序一個優點是空間複雜度也不高
*/
}
}
//主要排序部分
void heapSort(int* arr,int len)
{
//初始化大頂堆
//initHeap(arr,i,0);
//從最後一個非葉子節點開始
//第一次一定要從下至上一直排,一開始是亂序的
int i = len/2-1;
for(i;i>=0;i--)
{
adjust(arr,len,i);
}
swap(&arr[0],&arr[len-1]);
//第二次之後,只需要從根節點從上到下調整,遇到沒發生交換的直接可以退出循環了
//微調得到大頂堆(因爲只有堆頂不滿足而已)
int j = len -1; //去掉尾節點後的數組長度
//把最大值交換到最後
for(j;j>0;j--)
{
adjust(arr,j,0);
swap(&arr[0],&arr[j-1]);
}
}
//基數排序
//radix sort,說是桶排bucket sort的一種,具體沒仔細查證
//對於整數,按個位數先排,再排十位,再排百位..
//需要和原數組一樣的額外的空間,用來臨時存那些數字
void radixSort(int* arr,int len,int max)
{
int n = 1;//位數 1 10 100 1000 ...
int (*bucket)[len] = (int(*)[len])malloc(10*len*sizeof(int));//int[某數位爲某尾數的所有數字][某尾數]
int count[10] = {0};//某個數位0-9
while(n<=max)
{
int i = 0;
int digit;
for(i = 0;i<len;i++)
{
digit = (arr[i]/n)%10;//某個數位的數字,如n=10,arr[i]爲293,則這裏就是求十位,即=9
bucket[digit][count[digit]]= arr[i];
count[digit] += 1;//記得每次找到一個位數爲digit的,數量+1
}
//放回原數組
i = 0;
for(digit = 0;digit<10;digit++)
{
int j = 0;
for(j = 0;j<count[digit];j++)
{
arr[i++] = bucket[digit][j];
}
count[digit] = 0;//清空那個桶,準備下次放
}
n*=10;//位數10倍變化
}
free(bucket);
}
//---------------輔助函數等----------------
//打印數組
void printArr(int* arr,int len)
{
int i = 0;
for(i;i<len;i++)
{
printf("%d\t",*(arr++));
}
printf("\n");
}
//計算時間,精確到毫秒
long getTime()
{
struct timeb tb;
ftime(&tb);
//前面是毫秒,後面是微秒
return tb.time*1000+tb.millitm;
}
//小型主函數
void test()
{
//int a[MAX],a1[MAX],a2[MAX],a3[MAX],a4[MAX],a5[MAX],a6[MAX],a7[MAX];
int* a = (int*)malloc(MAX*sizeof(int));
int* a1 = (int*)malloc(MAX*sizeof(int));
int* a2 = (int*)malloc(MAX*sizeof(int));
int* a3 = (int*)malloc(MAX*sizeof(int));
int* a4 = (int*)malloc(MAX*sizeof(int));
int* a5 = (int*)malloc(MAX*sizeof(int));
int* a6 = (int*)malloc(MAX*sizeof(int));
int* a7 = (int*)malloc(MAX*sizeof(int));
int i = 0;
srand(time(NULL));
for(i;i<MAX;i++)
{
int temp = rand()%(MAX+1);
// int temp = i;//升序測試
// int temp = MAX-i-1;//降序測試
a[i] = temp;
a1[i] = temp;
a2[i] = temp;
a3[i] = temp;
a4[i] = temp;
a5[i] = temp;
a6[i] = temp;
a7[i] = temp;
}
// printArr(a,MAX);
long t1 = getTime();
//冒泡排序
// bubbleSort(a,MAX);
// printArr(a,MAX);
long t2 = getTime();
printf("冒泡排序排%d個隨機數據耗時%ld毫秒\n",MAX,t2-t1);
//選擇排序
// selectSort(a1,MAX);
// printArr(a1,MAX);
long t3 = getTime();
printf("選擇排序排%d個隨機數據耗時%ld毫秒\n",MAX,t3-t2);
//插入排序
// insertSort(a2,MAX);
// printArr(a2,MAX);
long t4 = getTime();
printf("插入排序排%d個隨機數據耗時%ld毫秒\n",MAX,t4-t3);
//希爾排序
shellSort(a3,MAX);
// printArr(a3,MAX);
long t5 = getTime();
printf("希爾排序排%d個隨機數據耗時%ld毫秒\n",MAX,t5-t4);
//快速排序
quickSort(a4,0,MAX-1);
// printArr(a4,MAX);
long t6 = getTime();
printf("快速排序排%d個隨機數據耗時%ld毫秒\n",MAX,t6-t5);
//歸併排序
mergeSort(a5,0,MAX-1);
// printArr(a5,MAX);
long t7 = getTime();
printf("歸併排序排%d個隨機數據耗時%ld毫秒\n",MAX,t7-t6);
//堆排序
heapSort(a6,MAX);
// printArr(a6,MAX);
long t8 = getTime();
printf("堆排序排%d個隨機數據耗時%ld毫秒\n",MAX,t8-t7);
//基數排序
radixSort(a7,MAX,MAX);
printArr(a7,MAX);
long t9 = getTime();
printf("基數序排%d個隨機數據耗時%ld毫秒\n",MAX,t9-t8);
}
void main()
{
test();
}
運行結果:
數據量較小的時候
150000個隨機數:
250000個隨機數:
數據量較大的時候
加入了個新成員基數排序
250w個隨機數:
2500w個數據:
最後一個掛掉的是基數排序,我的內存只有2G,基數排序用了桶的原理,需要額外空間。
千萬不要拿冒泡去試上面這個數據量,除非你真的很閒。
數據原本有序且是升序的時候
數據原本有序且是降序的時候
後話
注意,如果原本的序列就是有序(升序或降序)的,快排基本就是和冒泡是兄弟了,慢到讓你懷疑人生。希爾、歸併、堆排、基數基本影響不大。可見如果數據本來就只有微小的無序,還是不要用快排了。