1. 程式人生 > >插入排序/選擇排序/交換排序/歸併排序/基數排序

插入排序/選擇排序/交換排序/歸併排序/基數排序

資料處理時一個主要需求就是排序,目前主要的記憶體排序(處理的資料量百萬級以下)主要基於關鍵字大小,具體可分一下幾種:

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。

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;
            }
}
這種平均耗時O(N log N),不穩定

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),穩定