基數排序的效能優化

2021-09-23 05:52:01 字數 4600 閱讀 7591

最近需要對大小在0到100萬內的很多陣列進行排序,每乙個陣列的長度都不固定,短則幾十,長則幾千。為了最快完成排序,需要將陣列大小和資料範圍考慮進去。由於快速排序是常規排序中速度最快的,首選肯定是它。但是陣列中資料的範圍固定,可以考慮基數排序。為了使排序耗時盡可能短,需要測試這兩種排序演算法。

快排是面試過程中常考的手寫**,需要背得滾瓜爛熟,**如下:

void swap(int* a,int *b)  

void q_sort(int* a,int left,int right)

while(i<=right&&a[i]=left&&a[j]>pivot);

if(i>=j) break;

swap(&a[i],&a[j]);

} a[left]=a[j];

a[j]=pivot;

q_sort(a,left,j-1);

q_sort(a,j+1,right);

}

void quick_sort(int* a,int n)

下面著重介紹基數排序。關於基數排序的原理和兩種不同實現可以參考:基數排序,該部落格詳細介紹了lsd(least

significant digital)或msd(most significant digital)兩種基數排序的原理和**實現。由於lsd更符合大家的思維方式,所以對比時也採用lsd。最初版本的lsd**如下:

const static int radix=100;  

int get_part(int n,int i)

void radix_sort(int* a,int n)

memcpy(a,bucket,sizeof(int)*n);

} free(bucket);

free(count);

}

下面詳細分析一下上面的基數排序**。首先定義常量radix用來表示選擇的基數,上面的**一開始選擇的基數為100。很多**在使用基數排序的時候總是預設基數為10,但是這樣往往會提高複雜度,後面會詳細分析10為基的壞處。之後定義了乙個get_part函式,用來獲得資料的不同部分。最後是具體的基數排序函式。通過函式體可以很清楚的看出基數排序的複雜度,上面**給出的複雜度為o(3*(3n+r))。其中外面的3表示範圍在100萬以內的數通過基數100分解最多隻需要分解三次;裡面的3n+r表示每次迴圈的複雜度。for迴圈內部有三個小的for迴圈,複雜度分別為n、r和n。此外還有乙個memset呼叫,複雜度也是n,因而每次迴圈總的複雜度是3n+r。

由上面的分析,我們可以獲得基數排序在資料任意大小時的複雜度為:

其中,r為基數,n為陣列長度,max為陣列最大值。由此我們可以看出,影響基數排序的因素有三個:陣列長度,基數大小和陣列最大值。優化基數排序的方法也就是從這三方面入手。

在介紹優化之前,我們先對比原始的基數排序和快速排序的效能,測試結果如表一:

測試結果很讓人吃驚,基數排序完全比不上快速排序,效能差距而且不小,這很奇怪。如果看**,基數排序的複雜度確實很低,但是效能卻比較差勁。不過好的一點是,基數排序的效能確實滿足線性增長規律。

優化一:避免記憶體分配

原始基數排序中桶變數count使用malloc分配空間,我們首先將該動態記憶體分配改為棧上固定記憶體分配,基數排序的前後效能對比如表二。可以看出,通過改為固定記憶體分配,基數排序的效能有小幅提公升,但是這還不足以與快速排序相匹配。我們還需要考慮別的優化技巧。

優化二:修改基數

通過基數排序的複雜度可以看出,影響複雜度的很大乙個引數是基數的選擇。很多人在使用基數排序時都會預設基數為10,但是這樣會顯著增大演算法複雜度的常量,因而在陣列長度較大時,選用較大的基數可能會使效能更好。我們將演算法的基數改為1000,效能對比圖如表三。可以看出,通過修改基數,基數排序的效能有很大提公升,當陣列長度大於10000時,基數排序的效能已經超過快速排序。

但是這還不足以說明基數排序的優勢。按複雜度推算,快速排序的複雜度為o(n*logn),基數排序複雜度在基為1000時複雜度為o(6n+2r),因而當元素個數在500左右時,兩者的效能就應該達到一樣,這說明演算法還有優化餘地。

優化三:避免複雜的數**算

基數排序中有乙個頻繁的操作是獲取整數的不同部分,該操作通過乙個

get_part函式獲得,函式**如下:

int get_part(int n,int i)  

