插入排序/選擇排序/交換排序/歸併排序/基數排序
資料處理時一個主要需求就是排序,目前主要的記憶體排序(處理的資料量百萬級以下)主要基於關鍵字大小,具體可分一下幾種:
1. 插入排序:直接插入排序(穩定)和希爾排序(升級版,不穩定);
直接插入排序,關鍵是在以排序好的序列基礎上再將加入一個新元素並完成排序,即
陣列a[0]-a[i-1]是已經按照從小到大的順序排序好的序列,而a[i]-a[n-1]是待排序序列,取a[i]進入a[0]-a[i-1]重新排序並完成後a[0]-a[i-1] a[i]又是一個排序好的序列,每次從未排序中選擇元素的增量d=1
void my_compare::insert_direct(int a[],int n){ int i,j; int tmp; for(i=1;i<n;i++) { tmp=a[i]; j=i-1; while(j>=0&&a[j]>tmp){a[j+1]=a[j];j--;} a[j+1]=tmp; } }
可分析知道,當原始陣列就是從小到大排列時執行效率最高O(N)反之當資料基本反序時執行效率最差O(N^2),平均耗時O(N×N),穩定。
希爾排序,根據人名翻譯,其實質是縮小增量版的升級插入排序,核心是選擇一個增量d(一般是以原始陣列數目一般半為起始),剩下的類似直接插入排序,只不過每次從未排序中選擇元素的增量d>1,完成一次排序後縮小增量d,直至d=1。
這種平均耗時O(N log N),不穩定。void my_compare::insert_shell(int a[],int n){ int i,j; int tmp; int d=n/2; while(d>0){ //對相距gap距離的所有元素進行插入排序 for(i=d;i<n;i++) {tmp=a[i];j=i-d; while(j>=0&&a[j]>tmp){a[j+d]=a[j];j=j-d;} a[j+d]=tmp; } d=d/2; } }
2. 選擇排序:直接選擇排序(不穩定)和堆排序(以完全二叉樹為基礎,不穩定);
直接選擇排序主要是陣列a[0]-a[i-1]是已排序序列,a[i]-a[n-1]序列是未排序序列,從這未排序裡選擇最小值作為a[i](不同於直接插入是直接選擇a[i]),放在陣列a[0]-a[i-1]最後位置。其時間平均效率O(N^2),不穩定。
void my_compare::select_direct(int a[],int n){ int i,j; int tmp; for(i=0;i<n-1;i++) { int k=i; for(j=i+1;j<n;j++) if(a[j]<a[k])k=j; tmp=a[i]; a[i]=a[k]; a[k]=tmp; } }
直接選擇排序與原始序列是否有序無關,,時間複雜度都是O(N^2)。
堆排序,借鑑了優先佇列裡堆序的特點即每個樹的根是最值,類似堆Deletemin操作每次選擇根值(最值)進行n-1操作即完成排序。
void my_compare::sift(int a[],int low,int high){
int i=low,j=2*i;
int tmp=a[i];
while(j<=high){
if(j<high&&(j+1)<=high&&a[j]<a[j+1])j++;
if(tmp<a[j]){a[i]=a[j];i=j;j=2*i;}
else break;//不滿足直接退出
}
a[i]=tmp;
}
void my_compare::select_heap(int a[],int n){
int i;
int tmp;n--;
//先建立堆,從最後非葉子開始
for(i=n/2;i>=1;i--)
sift(a,i,n);
for(i=n;i>=2;i--)
{
tmp=a[1];a[1]=a[i];a[i]=tmp;sift(a,1,i-1);
}
}
時間效率(N log N),不穩定。3. 交換排序:氣泡排序(穩定)和快速排序(升級版,選擇基準,不穩定);
不同於直接插入和直接選擇區分排序區和未排序區,氣泡排序演算法思想是直接對整個無序的原始陣列進行處理,每趟對相鄰關鍵字進行比較和位置置換,一趟完成使得最值如氣泡一般漂浮到最後位置,接著對剩下的陣列進行類似處理。
void my_compare::swap_bubble(int a[],int n){
int i,j;
int tmp;
for(i=0;i<n-1;i++)//n-1趟比較即可
{
for(j=0;j<n-i-1;j++)
if(a[j]>a[j+1]){tmp=a[j];a[j]=a[j+1];a[j+1]=tmp;}
}
}
類似直接插入排序當原始陣列就是從小到大排列時執行效率最高O(N)反之當資料基本反序時執行效率最差O(N^2),平均耗時O(Nlog N),穩定。
快速排序相比較之前排序有點複雜, 快速排序由於排序效率在同為O(N*logN)的幾種排序方法中效率較高,因此經常被採用,再加上快速排序思想----分治法也確實實用,因此很多軟體公司的筆試面試,包括像騰訊,微軟等知名IT公司都喜歡考這個,還有大大小的程式方面的考試如軟考,考研中也常常出現快速排序的身影。故重點介紹:
快速排序是基於交換的氣泡排序的改進,基本思想是在n個關鍵字中取一個記錄作為基準樞紐元,然後對剩下的n-1個元素進行分類,所有比基元小的放置在前子區間A,比基元大的都放在後子區間B,完成了一趟快排即:
前子區間A 基元 後子區間B-----------------------------------A與B數目儘量相同,否則為劣質分割
此時基元上的關鍵字已完成最終位置的排序,剩下的就是分別對兩個子區間也採用這種思路,直到每個子區間只有一個關鍵字為止,總而言之是分而治之。
雖然上述演算法無論選擇哪個元素作為基準都能完成排序,但是不同基準選擇效果不同:
1 最容易想到的選擇第一個元素作為基元
void my_compare::quicksort(int a[],int low,int high){
int i=low,j=high;//指向無序區的第一個和最後一個以便從兩端向中間
int tmp;
if(low<high){
tmp=a[low];//選擇第一個作為基準
while(i<j){
while(j>i&&a[j]>tmp)j--;
if(j>i){a[i]=a[j];i++;}
while(j>i&&a[i]<tmp)i++;
if(j>i){a[j]=a[i];j--;}
}
a[i]=tmp;
//完成一趟快排,之後對兩個子區間遞迴
quicksort(a,low,i-1);
quicksort(a,i+1,high);
}
}
void my_compare::swap_quick(int a[],int n){
quicksort(a,0,n-1);
}
這種情況一般針對與輸入的關鍵字都是隨機的,此時時間效率能達到O(Nlog N),不穩定。但是一旦輸入是預排序或者反序,這樣的基準將產生非常惡劣影響:剩下的n-1個元素不是都劃入A就是被劃入B。例如陣列是預排序的,若採用第一個元素作為基元,則根本沒有“一分為二”,時間效率O(N^2),時間增加了,但是實際上是浪費了(因為花了如此多時間還只是對已經排好的序列再進行一次排列而已)。2 最安全的選擇隨機元素作為基元
一般選擇採用隨機選擇基元是安全的,另外也可以採用叄數中值分隔法,即選擇序列左端/右端/及中心位置的這三個元素中值作為基元例如8,1,4,9,6,3,5,2,7,0最左端8,最右端0中間位置(left_right)/2上是6,這三個數中值為6故基元為6。且在選擇基元后,可把這三個數值進行排序,把最小者放在最左位置(這也正是它在A中的應有位置)把最大放在最右端(這也正是它在B中的應有位置)以減少分割時的操作。
具體的分割策略如下:
所有元素互異:當i在j左邊時,i右移,移過那些小於基元的元素,同時j左移,移過那些大於基元的元素,當i和j停止時,當i小於j前提下,i指向一個大於基元而j指向一個小於基元,交換i和j所指元素,直到i和j已經交錯,分割的最後一步是將基元與i最後到達位置進行交換以便讓基元回到最終位置。
有元素等於基元:無非三種情況,要麼i和j遇見與基元相等的直接跳過;要麼停下進行交換;要麼一個停下一個跳過,這種不對稱做法容易導致分割兩部分不均故直接捨棄。為了分析可以假設所有元素都相同。
對於直接跳過情景,由於基元與i最後到達位置進行交換,故將導致產生兩個非常不均衡的子區間,類似與預排序陣列與第一個元素作為基準的情景此時時間O(N^2),故舍棄;
對於都停下交換情景:雖然沒有實際意義,但是效果在於i和j將在中間交替,而不像直接跳過情景i直接跳到了序列最後位置。因此將基元與i最後到達位置進行交換以便讓基元回到最終位置後,分割兩部分基本均衡,這種完美均衡效果就是執行時間O(N log N)。故採用這種。
//*********對基元的選擇********************//
//將三者最小者放在左端,最大者放在右端
int my_compare::getpivot(int a[],int low,int high){
int middle=(low+high)/2;
int tmp;
if(a[low]>a[middle]){tmp=a[low];a[low]=a[middle];a[middle]=tmp;}
if(a[low]>a[high]){tmp=a[low];a[low]=a[high];a[high]=tmp;}
if(a[high]<a[middle]){tmp=a[high];a[high]=a[middle];a[middle]=tmp;}
//完成三者位置最終佈置
//將選擇中值此時也即中間位置值作為基元並交換到最後第二位置
tmp=a[middle];a[middle]=a[high-1];a[high-1]=tmp;
return a[high-1];
}
void my_compare::quicksort1(int a[],int low,int high){
int i,j;
int pivot;
int tmp;
if(low<high){
pivot=getpivot(a,low,high);
i=low+1;j=high-2;//low位置為最小值,故Low+1,high位置最大值且High-1位置為基元故high-2
while(i!=j){
while(j>i&&a[i]<pivot)i++;
while(j>i&&a[j]>pivot)j--;
if(i<j){tmp=a[i];a[i]=a[j];a[j]=tmp;}
else break;
}
tmp=a[i];a[i]=a[high-1];a[high-1]=tmp;//pivot在high-1處,把基元放回i最後位置
quicksort1(a,low,i-1);
quicksort1(a,i+1,high);
}
}
void my_compare::swap_quick(int a[],int n){
quicksort1(a,0,n-1);
}
快排一般也會問時間複雜度:
在理想情況即A和B兩子區間分得均勻:
對於一長度n的序列,先掃描找到基準,然後兩個子區間分別遞迴:
第一次時:T(n)=2*T(n/2)+n
第二次時:T(n)=2*T(n/2)+n=T(n)=2*(2*T(n/4)+n/2)+n=4T(n/4)+2n
第三次時:T(n)=2*T(n/2)+n=8T(n/8)+3n
第k次時:T(n)=2*T(n/2)+n=2^kT(n/2^k)+kn
已知T(1)=0,則k=log2(n),T(n)=n*log2(n)
在惡劣條件下:
當待排序的序列為正序或逆序排列時,且每次劃分只得到一個比上一次劃分少一個記錄的子序列,注意另一個為空。需要進行n-1次比較,每次比較只少一個元素。
4. 歸併排序(從小到大,穩定);
其核心思想是把a[0]-a[n-1]看成n個長度為1的有序表,將相鄰的有序表成對歸併得到n/2個長度為2的有序表,然後繼續按照此思路歸併知道最後得到1個長度為n的有序表。設計思路:
a 先完成相鄰兩個有序表a[low]-a[mid]與a[mid+1]-a[high]的合併
b 完成給定長度lenghth下原始陣列的合併(注意對於最後一個子表長度小於Length的處理,這是經常遇見的情形)
c 完成lenght=1,=2,=4...n的迴圈
void my_compare::merge_two(int a[],int low,int mid,int high){
int len=high-low+1;
int *pt=(int *)malloc(sizeof(int)*len);
int i=low,j=mid+1;//兩個有序區a[low]-a[mid] a[mid+1]-a[high]開始位置
int k=0;
while(i<=mid&&j<=high){
if(a[i]<a[j]){pt[k]=a[i];i++;k++;}
else {pt[k]=a[j];j++;k++;}
}
while(i<=mid){pt[k]=a[i];i++;k++;}
while(j<=high){pt[k]=a[j];j++;k++;}
for(k=0,i=low;k<len;i++,k++)
a[i]=pt[k];//將排序好的還原到a陣列
}
void my_compare::merge_1(int a[],int length,int n)//某length對應每趟下子表長
{
int i;
for(i=0;i+2*length-1<n;i=i+2*length)
merge_two(a,i,i+length-1,i+2*length-1);//歸併length長兩相鄰子表,非最後
if(i+length-1<n)
merge_two(a,i,i+length-1,n-1);//處理最後兩個子表
}
void my_compare::merge_(int a[],int n){
int l;
for(l=1;l<n;l=2*l)
merge_1(a,l,n);
}
歸併排序不同於插入之希爾排序/選擇之堆排序/交換之快速排序從長到短的分而治之,他是從短到長一一破解。時間效率也是O(N log N),穩定。5 番外:基數排序(穩定)
核心是不同於插入/交換/選擇排序比較關鍵字比較,基數排序直接比較數值的每一位數字,對陣列n個元素進行若干趟分配與和收集。要求每個元素是d位的十進位制正整數元素。對於n個元素每一位數字無非從0-9,建立這樣的陣列alist[10],且每個陣列元素alist[j]指向一個 單鏈表,每條單鏈表上元素均是某趟下數字為j的元素。下一趟處理是建立在上一趟分配收集完基礎上。
int my_compare::getres(int a,int d,int i){
if(i<1||i>d)return -1;
int j=i;
int res;
do{res=a%10;
a=a/10;
j++;
}while(j<=d);
return res;
}// 得到指定位數字
void my_compare::radix(int a[],int n,int d){
typedef struct ele{
int key;
ele *next;
}newtype;
newtype *tp[10],*tail[10],*p=NULL;
int i,j,k;
//從低位到高位做d趟排序
for(i=d-1;i>=0;i--){
for(j=0;j<10;j++){tp[j]=tail[j]=NULL;}
for(k=0;k<n;k++)
{int res=getres(a[k],d,i+1);// 獲得該未數字
newtype *s=(newtype*)malloc(sizeof(newtype));
s->key=a[k];s->next=NULL;
if(tp[res]==NULL){tp[res]=s;tail[res]=s;}
else {tail[res]->next=s;tail[res]=s;}
}
//完成一趟排序後,收集
k=0;
for(j=0;j<10;j++)
if(tp[j]){p=tp[j];while(p){a[k++]=p->key;p=p->next;}}
}
}
void my_compare::radix_(int a[],int n){
radix(a,n,3);
}
這種情況時間複雜度O(d*(n+10)),空間複雜度O(n+10),穩定。