桶排序、計(jì)數(shù)排序、基數(shù)排序和前面講的那些排序有所不同,不是基于比較的排序算法,而是一種線性排序。他們的時(shí)間復(fù)雜度更低:O(n),但是對(duì)要排序的數(shù)據(jù)要求很苛刻,所以我們今天學(xué)習(xí)重點(diǎn)的是掌握這些排序算法的適用場(chǎng)景
。
桶排序
首先,我們來(lái)看桶排序。桶排序,顧名思義,會(huì)用到“桶”,核心思想是將要排序的數(shù)據(jù)分到幾個(gè)有序的桶里,每個(gè)桶里的數(shù)據(jù)再單獨(dú)進(jìn)行排序。桶內(nèi)排完序之后,再把每個(gè)桶里的數(shù)據(jù)按照順序依次取出,組成的序列就是有序的了。
桶排序的時(shí)間復(fù)雜度為什么是O(n)呢?我們一塊兒來(lái)分析一.下。
如果要排序的數(shù)據(jù)有n個(gè),我們把它們均勻地劃分到m個(gè)桶內(nèi),每個(gè)桶里就有k=n/m個(gè)元素。每個(gè)桶內(nèi)部使用快速排序,時(shí)間復(fù)雜度為O(k * logk)。m個(gè)桶排序的時(shí)間復(fù)雜度就是O(m*k logk),因?yàn)閗=n/m,所以整個(gè)桶排序的時(shí)間復(fù)雜度就是0(nlog(n/m))。當(dāng)桶的個(gè)數(shù)m接近數(shù)據(jù)個(gè)數(shù)n時(shí),log(n/m) 就是一個(gè)非常小的常量,這個(gè)時(shí)候桶排序的時(shí)間復(fù)雜度接近0(n)。
桶排序看起來(lái)很優(yōu)秀,那它是不是可以替代我們之前講的排序算法呢?
答案當(dāng)然是否定的。為了讓你輕松理解桶排序的核心思想,我剛才做了很多假設(shè)。實(shí)際上,桶排序?qū)σ判驍?shù)據(jù)的要求是非常苛刻的。
首先,要排序的數(shù)據(jù)需要很容易就能劃分成m個(gè)桶,并且,桶與桶之間有著天然的大小順序。這樣每個(gè)桶內(nèi)的數(shù)據(jù)都排序完之后,桶與桶之間的數(shù)據(jù)不需要再進(jìn)行排序。
其次,數(shù)據(jù)在各個(gè)桶之間的分布是比較均勻的。如果數(shù)據(jù)經(jīng)過(guò)桶的劃分之后,有些桶里的數(shù)據(jù)非常多,有些非常少,很不平均,那桶內(nèi)數(shù)據(jù)排序的時(shí)間復(fù)雜度就不是常量級(jí)了。在極端情況下,如果數(shù)據(jù)都被劃分到一個(gè)桶里,那就退化為O(nlogn)的排序算法了。
桶排序比較適合用在外部排序中
所謂的外部排序就是數(shù)據(jù)存儲(chǔ)在外部磁盤中,數(shù)據(jù)量比較大,內(nèi)存有限,無(wú)法將數(shù)據(jù)全部加載到內(nèi)存中。
比如說(shuō)我們有10GB的訂單數(shù)據(jù),我們希望按訂單金額(假設(shè)金額都是正整數(shù))進(jìn)行排序,但是我們的內(nèi)存有限,只有幾百M(fèi)B,沒(méi)辦法- - -次性把10GB的數(shù)據(jù)都加載到內(nèi)存中。這個(gè)時(shí)候該怎么辦呢?
現(xiàn)在我來(lái)講一下,如何借助桶排序的處理思想來(lái)解決這個(gè)問(wèn)題。
我們可以先掃描一遍文件,看訂單金額所處的數(shù)據(jù)范圍。假設(shè)經(jīng)過(guò)掃描之后我們得到,訂單金額最小是1元,最大是10萬(wàn)元。我們將所有訂單根據(jù)金額劃分到100個(gè)桶里,第一個(gè)桶我們存儲(chǔ)金額在1元到1000元之內(nèi)的訂單,第二桶存儲(chǔ)金額在1001元到2000元之內(nèi)的訂單,以此類推。每一個(gè)桶對(duì)應(yīng)一個(gè)文件,并且按照金額范圍的大小順序編號(hào)命名(00, 01, 02...99) 。
理想的情況下,如果訂單金額在1到10萬(wàn)之間均勻分布,那訂單會(huì)被均勻劃分到100個(gè)文件中,每個(gè)小文件中存儲(chǔ)大約100MB的訂單數(shù)據(jù),我們就可以將這100個(gè)小文件依次放到內(nèi)存中,用快排來(lái)排序。等所有文件都排好序之后,我們只需要按照文件編號(hào),從小到大依次讀取每個(gè)小文件中的訂單數(shù)據(jù),并將其寫(xiě)入到一個(gè)文件中,那這個(gè)文件中存儲(chǔ)的就是按照金額從小到大排序的訂單數(shù)
據(jù)了。
不過(guò),你可能也發(fā)現(xiàn)了,訂單按照金額在1元到10萬(wàn)元之間并不一-定是均勻分布的,所以10GB訂單數(shù)據(jù)是無(wú)法均勻地被劃分到100個(gè)文件中的。有可能某個(gè)金額區(qū)間的數(shù)據(jù)特別多,劃分之后對(duì)應(yīng)的文件就會(huì)很大,沒(méi)法一次性讀入內(nèi)存。這又該怎么辦呢?
針對(duì)這些劃分之后還是比較大的文件,我們可以繼續(xù)劃分,比如,訂單金額在1元到1000元之間的比較多,我們就將這個(gè)區(qū)間繼續(xù)劃分為10個(gè)小區(qū)間,1元到100元,101 元到200元,201 元到300元...901元到1000元。如果劃分之后,101 元到200元之間的訂單還是太多,無(wú)法- -次性讀入內(nèi)存,那就繼續(xù)再劃分,直到所有的文件都能讀入內(nèi)存為止。
代碼如下:
#pragma mark -
#pragma mark 桶排序
- (void)bucketSort:(NSMutableArray *)datasource
{
//預(yù)計(jì)每個(gè)桶內(nèi)能裝3個(gè)
NSInteger size = 3;
//桶的數(shù)量
NSInteger bucketsCount = datasource.count / size;
//找出最小值和最大值
NSInteger min = [datasource[0] integerValue];
NSInteger max = [datasource[0] integerValue];
for (NSNumber *number in datasource)
{
if (number.integerValue < min)
{
min = number.integerValue;
}
if (number.integerValue > max)
{
max = number.integerValue;
}
}
//平均值
NSInteger average = ceil((double)(max - min)/(double)bucketsCount);
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
for (NSInteger i = 0; i < bucketsCount; i++)
{
NSMutableArray *bucketArray = [NSMutableArray array];
NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
[dictionary setValue:bucketArray forKey:key];
}
for (NSNumber *number in datasource)
{
NSInteger i = floor((double)(number.integerValue - min) / (double)average);
NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
NSMutableArray *bucketArray = [dictionary valueForKey:key];
[bucketArray addObject:number];
}
NSInteger length = 0;
for (NSInteger i = 0; i < dictionary.allKeys.count; i++)
{
NSString *key = [NSString stringWithFormat:@"%@-%@",@(min + i * average),@(min + (i + 1) * average)];
NSMutableArray *bucketArray = [dictionary objectForKey:key];
[self quickSort:bucketArray];
[datasource replaceObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(length, bucketArray.count)] withObjects:bucketArray];
length += bucketArray.count;
}
}
計(jì)數(shù)排序
我個(gè)人覺(jué)得,計(jì)數(shù)排序其實(shí)是桶排序的-種特殊情況
。當(dāng)要排序的n個(gè)數(shù)據(jù),所處的范圍并不大的時(shí)候,比如最大值是k,我們就可以把數(shù)據(jù)劃分成k個(gè)桶。每個(gè)桶內(nèi)的數(shù)據(jù)值都是相同的,省掉了桶內(nèi)排序的時(shí)間。
我們都經(jīng)歷過(guò)高考,高考查分?jǐn)?shù)系統(tǒng)你還記得嗎?我們查分?jǐn)?shù)的時(shí)候,系統(tǒng)會(huì)顯示我們的成績(jī)以及所在省的排名。如果你所在的省有50萬(wàn)考生,如何通過(guò)成績(jī)快速排序得出名次呢?
考生的滿分是900分,最小是0分,這個(gè)數(shù)據(jù)的范圍很小,所以我們可以分成901個(gè)桶,對(duì)應(yīng)分?jǐn)?shù)從0分到900分。根據(jù)考生的成績(jī),我們將這50萬(wàn)考生劃分到這901個(gè)桶里。桶內(nèi)的數(shù)據(jù)都是分?jǐn)?shù)相同的考生,所以并不需要再進(jìn)行排序。我們只需要依次掃描每個(gè)桶,將桶內(nèi)的考生依次輸出到一個(gè)數(shù)組中,就實(shí)現(xiàn)了50萬(wàn)考生的排序。因?yàn)橹簧婕皰呙璞闅v操作,所以時(shí)間復(fù)雜度是O(n)。
計(jì)數(shù)排序的算法思想就是這么簡(jiǎn)單,跟桶排序非常類似,只是桶的大小粒度不一樣。不過(guò),為什么這個(gè)排序算法叫“計(jì)數(shù)”排序呢?“計(jì)數(shù)”的含義來(lái)自哪里呢?
想弄明白這個(gè)問(wèn)題,我們就要來(lái)看計(jì)數(shù)排序算法的實(shí)現(xiàn)方法。我還拿考生那個(gè)例子來(lái)解釋。為了方便說(shuō)明,我對(duì)數(shù)據(jù)規(guī)模做了簡(jiǎn)化。假設(shè)只有8個(gè)考生,分?jǐn)?shù)在0到5分之間。這8個(gè)考生的成績(jī)我們放在一個(gè)數(shù)組A[8]中,它們分別是: 2, 5, 3, O, 2, 3, O, 3。
考生的成績(jī)從0到5分,我們使用大小為6的數(shù)組C[6]表示桶,其中下標(biāo)對(duì)應(yīng)分?jǐn)?shù)。不過(guò),C[6]內(nèi)存儲(chǔ)的并不是考生,而是對(duì)應(yīng)的考生個(gè)數(shù)。像我剛剛舉的那個(gè)例子,我們只需要遍歷一遍考生分?jǐn)?shù),就可以得到C[6]的值。
從圖中可以看出,分?jǐn)?shù)為 3 分的考生有 3 個(gè),小于 3 分的考生有 4 個(gè),所以,成績(jī)?yōu)?3 分的考生在排序之后的有序數(shù)組 R[8] 中,會(huì)保存下標(biāo) 4,5,6 的位置。
那我們?nèi)绾慰焖儆?jì)算出,每個(gè)分?jǐn)?shù)的考生在有序數(shù)組中對(duì)應(yīng)的存儲(chǔ)位置呢?這個(gè)處理方法非常巧
妙,很不容易想到。
思路是這樣的:我們對(duì)C[6]數(shù)組順序求和,C[6] 存儲(chǔ)的數(shù)據(jù)就變成了下面這樣子。C[k] 里存儲(chǔ)小
于等于分?jǐn)?shù)k的考生個(gè)數(shù)。
有了前面的數(shù)據(jù)準(zhǔn)備之后,現(xiàn)在我就要講計(jì)數(shù)排序中最復(fù)雜、最難理解的一部分了,請(qǐng)集中精力跟著我的思路!
我們從后到前依次掃描數(shù)組A。比如,當(dāng)掃描到3時(shí),我們可以從數(shù)組C中取出下標(biāo)為3的值7,也就是說(shuō),到目前為止,包括自己在內(nèi),分?jǐn)?shù)小于等于3的考生有7個(gè),也就是說(shuō)3是數(shù)組R中的第7個(gè)元素(也就是數(shù)組R中下標(biāo)為6的位置)。當(dāng)3放入到數(shù)組R中后,小于等于3的元素就只剩下了6個(gè)了,所以相應(yīng)的C[3]要減1,變成6。
以此類推,當(dāng)我們掃描到第2個(gè)分?jǐn)?shù)為3的考生的時(shí)候,就會(huì)把它放入數(shù)組R中的第6個(gè)元素的位置(也就是下標(biāo)為5的位置)。當(dāng)我們掃描完整個(gè)數(shù)組A后,數(shù)組R內(nèi)的數(shù)據(jù)就是按照分?jǐn)?shù)從小到大有序排列的了。
代碼如下:
#pragma mark -
#pragma mark 計(jì)數(shù)排序
- (void)countingSort:(NSMutableArray *)a
{
if (a.count <= 1)
{
return;
}
NSInteger max = [a[0] integerValue];
for (NSNumber *number in a)
{
if (number.integerValue > max)
{
max = number.integerValue;
}
}
NSMutableArray *c = [NSMutableArray array];
for (NSInteger i = 0; i <= max; i++)
{
[c addObject:@(0)];
}
NSMutableArray *r = [NSMutableArray array];
for (NSInteger i = 0; i < a.count; i++)
{
[r addObject:@(0)];
NSNumber *index = a[i];
NSNumber *count = c[index.integerValue];
[c replaceObjectAtIndex:index.integerValue withObject:@(count.integerValue + 1)];
}
for (NSInteger i = 1; i <= max; i++)
{
[c replaceObjectAtIndex:i withObject:@([c[i] integerValue] + [c[i - 1] integerValue])];
}
for (NSInteger i = a.count - 1; i >= 0; i--)
{
NSNumber *index = a[i];
NSInteger count = [c[index.integerValue] integerValue] - 1;
[r replaceObjectAtIndex:count withObject:a[i]];
[c replaceObjectAtIndex:index.integerValue withObject:@(count)];
}
for (NSInteger i = 0; i < a.count; i++)
{
[a replaceObjectAtIndex:i withObject:r[i]];
}
}
這種利用另外一個(gè)數(shù)組來(lái)計(jì)數(shù)的實(shí)現(xiàn)方式是不是很巧妙呢?這也是為什么這種排序算法叫計(jì)數(shù)排序的原因。不過(guò),你千萬(wàn)不要死記硬背上面的排序過(guò)程,重要的是理解和會(huì)用。
我總結(jié)一下,計(jì)數(shù)排序只能用在數(shù)據(jù)范圍不大的場(chǎng)景中,如果數(shù)據(jù)范圍k比要排序的數(shù)據(jù)n大很多,就不適合用計(jì)數(shù)排序了。而且,計(jì)數(shù)排序只能給非負(fù)整數(shù)排序,如果要排序的數(shù)據(jù)是其他類型的,要將其在不改變相對(duì)大小的情況下,轉(zhuǎn)化為非負(fù)整數(shù)。
比如,還是拿考生這個(gè)例子。如果考生成績(jī)精確到小數(shù)后- -位, 我們就需要將所有的分?jǐn)?shù)都先乘以10,轉(zhuǎn)化成整數(shù),然后再放到9010個(gè)桶內(nèi)。再比如,如果要排序的數(shù)據(jù)中有負(fù)數(shù),數(shù)據(jù)的范圍是[-1000, 1000],那我們就需要先對(duì)每個(gè)數(shù)據(jù)都加1000,轉(zhuǎn)化成非負(fù)整數(shù)。
基數(shù)排序
我們?cè)賮?lái)看這樣一個(gè)排序問(wèn)題。假設(shè)我們有10萬(wàn)個(gè)手機(jī)號(hào)碼,希望將這10萬(wàn)個(gè)手機(jī)號(hào)碼從小到大排序,你有什么比較快速的排序方法呢?
我們之前講的快排,時(shí)間復(fù)雜度可以做到O(nlogn),還有更高效的排序算法嗎?桶排序、計(jì)數(shù)排序能派上用場(chǎng)嗎?手機(jī)號(hào)碼有11位,范圍太大,顯然不適合用這兩種排序算法。針對(duì)這個(gè)排序問(wèn)題,有沒(méi)有時(shí)間復(fù)雜度是O(n)的算法呢?現(xiàn)在我就來(lái)介紹一種新的排序算法,基數(shù)排序。
剛剛這個(gè)問(wèn)題里有這樣的規(guī)律:假設(shè)要比較兩個(gè)手機(jī)號(hào)碼a, b的大小,如果在前面幾位中,a手機(jī)號(hào)碼已經(jīng)比b手機(jī)號(hào)碼大了,那后面的幾位就不用看了。
借助穩(wěn)定排序算法,這里有一個(gè)巧妙的實(shí)現(xiàn)思路。還記得我們第11節(jié)中,在闡述排序算法的穩(wěn)定性的時(shí)候舉的訂單的例子嗎?我們這里也可以借助相同的處理思路,先按照最后一位來(lái)排序手機(jī)號(hào)碼,然后,再按照倒數(shù)第二位重新排序,以此類推,最后按照第一位重新排序。經(jīng)過(guò)11次排序之后,手機(jī)號(hào)碼就都有序了。
手機(jī)號(hào)碼稍微有點(diǎn)長(zhǎng),畫(huà)圖比較不容易看清楚,我用字符串排序的例子,畫(huà)了一張基數(shù)排序的過(guò)程分解圖,你可以看下。
注意,這里按照每位來(lái)排序的排序算法要是穩(wěn)定的,否則這個(gè)實(shí)現(xiàn)思路就是不正確的。因?yàn)槿绻欠欠€(wěn)定排序算法,那最后一次排序只會(huì)考慮最高位的大小順序,完全不管其他位的大小關(guān)系,那么低位的排序就完全沒(méi)有意義了。
根據(jù)每一位來(lái)排序,我們可以用剛講過(guò)的桶排序或者計(jì)數(shù)排序,它們的時(shí)間復(fù)雜度可以做到O(n)。如果要排序的數(shù)據(jù)有k位,那我們就需要k次桶排序或者計(jì)數(shù)排序,總的時(shí)間復(fù)雜度是O(k*n)。當(dāng)k不大的時(shí)候,比如手機(jī)號(hào)碼排序的例子,k最大就是11,所以基數(shù)排序的時(shí)間復(fù)雜度就近似于O(n)。
實(shí)際上,有時(shí)候要排序的數(shù)據(jù)并不都是等長(zhǎng)的,比如我們排序牛津字典中的20萬(wàn)個(gè)英文單詞,最短的只有1個(gè)字母,最長(zhǎng)的我特意去查了下,有45個(gè)字母,中文翻譯是塵肺病。對(duì)于這種不等長(zhǎng)的數(shù)據(jù),基數(shù)排序還適用嗎?
實(shí)際上,我們可以把所有的單詞補(bǔ)齊到相同長(zhǎng)度,位數(shù)不夠的可以在后面補(bǔ)“0”,因?yàn)楦鶕?jù)ASClI值,所有字母都大于“O”,所以補(bǔ)“0”不會(huì)影響到原有的大小順序。這樣就可以繼續(xù)用基數(shù)排序了。
我來(lái)總結(jié)一下,基數(shù)排序?qū)σ判虻臄?shù)據(jù)是有要求的,需要可以分割出獨(dú)立的“位”來(lái)比較,而且位之間有遞進(jìn)的關(guān)系,如果a數(shù)據(jù)的高位比b數(shù)據(jù)大,那剩下的低位就不用比較了。除此之外,每一位的數(shù)據(jù)范圍不能太大,要可以用線性排序算法來(lái)排序,否則,基數(shù)排序的時(shí)間復(fù)雜度就無(wú)法做到O(n)了。
代碼如下:
#pragma mark -
#pragma mark 基數(shù)排序
- (void)radixSort:(NSMutableArray *)datasource
{
NSMutableArray *bucket = [self createBucket];
NSInteger maxNumber = [self listMaxItem:datasource];
NSInteger maxLength = [self numberLength:maxNumber];
for (NSInteger digit = 1; digit <= maxLength; digit++)
{
//入桶
for (NSNumber *number in datasource)
{
NSInteger baseNumber = [self fetchBaseNumber:number.integerValue digit:digit];
NSMutableArray *subArray = bucket[baseNumber];
[subArray addObject:number];
}
//出桶
NSInteger index = 0;
for (NSInteger i = 0; i < bucket.count; i++)
{
NSMutableArray *subArray = bucket[i];
while (subArray.count > 0)
{
NSNumber *item = subArray[0];
[datasource replaceObjectAtIndex:index withObject:item];
[subArray removeObjectAtIndex:0];
index++;
}
}
}
}
//創(chuàng)建10個(gè)空桶
- (NSMutableArray *)createBucket
{
NSMutableArray *bucketArray = [NSMutableArray array];
for (NSInteger i = 0; i < 10; i++)
{
[bucketArray addObject:[NSMutableArray array]];
}
return bucketArray;
}
//計(jì)算無(wú)序序列中最大的數(shù)值
- (NSInteger)listMaxItem:(NSArray *)array
{
NSInteger maxNumber = [array[0] integerValue];
for (NSNumber *number in array)
{
if (maxNumber < number.integerValue)
{
maxNumber = number.integerValue;
}
}
return maxNumber;
}
//獲取數(shù)字的長(zhǎng)度
- (NSInteger)numberLength:(NSInteger)number
{
NSString *numberStr = [NSString stringWithFormat:@"%ld",(long)number];
return numberStr.length;
}
//獲取數(shù)值中特定位數(shù)的值
- (NSInteger)fetchBaseNumber:(NSInteger)number digit:(NSInteger)digit
{
if (digit > 0 && digit <= [self numberLength:number])
{
NSMutableArray *numbersArray = [NSMutableArray array];
NSString *numberStr = [NSString stringWithFormat:@"%ld",(long)number];
for (NSInteger i = 0; i < numberStr.length; i++)
{
NSString *subStr = [numberStr substringWithRange:NSMakeRange(i, 1)];
[numbersArray addObject:[NSNumber numberWithInteger:subStr.integerValue]];
}
return [numbersArray[numbersArray.count - digit] integerValue];
}
return 0;
}
內(nèi)容小結(jié)
今天,我們學(xué)習(xí)了3種線性時(shí)間復(fù)雜度的排序算法,有桶排序、計(jì)數(shù)排序、基數(shù)排序。它們對(duì)要排序的數(shù)據(jù)都有比較苛刻的要求,應(yīng)用不是非常廣泛。但是如果數(shù)據(jù)特征比較符合這些排序算法的要求,應(yīng)用這些算法,會(huì)非常高效,線性時(shí)間復(fù)雜度可以達(dá)到0(n)。
桶排序和計(jì)數(shù)排序的排序思想是非常相似的,都是針對(duì)范圍不大的數(shù)據(jù),將數(shù)據(jù)劃分成不同的桶來(lái)實(shí)現(xiàn)排序。基數(shù)排序要求數(shù)據(jù)可以劃分成高低位,位之間有遞進(jìn)關(guān)系。比較兩個(gè)數(shù),我們只需要比較高位,高位相同的再比較低位。而且每一-位的數(shù)據(jù)范圍不能太大,因?yàn)榛鶖?shù)排序算法需要借助桶排序或者計(jì)數(shù)排序來(lái)完成每- -個(gè)位的排序工作。
最后:
自己寫(xiě)了一個(gè)NSMutableArray+GLYSort算法分類,只需1行代碼,即可完成復(fù)雜排序操作。