上述函式有乙個複雜的pow系統呼叫,這可能會影響速度。但其實該操作就是計算基數的次方。在基數固定的前提下,我們可以將次方計算提前計算出來,每次通過查表來獲得基數的次方。為此,我們定義乙個常量陣列p用來儲存基數的次方:

static int p=;
當基數為1000時,上述陣列可以應對最大為10^12的整數。然後get_part函式就變為:

int get_part(int n,int i)  

在此測試兩種排序,效能對比如表四。這次可以看出,當元素為1000左右時,基數排序效能開始佔優;當陣列很長時,基數排序有很明顯的效能提公升,已經遠遠超過了快速排序。

優化四:除法變乘法

在get_part函式中有乙個除法取整的操作,一般情況下除法要比乘法更耗時,乙個優化技巧是將除法變成乘法。在此我們可以定義乙個常量陣列,用來儲存基數次方的倒數,這樣就可以將除法轉變成乘法:

static double rp=;
然後get_part函式就變為:

int get_part(int n,int i)  

再次測試基數排序前後的效能有表五。可以看出,避免除法操作又可以使效能獲得較大提公升。當陣列長度在400左右時,基數排序效能開始佔優。

優化五:內聯函式

可以看出

優化六:採用2的冪作為基數

在選擇基數時我們總會習慣性的以10的冪作為基數,這與我們平時多用10進製運算相符合。但是,計算機是以二進位制儲存資料的,所以當採用10的冪作為基數時就會出現很多問題,最明顯的就是上面出現的乘法除法問題。雖然我們對

get_part函式做了很多優化,但是還有乙個取餘操作尚未優化。昨天,經俊哥指點,我們完全可以採用2的冪作為基數,這樣就可以完全避免複雜的乘除法運算。

我們將基數改為1024,此時我們需要修改常量陣列p為:

static int p=;
然後get_part函式變為:

inline int get_part(int n,int i)  

可以看出,修改基數為1024之後,除法操作就變為了右移操作,取模操作就變成了與操作。直觀上,效能會有很大提公升;測試結果也是如此,如表七。當陣列長度在200左右時,基數排序效能開始佔優。

下面把調優之後的完整基數排序**列出:

const static int radix=1024;  

static int p=;

inline int get_part(int n,int i)

void radix_sort(int* a,int n)

memcpy(a,bucket,sizeof(int)*n);

} free(bucket);

}

再次執行快速排序和上述**,獲得乙個效能對比圖:

總結通過上面的分析我們可以看出,基數排序確實符合它線性複雜度的優勢。如果我們知曉整數陣列元素的範圍,基數排序確實是乙個很好的選擇。但是要獲得好的效能並不是特別容易,需要很多優化技巧。最好的優化方法就是選擇2的冪作為基數。在具體應用時,我們要根據實際的資料範圍去合理的選擇基數,在確定基數之後再去考慮需要迴圈的次數。在上面的對比中,資料範圍在100w以內,因而迴圈只有兩次,所以快速排序和基數排序的效能差異接近5倍;如果在10億以內,則需要三次迴圈,效能差異可能就會降為3倍左右。其實,10億以內的數幾乎快覆蓋了int型整數的範圍;如果基數選擇為2048,則三次迴圈就完全覆蓋了整個int型整數範圍。所以,如果要排序的資料範圍很大,但是資料量又不足以使用計數排序時,可以考慮採用基數為2048的基數排序。

演算法 基數排序及優化

基數排序的本質就是按位數諸位分離歸類,直到最高位也統計完成,則整體排序完成 準備10個空桶,對應數字0 9,即乙個二維陣列。第一次以個位為標準,遍歷arr,把個位為0的數字都放進乙個子桶,個位為1的都放進另乙個子桶,以此類推,直到個位為9的也都放進乙個子桶,最後把桶裡的數按序取出 第二次以十位為標準...

排序 基數排序

基數排序 radix sort 是屬於 分配式排序 distribution sort 基數排序法又稱 桶子法 bucket sort 或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些 桶 中,藉以達到排序的作用。排序思想 首先按照資料的最低位 個位 將資料分配到0 9...

排序 基數排序

1 基數排序 桶排序 介紹 1 基數排序 radix sort 屬於 分配式排序 distribution sort 又稱 桶子法 bucket sort 或bin sort,顧名思義,它是通過鍵值的各個位的值,將要排序的元素分配至某些 桶 中,達到排序的作用 2 基數排序法是屬於穩定性的排序,基數...