排序算法5

1、序言

這是《漫談經典排序算法系列》第五篇,給出了三種線性時間排序,分別是計數排序、基數排序、桶排序

各種排序算法的解析請參考如下:

 

《漫談經典排序算法:一、從簡單選擇排序到堆排序的深度解析》

《漫談經典排序算法:二、各種插入排序解析及性能比較》

《漫談經典排序算法:三、冒泡排序 && 快速排序》

《漫談經典排序算法:四、歸併排序》

《漫談經典排序算法:五、線性時間排序(計數、基數、桶排序)》

《漫談經典排序算法:六、各種排序算法總結》

注:爲了敘述方便,本文以及源代碼中均不考慮A[0],默認下標從1開始。

2、計數排序

          2.1 引出

            前面四篇博客中,所有的排序算法都存在比較,都可以稱爲”比較排序“。比較排序的下界爲o(nlogn)。那麼有沒有時間複雜度爲o(n)的線性時間排序算法呢?計數排序便是很基礎的一種線性時間排序,它是基數排序的基礎。基本思想是:對每一個元素x,確定小於x的元素個數,就可以把x直接放到它在有序序列中的位置上。過程描述:假設待排序序列a中值的範圍[0,k],其中k表示待排序序列中的最大值。首先用一個輔助數組count記錄各個值在a中出現的次數,比如count[i]表示i在a中的個數。然後依次改變count中元素值,使count[i]表示a中不大於i的元素個數。然後從後往前掃描a數組,a中的元素根據count中的信息直接放到輔助數組b中。最後把有序序列b複製到a。

          2.2 代碼

[cpp] view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3.   
  4. //計數排序,n爲數組a的記錄個數,k爲記錄中最大值  
  5. void countingSort(int *a,int n,int k)  
  6. {  
  7.     int i;  
  8.     int *count=(int *)malloc(sizeof(int)*(k+1));  
  9.     int *b=(int *)malloc(sizeof(int)*(n+1));  
  10.     //初始化計數數組count  
  11.     for(i=0;i<=k;i++)  
  12.         *(count+i)=0;  
  13.     //計算等於a[i]的記錄個數  
  14.     for(i=1;i<=n;i++)  
  15.         (*(count+a[i]))++;  
  16.     //計算小於等於a[i]的記錄個數  
  17.     for(i=1;i<=k;i++)  
  18.         *(count+i) += *(count+i-1);  
  19.     //掃描a數組,把各個元素放在有序序列中相應的位置上  
  20.     for(i=n;i>=1;i--){  
  21.         *(b + *(count + a[i]))=a[i];  
  22.         (*(count+a[i]))--;   
  23.     }  
  24.     for(i=1;i<=n;i++)  
  25.         a[i]=*(b+i);  
  26.     free(count);  
  27.     free(b);  
  28. }  
  29.   
  30. void main()  
  31. {  
  32.     int i;  
  33.     int a[7]={0,3,5,8,9,1,2};//不考慮a[0]  
  34.     countingSort(a,6,9);  
  35.     for(i=1;i<=6;i++)  
  36.         printf("%-4d",a[i]);  
  37.     printf("\n");  
  38. }  

          2.3 效率分析

從代碼來看,計數排序有5個for循環,其中三個時間是n,兩個時間是k。所以總時間T(3n+2k),時間複雜度o(n+k),不管是在最壞還是最佳情況下,此時間複雜度不變.此外,計數排序是穩定的,輔助空間n+k,這個空間是比較大的,計數排序對待排序序列有約束條件(如前面我們假設待排序序列a中值的範圍[0,k],其中k表示待排序序列中的最大值),元素值需是非負數,k太大的話會大大降低效率。這裏要注意的是 “掃描a數組把各個元素放在有序序列相應的位置上” 這步爲什麼要從後往前掃描a數組呢?大家想一想計數排序的過程就知道,因爲從前掃描導致計數排序不穩定,前面說了,計數排序是基數排序的基礎,所以它的穩定性直接影響到基數排序的穩定。

3、基數排序

          3.1 引出

            在計數排序中,當k很大時,時間和空間的開銷都會增大(可以想一下對序列{8888,1234,9999}用計數排序,此時不但浪費很多空間,而且時間方面還不如比較排序)。於是可以把待排序記錄分解成個位(第一位)、十位(第二位)....然後分別以第一位、第二位...對整個序列進行計數排序。這樣的話分解出來的每一位不超過9,即用計數排序序列中最大值是9.

          3.2 代碼

[cpp] view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<math.h>  
  4.   
  5.   
  6. //計數排序,n爲數組a的記錄個數,k爲記錄中最大值,按第d位排序  
  7. void countingSort(int *a,int n,int k,int d)  
  8. {  
  9.     int i;  
  10.     int *count=(int *)malloc(sizeof(int)*(k+1));  
  11.     int *b=(int *)malloc(sizeof(int)*(n+1));  
  12.     //初始化計數數組count  
  13.     for(i=0;i<=k;i++)  
  14.         *(count+i)=0;  
  15.     //計算等於a[i]在d位(a[i]/(int)pow(10,d-1)%10)的記錄個數  
  16.     for(i=1;i<=n;i++)  
  17.         (*(count+a[i]/(int)pow(10,d-1)%10))++;  
  18.   
  19.     //計算小於等於a[i]在d位(a[i]/(int)pow(10,d-1)%10)的記錄個數  
  20.     for(i=1;i<=k;i++)  
  21.         *(count+i) += *(count+i-1);  
  22.     //掃描a數組,把各個元素放在有序序列中相應的位置上  
  23.     for(i=n;i>=1;i--){  
  24.         *(b + *(count + a[i]/(int)pow(10,d-1)%10))=a[i];  
  25.         (*(count+a[i]/(int)pow(10,d-1)%10))--;   
  26.     }  
  27.     for(i=1;i<=n;i++)  
  28.         a[i]=*(b+i);  
  29.     free(count);  
  30.     free(b);  
  31. }  
  32.   
  33.   
  34. //基數排序,n爲數組a的記錄個數,每一個記錄中有d位數字  
  35. void radixSort(int *a,int n,int d)  
  36. {  
  37.     int i;  
  38.     for(i=1;i<=d;i++){  
  39.         countingSort(a,6,9,i);  
  40.     }  
  41. }  
  42.   
  43. void main()  
  44. {  
  45.     int i;  
  46.     int a[7]={0,114,118,152,114,111,132};//不考慮a[0]  
  47.     radixSort(a,6,3);  
  48.     for(i=1;i<=6;i++)  
  49.         printf("%-4d",a[i]);  
  50.     printf("\n");  
  51. }  


          3.3 效率分析

基數排序時間T(n)=d*(2k+3n),其中d是記錄值的位數,(2k+3n)是每一趟計數排序時間,上文分析過了,k不超過9,d的值一般也很小,k、d都可以看成是一個很小的常數,所以時間複雜度o(n)。最壞最佳情況並不改變時間複雜度。基數排序是穩定的。輔助空間同計數排序k+n.

4、桶排序

          4.1 引出

            同計數排序一樣,桶排序也對待排序序列作了假設,桶排序假設序列由一個隨機過程產生,該過程將元素均勻而獨立地分佈在區間[0,1)上。基本思想是:把區間[0,1)劃分成n個相同大小的子區間,稱爲桶。將n個記錄分佈到各個桶中去。如果有多於一個記錄分到同一個桶中,需要進行桶內排序。最後依次把各個桶中的記錄列出來記得到有序序列。

          4.2 代碼

[cpp] view plain copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3.   
  4. //桶排序  
  5. void bucketSort(double* a,int n)  
  6. {  
  7.     //鏈表結點描述  
  8.     typedef struct Node{  
  9.         double key;  
  10.         struct Node * next;   
  11.     }Node;  
  12.     //輔助數組元素描述  
  13.     typedef struct{  
  14.          Node * next;  
  15.     }Head;  
  16.     int i,j;  
  17.     Head head[10]={NULL};  
  18.     Node * p;  
  19.     Node * q;  
  20.     Node * node;  
  21.     for(i=1;i<=n;i++){  
  22.         node=(Node*)malloc(sizeof(Node));  
  23.         node->key=a[i];  
  24.         node->next=NULL;  
  25.         p = q =head[(int)(a[i]*10)].next;  
  26.         if(p == NULL){  
  27.             head[(int)(a[i]*10)].next=node;  
  28.             continue;  
  29.         }  
  30.         while(p){  
  31.             if(node->key < p->key)  
  32.                 break;  
  33.             q=p;  
  34.             p=p->next;  
  35.         }  
  36.         if(p == NULL){  
  37.             q->next=node;  
  38.         }else{  
  39.             node->next=p;  
  40.             q->next=node;  
  41.         }  
  42.     }  
  43.     j=1;  
  44.     for(i=0;i<10;i++){  
  45.         p=head[i].next;  
  46.         while(p){  
  47.             a[j++]=p->key;  
  48.             p=p->next;  
  49.         }  
  50.     }  
  51. }  
  52.   
  53. void main()  
  54. {  
  55.     int i;  
  56.     double a[13]={0,0.13,0.25,0.18,0.29,0.81,0.52,0.52,0.83,0.52,0.69,0.13,0.16};//不考慮a[0]  
  57.     bucketSort(a,12);  
  58.     for(i=1;i<=12;i++)  
  59.         printf("%-6.2f",a[i]);  
  60.     printf("\n");  
  61. }  


          4.3 效率分析

當記錄在桶中分佈均勻時,即每個桶只有一個元素,此時時間複雜度o(n)。因此桶排序適合對很少重複的記錄排序。輔助空間2n。桶排序是穩定的排序,實現比較複雜。

5、附錄

      參考書籍:  《算法導論》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章