上次把時(shí)間復(fù)雜度趨近于O(nlogn)的算法寫完了,這次先接著把時(shí)間復(fù)雜度O(n)的兩個(gè)算法寫完。它們分別是計(jì)數(shù)排序和基數(shù)排序,由于O(nlogn)已經(jīng)是基于比較的排序算法的下限,這兩個(gè)算法都不需要比較。
一、計(jì)數(shù)排序
時(shí)間復(fù)雜度O(n),空間復(fù)雜度O(k),k是輸入序列的值的范圍(最大值-最小值),是穩(wěn)定的。計(jì)數(shù)排序一般用于已知輸入值的范圍相對(duì)較小,比如給公司員工的身高體重信息排序。
算法導(dǎo)論里面講的計(jì)數(shù)排序大致是這樣的思路:
假設(shè)n個(gè)輸入元素中的每一個(gè)都是介于0到k之間的整數(shù),對(duì)每一個(gè)輸入元素x,確定出小于x的元素個(gè)數(shù),有了這一信息,就可以把x直接放在它在最終輸出數(shù)組的位置上,例如,如果有17個(gè)元素小于x,則x就是屬于第18個(gè)輸出位置。當(dāng)幾個(gè)元素相同是,方案要略作修改。
我感覺這種方法不太好理解,而且代碼寫起來也麻煩。所以我下面的代碼是用桶排序的思想實(shí)現(xiàn)的。
這種實(shí)現(xiàn)有點(diǎn)像散列里的鍵值轉(zhuǎn)換思想,把輸入的值映射成桶的序號(hào)(桶數(shù)組的下標(biāo)),然后再遍歷一遍桶數(shù)組把元素倒出。過程大致是根據(jù)輸入序列的值的范圍k,創(chuàng)建k個(gè)桶(用大小為k的數(shù)組表示,下標(biāo)表示桶的序號(hào),值表示該桶里元素個(gè)數(shù))。遍歷一遍輸入數(shù)組,放入相應(yīng)位置的桶里,再把桶按順序倒出來即可。
舉個(gè)例子,假設(shè)輸入數(shù)組A為{3,5,1,2,4,3},值的范圍是1~5,所以創(chuàng)建5個(gè)桶,序號(hào)1,2,3,4,5。裝桶時(shí)遍歷一遍輸入數(shù)組,A[0]=3,把它放到3號(hào)桶;A[1]=5,放到5號(hào)桶;1放到1號(hào)桶……最后3放到3號(hào)桶。現(xiàn)在三號(hào)桶的值為2,其他桶的值為1,再遍歷一遍桶數(shù)組,按順序把桶倒出,元素被倒出的順序就是排序的順序了。
import java.util.*;
public class CountingSort {
public int[] countingSort(int[] A, int n) {
//找數(shù)組中的最大值和最小值,確定桶的個(gè)數(shù)
int max=A[0];
int min=A[0];
for(int i=0;i<n;i++){
if(A[i]>max)
max=A[i];
if(A[i]<min)
min=A[i];
}
//定義桶數(shù)組B并初始化
int[] B= new int[max-min+1];
for(int i=0;i<max-min+1;i++)
B[i]=0;
//把數(shù)組A的元素裝到對(duì)應(yīng)桶里
for(int i=0;i<n;i++){
B[A[i]-min]++;
}
//把所有桶倒出來
for(int i=0,j=0;j<max-min+1;j++){
//倒桶j
for(int k=B[j];k>0;k--){
A[i++]=j+min;
}
}
return A;
}
}
二、基數(shù)排序
1.多關(guān)鍵字排序思想
基數(shù)排序時(shí)一種借助多關(guān)鍵字排序的思想對(duì)單邏輯關(guān)鍵字進(jìn)行排序的方法。按照關(guān)鍵字先后順序,基數(shù)排序可以分為高位優(yōu)先法和低位優(yōu)先法。
比如說,對(duì)撲克牌排序,規(guī)定花色(假設(shè)黑桃>梅花>紅桃>方片)優(yōu)先級(jí)高于面值,花色是主關(guān)鍵字,面值是次關(guān)鍵字。我們可以先按照花色將撲克牌分為有序的四堆,再對(duì)每一堆分別按照面值排序。這就是所謂的高位優(yōu)先法。是不是感覺和歸并有點(diǎn)像?這是因?yàn)檫@里也是利用了分治法思想進(jìn)行的排序。
低位優(yōu)先法則與上面相反。先按面值把撲克牌分成13堆(叫分配操作),再按順序把這13堆從小到大疊起來(收集),然后再對(duì)花色執(zhí)行分配收集操作,然后撲克就成有序的了。
再看值是數(shù)字的例子。假設(shè)輸入為{234,112,73,252,31},我們可以把個(gè)位,十位,百位上的數(shù)字看作四個(gè)關(guān)鍵字,優(yōu)先級(jí)依次升高。按上面的思想,先按百位分成三堆{73,31;112;234,252}(0;1;2),在每一堆內(nèi)再分別按十位分,{31,73;112;234,252},再按個(gè)位分。這就是高位優(yōu)先,低位優(yōu)先和上面也是一樣的道理。
一般來說低位優(yōu)先法要比高位簡(jiǎn)單。因?yàn)楦呶粌?yōu)先是分治法思想,對(duì)子集按相同方法排序,是一個(gè)遞歸的過程。而低位優(yōu)先法只要通過x次分配收集操作就可以完成排序,x是取決于關(guān)鍵字的多少。
2.桶排序思想實(shí)現(xiàn)基數(shù)排序
書上講的基數(shù)排序算法就是利用桶的思想實(shí)現(xiàn)低位優(yōu)先法,按照輸入數(shù)值的各個(gè)位作為關(guān)鍵字進(jìn)行排序。時(shí)間復(fù)雜度O(xn)=O(n),因?yàn)槊看畏峙涫占僮鞫际蔷€性時(shí)間,關(guān)鍵詞個(gè)數(shù)x也是常量。空間復(fù)雜度也大大減小,為O(1),桶的個(gè)數(shù)也是常量。是個(gè)穩(wěn)定的排序算法。但是由于基數(shù)排序非常不靈活,輸入數(shù)據(jù)類型稍有變化,可能就要重寫,重新規(guī)定關(guān)鍵字等問題。所以基數(shù)排序并沒有其他算法使用廣泛。
下面講算法的實(shí)現(xiàn)。也是利用桶排序思想。假設(shè)輸入數(shù)據(jù)都是十進(jìn)制的,都是四位數(shù)以內(nèi)的數(shù)。我們準(zhǔn)備十個(gè)桶,序號(hào)0,1,2,3,4,5,6,7,8,9。我們首先根據(jù)個(gè)位上的數(shù)值選擇進(jìn)入哪一個(gè)桶,然后按順序倒出,進(jìn)行第一次排序。然后再根據(jù)十位上的數(shù)進(jìn)入桶,再倒出。然后百位,千位,進(jìn)行四次分配收集。
計(jì)數(shù)排序算法里的桶只需要存儲(chǔ)元素的個(gè)數(shù),所以一個(gè)int值即可。但現(xiàn)在桶里面要存儲(chǔ)元素的數(shù)值,所以每個(gè)桶就要用別的方式表示了。我在下面的代碼中寫的是
int [][] B = new int [10][n];
就是創(chuàng)建了十個(gè)桶,每個(gè)桶是大小為n的數(shù)組,因?yàn)槊總€(gè)桶里最多可能要放n個(gè)元素。但是這樣寫空間開銷很大,大量空間都被浪費(fèi)了。更好的做法應(yīng)該是用鏈表來存儲(chǔ)桶里的元素(但是我是寫完才意識(shí)到的,就懶得改了。。。)。而且桶應(yīng)該是用一個(gè)隊(duì)列表示的。其實(shí)如果是在線做題的話,完全可以用一個(gè)Queue類表示,很簡(jiǎn)單。
我之前在做在線題的時(shí)候有一次需要用到堆,然后我自己就寫了一個(gè)堆,花時(shí)間不說,還出錯(cuò)了。。。但其實(shí)完全可以用優(yōu)先隊(duì)列類輔助。面試的話面試官可能想考察你堆的掌握,但在線題只要OC就行了嘛。畢竟我們用的是java。反思自己還是對(duì)java不夠熟悉,做算法題還是停留在c/c++的思維里,并沒有利用java的特性。下面上代碼,因?yàn)閷懙臅r(shí)候思路不清晰,而且沒用鏈表隊(duì)列表示桶,所以可能有點(diǎn)亂,大家湊乎看。
package fuckingtest;
import java.util.*;
public class RadixSort {
public int[] radixSort(int[] A, int n) {
//定義桶數(shù)組B并初始化,每個(gè)桶的第一個(gè)元素表示桶內(nèi)元素個(gè)數(shù)
int[][] B= new int[10][n+1];
for(int i=0;i<10;i++)
for(int j=0;j<n+1;j++)
B[i][j]=0;
for(int x=1;x<=4;x++){
//把數(shù)組A的第x位元素裝到對(duì)應(yīng)桶里
for(int i=0;i<n;i++){
B[getNum(A[i],x)][0]++;
B[getNum(A[i],x)][B[getNum(A[i],x)][0]]=A[i];
}
//把所有桶倒出來
for(int i=0,j=0;j<10;j++){
//倒桶j
for(int k=1;B[j][0]>0&&k<=B[j][0];k++){
A[i++]=B[j][k];
}
B[j][0]=0;
}
}
return A;
}
int getNum(int x,int d){
int[] a={1,1,10,100,1000};
System.out.println((x/a[d])%10);
return (x/a[d])%10;
}
public static void main(String[] args) {
int[] r=new int[6];
int[] a=new int[]{109,551,32,4,294,6};
RadixSort h = new RadixSort();
r = h.radixSort(a,6);
for(int t:r){
System.out.println(t);
}
}
}