本文將介紹3區基數快速排序、后綴排序法。
1. 前文回顧
在字符串算法—字符串排序(上篇)中,我們介紹了鍵索引計數法、LSD基數排序、MSD基數排序。
但LSD基數排序要求需排序字符串的長度一致;MSD基數排序雖然對字符串的長度沒要求,但其遞歸循環里的每次循環都需要進行很多操作,且需要額外的空間。
本文將介紹一種更高效的字符串排序方法:結合MSD基數排序和3區快速排序。如果對這兩種算法不熟悉的,建議先去了解一下。
2. 3區基數快速排序(3-way radix quicksort)
從例子入手:與MSD基數排序一樣,我們設定每個字符串的最后一位字符的下一個位置的空字符的鍵索為-1。a[]是擁有一堆字符串的字符串數組。
與MSD基數排序一樣,每個字符配一個正數鍵索,用於比較字符間的大小。
首先,對a[0](she)的第一個字符(s)進行3區快速排序:
3區快速排序會把整個數組分為3個區:
第一個區里的所有字符串的第一個字符都比(she)的第一個字符(s)小,它含有a[0]、a[1];
第二個區里的所有字符串的第一個字符都與(she)的第一個字符(s)相同,它含有a[2]~a[11];
第三個區里的所有字符串的第一個字符都比(she)的第一個字符(s)大,它含有a[12]、a[13]。
然后從上往下的,先看第一個區,此區含有多個字符串,對此區的第一個字符串(by)的第一個字符(b)進行3區快速排序:(為了方便觀察,待排序的字符串用黑色表示)
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第一個字符都比(by)的第一個字符(b)小,它含有a[0];
第二個區里的所有字符串的第一個字符都與(by)的第一個字符(b)相同,它含有a[1];
第三個區里的所有字符串的第一個字符都比(by)的第一個字符(b)大,它沒有元素;
然后從上往下的,先看第一個區,它只有一個字符串,此區排序完畢;
看第二個區,它只有一個字符串,此區排序完畢;
看第三個區,它沒有字符串,此區排序完畢;
(為了方便觀察,已排序完畢的字符串用綠色表示)
然后看下一個區,此區含有多個字符串,對此區的第一個字符串(seashells)的第二個字符(e)進行3區快速排序:
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第二個字符都比(seashells)的第二個字符(e)小,它沒有元素;
第二個區里的所有字符串的第二個字符都與(seashells)的第二個字符(e)相同,它含有a[2]~a[6];
第三個區里的所有字符串的第二個字符都比(seashells)的第二個字符(e)大,它含有a[7]~a[11];
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,對此區的第一個字符串(seashells)的第三個字符(a)進行3區快速排序:
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第三個字符都比(seashells)的第三個字符(a)小,它沒有元素;
第二個區里的所有字符串的第三個字符都與(seashells)的第三個字符(a)相同,它含有a[2]~a[4];
第三個區里的所有字符串的第三個字符都比(seashells)的第三個字符(a)大,它含有a[5]~a[6];
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,對此區的第一個字符串(seashells)的第四個字符(s)進行3區快速排序:
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第四個字符都比(seashells)的第四個字符(s)小,它含有a[2];
第二個區里的所有字符串的第四個字符都與(seashells)的第四個字符(s)相同,它含有a[3]~a[4];
第三個區里的所有字符串的第四個字符都比(seashells)的第四個字符(s)大,它沒有元素;
然后從上往下的,先看第一個區,它只有一個字符串,此區排序完畢;
看第二個區,它有多個字符串,對此區的第一個字符串(seashells)的第五個字符(h)進行3區快速排序:
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第五個字符都比(seashells)的第五個字符(h)小,它沒有元素;
第二個區里的所有字符串的第五個字符都與(seashells)的第五個字符(h)相同,它含有a[3]~a[4];
第三個區里的所有字符串的第五個字符都比(seashells)的第五個字符(h)大,它沒有元素;
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,對此區的第一個字符串(seashells)的第六、七、八、九、十個字符(h)進行3區快速排序:(由於這兩個字符串是一樣的,排序結果不變,這里省略中間過程,直接到第十個字符的排序)
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第五個字符都比(seashells)的第五個字符(h)小,它沒有元素;
第二個區里的所有字符串的第五個字符都與(seashells)的第五個字符(h)相同,它含有a[3]~a[4];
第三個區里的所有字符串的第五個字符都比(seashells)的第五個字符(h)大,它沒有元素;
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,但此區的第一個字符串(seashells)沒有第十一個字符,故此區排序結束;
看第三個區,它沒有字符串,此區排序完畢;
看下一個區,它有多個字符串,對此區的第一個字符串(sells)的第四個字符(l)進行3區快速排序:
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第四個字符都比(sells)的第四個字符(l)小,它沒有元素;
第二個區里的所有字符串的第四個字符都與(sells)的第四個字符(hl)相同,它含有a[5]~a[6];
第三個區里的所有字符串的第四個字符都比(sells)的第四個字符(l)大,它沒有元素;
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,對此區的第一個字符串(sells)的第五、六個字符(h)進行3區快速排序:(由於這兩個字符串是一樣的,排序結果不變,這里省略中間過程,直接到第六個字符的排序)
3區快速排序會把整個區分為3個區:
第一個區里的所有字符串的第六個字符都比(sells)的第六個字符小,它沒有元素;
第二個區里的所有字符串的第六個字符都與(sells)的第六個字符相同,它含有a[5]~a[6];
第三個區里的所有字符串的第六個字符都比(sells)的第六個字符大,它沒有元素;
然后從上往下的,先看第一個區,它沒有字符串,此區排序完畢;
看第二個區,它有多個字符串,但此區的第一個字符串(sells)沒有第七個字符,故此區排序結束;
看第三個區,它沒有字符串,此區排序完畢;
看下一個區,它有多個字符串,對此區的第一個字符串(shells)的第三個字符(e)進行3區快速排序:
如此類推,直到所有區排序完畢。
總結一下:
1. 對第一個字符串的第一個字符通過3區快速排序把這堆字符串排一次序,根據排序結果進行分區;
2. 從上往下處理各個分區:如果分區只有一個字符串,則此區處理完畢;如果有多個字符串,則對此區的第一個字符串的第d個字符進行排序、分區、處理分區。(d值隨着排序進行而改變)
3. 所有分區處理完后,排序完畢,算法結束。
代碼實現:
3. 算法效率
注釋:
N為要排序的元素個數。
LSD的W: 由於LSD只針對於要排序的那堆字符串長度相同,故W為字符串的長度。
MSD的W: 要排序的那堆字符串的長度平均值。
guarantee: 算法保證能在多少次操作后完成。
random: 如果要排序的數組里的元素順序是隨機的,則算法可以在多少次操作后完成。
extra space: 算法需要的額外空間。
3區基數快速排序不需要構建額外數組,相比於MSD,它對內存是比較友好的,但它是不穩定(unstable)的。
4. 后綴排序法(suffix arrays)
當我們面對着搜索關鍵詞、尋找文章中最長的重復詞等需求時,可以用后綴排序法來高效解決。
從例子入手:假設我們有一段語句如下
我們把這段語句看成是一個字符數組S[],則S[0]=i; S[1]=t;S[2]=w;......
然后把這段語句變成一個字符串數組A[]:
A[0]由S[0]~S[14]組成;A[1]由S[1]~S[14]組成;A[2]由S[2]~S[14]組成;......;A[14]由S[14]組成;
然后用MSD基數排序或者3區基數快速排序對這個字符串數組排序:
排完序后,后綴排序法就結束了。
現在我們根據不同的需求,進行不同的操作:
一. 需求:尋找關鍵詞
假設我們現在要從上面那段語句中尋找“itwas”這個詞,我們將使用二分法檢索(binary search)。
首先簡單回顧一下二分法檢索:
將給定值key與數組中間位置上元素的關鍵碼(key)比較,如果相等,則檢索成功;
否則,若key小,則在數組前半部分中繼續進行二分法檢索;若key大,則在數組后半部分中繼續進行二分法檢索。
這樣,經過一次比較就縮小一半的檢索區間,如此進行下去,直到檢索成功或檢索失敗。
然后回到我們現在的需求。
itwas這個詞的第一個字符為i,然后在字符串數組A[]中所有字符串的第一個字符里進行二分法檢索。
檢索結果:A[0],A[9]符合。
然后itwas這個詞的第二個字符為t,在A[0],A[9]的第二個字符中進行二分法檢索,仍然符合。
然后檢索itwas的第三、四、五個字符,如果全部符合,則A[0],A[9]就是我們要找的關鍵詞。A[0],A[9]就是原語句的第0、9個字符。
需求完成。
二. 需求:尋找文章中最長的重復詞
其實當我們后綴排序完后,重復詞已經緊挨在一起了,找出來也很簡單:
A[0]與A[1]對比,找到重復詞為"as",長度為2,as暫時為最長的重復詞;
A[1]與A[2]對比,沒有重復詞;
A[2]與A[3]對比,沒有重復詞;
A[3]與A[4]對比,沒有重復詞;
A[4]與A[5]對比,找到重復詞為"itwas",長度為5,5>2,更新itwas為最長的重復詞;
A[5]與A[6]對比,沒有重復詞;
如此類推。。。
A[13]與A[14]對比,找到重復詞為"wa",長度為2,2<5, 不更新最長的重復詞;
A[14]為最后一個元素,對比完畢,此語句最長重復詞為"itwas"。
對比方法也不難實現:把兩個詞相同位置的字符進行逐一對比,一旦出現不相同,則結束對比,返回相同的字符,形成重復詞。
實現代碼: