線性排序算法是不用進行元素比較的算法,因此它的時間複雜度不受Ω(NlogN)的限制,它的排序時間複雜度是線性 。這裏將會介紹最常見的三種線性排序算法:桶排序,計數排序,基數排序。
一 桶排序
桶排序(bucke sort)假設輸入數據服從均勻分佈,平均情況下它的時間代價爲O(N),最壞爲O(N^2)。桶排序將[0,1)區間換分爲k個相同大小的子區間,也叫桶。然後根據輸入的數據,將它們放在各個桶中,再對每個桶單獨排序,最後遍歷每個桶,就得到了結果。實際中,並不一定按0-1區分,也可以按照輸入值的範圍進行桶大小的確定。比如,輸入的數據範圍是0-99,這裏用10個桶,第一個桶裝0-9,第二個裝10-19,以此類堆。因爲每個桶中可能會有多個數,因此需要用其他數據結構裝載,這裏用鏈表,每個桶中用快排來排序。如果數據分佈不均勻,把所有數據放在一個桶中,時間代價就是O(N^2)。Java代碼如下:
//桶排序 假設a數組的值在0-99之間
//可以設10個桶 桶0裝0-10,桶1裝1-20,以此類推,相同的值需要用到鏈表,桶值爲鏈表
public static void bucketSort(int[] a)
{
LinkedList[] ll=new LinkedList[10]; //不能創建泛型數組,要將類型變量去掉才行
for(int i=0;i<a.length;i++) //將每個值插入到桶中,如果該桶(鏈表)未建立,則建立再插入
{
int tmp=a[i]/10;
if(ll[tmp]!=null)
{
ll[tmp].add(a[i]);
}
else
{
ll[tmp]=new LinkedList<Integer>();//等有數據插入時才創建
ll[tmp].add(a[i]);
}
}
for(int i=0;i<10;i++) //對每個桶在進行排序
{
if(ll[i]!=null)
Collections.sort(ll[i]);
}
int c=0;
for(int i=0;i<10;i++) //排序好的輸回到數組a中
{
while(ll[i]!=null && ll[i].size()!=0)
{
a[c]=(Integer)ll[i].pop();
c++;
}
}
//輸出
for(int e:a)
System.out.print(e+" ");
}
結果:
桶排序就適合於那些分佈均勻的數據。
二 計數排序
計數排序(counting sort)假設每個元素都在0-k區間,建立一個臨時數組c[0..k],將元素值作爲數組c的座標(其實類似於Hash),c[i]存放小於或者等於i的元素的個數。最後輸出時數組c其實就是存儲了各個元素的位置,還需要一個臨時數據b作爲輸出。計數排序適用於k不大的情況,負責考慮採用基數排序。
//計數排序 顧名思義,對數組中的每個值存儲它的個數
//假設輸入值0-k,則要建立數組c[k+1](爲<=i的值的個數)存儲個數,建立B[]數組存放輸出
public static void countingSort(int[] a)
{
int[] b=new int[a.length];
int[] c=new int[21]; //假設輸入a的範圍:0-20
for(int i=0;i<a.length;i++)
{
c[a[i]]=c[a[i]]+1; //這裏c[i]代表值i的個數,下面的循環將c[i]變爲<=i的值的個數
}
for(int i=1;i<21;i++)
{
c[i]=c[i]+c[i-1];
}
for(int i=a.length-1;i>=0;i--) //倒序輸出是保證穩定性
{
b[c[a[i]]-1]=a[i]; //下標從0開始,而c[i]存儲的是長度,這裏b[]裏面減一
c[a[i]]=c[a[i]]-1;
}
//輸出
for(int e:b)
System.out.print(e+" ");
}
結果:
它的時間複雜度O(n+k)(最壞和平均都是它),k爲元素最大值。此外,計數排序是穩定的,輔助空間n+k,這個空間是比較大的,計數排序對待排序序列有約束條件(如前面我們假設待排序序列a中值的範圍[0,k],其中k表示待排序序列中的最大值),元素值需是非負數,k太大的話會大大降低效率。
三 基數排序
在計數排序中,如果k太大則不能使用,比如{8888,1234,9999}用計數排序,此時不但浪費很多空間,而且時間方面還不如比較排序。於是可以把待排序記錄分解成個位(第一位)、十位(第二位)....然後分別以第一位、第二位...對整個序列進行排序。這樣的話分解出來的每一位不超過9,即用計數排序序列中最大值是9。對每一位的排序可以使用計數排序,也可以用桶排序。下面的程序使用計數排序。
//基數排序 將數字按位數排序 可以從高位到低位,也可以從低位到高位
//每一個位數的排序再採用穩定排序,可以用桶排序或者計數排序
//如果給的數值過大,計數排序不再適用,可考慮基數排序
//假設a的值最大是9999
public static void radixSort(int[] a)
{
int[] c=new int[10];
int[] b=new int[a.length];
for(int j=0;j<4;j++)
{
int ss=(int)Math.pow(10,j);
for(int i=0;i<a.length;i++)
{
c[a[i]/ss%10]=c[a[i]/ss%10]+1; //這裏c[i]代表值i的個數,下面的循環將c[i]變爲<=i的值的個數
}
for(int i=1;i<10;i++)
{
c[i]=c[i]+c[i-1];
}
for(int i=a.length-1;i>=0;i--) //倒序輸出是保存穩定性
{
b[c[a[i]/ss%10]-1]=a[i]; //下標從0開始,而c[i]存儲的是長度,這裏b[]裏面減一
c[a[i]/ss%10]=c[a[i]/ss%10]-1;
}
for(int i=0;i<a.length;i++) //將按位排好的值重新賦給數組a
a[i]=b[i];
for(int i=0;i<10;i++) //清空數組c
c[i]=0;
}
//輸出
for(int e:b)
System.out.print(e+" ");
}
結果:
基數排序也是穩定排序,它不是原址排序,時間複雜度O(d(n+k))(最壞和平均)。n是元素的個數,d是數據的位數,d一般不會很大。輔助空間n+k。那選擇基數排序還是比較排序呢?不僅要比較d和logn的大小,還要比較空間的使用。
總結:
上面的程序裏的參數還需要根據輸入元素的區間範圍再去修改,三種線性排序都是穩定排序。選擇排序、快速排序、希爾排序、堆排序不是穩定的排序算法;冒泡,插入,歸併和三種線性排序都是穩定排序算法。
參考:
《算法導論》