中文全文檢索中很重要的一個環節就是分詞,而一般分詞都是基於字典的,特別是對於特定的業務,需要從特定的語料庫中抽出高頻有意義的詞來生成字典。這系列文章,就一步一步來實現一個從大規模語料庫正抽取出高頻詞的程序。
抽詞的過程如下圖:
本文先講解“子串字典序排序”部分,也就是字典序排序部分。本文使用兩種算法:快排 和 基數排序,兩種算法各有應用場景,快排在分析長度20萬字符串時所用的時間明顯低於基數排序,但是,超過時,基數排序明顯有優勢;本文僅僅對於實現的算法做簡單分析和實現,真正生成環境中,將引入多線程,分布式處理等優化手段,這里不提及。
這里,我要先用通俗一些的話語來解釋一些概念,有不正確的地方,歡迎指出;
字典序:字典序是按照字符的順序來排序。例如,‘a’跟‘b’,則字典序為: ‘a’,‘b’;“adc”跟“acd”的字典序為:“acd”,“adc”。
子串:假設現在有字符串S:“S1S2S3…Sn-1Sn”,如果存在i≥1且j≤n(i≤j), 則“SiSi+1…Sj-1Sj”為S的子串。
后綴數組:后綴樹的代替方案;后綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字符串r的從 第 i 個字符開始的后綴表示為 Suffix(i) ,也 就是Suffix(i)=r[i..len(r)]。而后綴數組則是記錄開始位置i的一個數組。
PAT數組:主要用於對文本進行全文索引;PAT數組是一個字符串的后綴樹的字典序的排列。
LCP數組:對應一個PAT數組,表示PAT數組間相鄰的兩個后綴子串的最長公共前綴。
(注,PAT和LCP兩個概念相當重要)
【例】有字符串“abcba”,則其后綴樹為{“abcba”,“bcba”,“cba”,“ba”,“a”},
后綴數組為:{0,1,2,3,4},字典序排序后為:{“a”,“abcda”,“ba”,“bcba”,“cba”},
PAT為:{4,0,3,1,2}, LCP:{1,0,1,0,0}
從例子可以知道,子串字典序排序的問題可以分解為,求后綴數組,后綴數組字典序排序兩個子問題。
獲取后綴數組是一種特殊的子串集合,相當容易獲取,這里就不累贅。
1. 使用快排來實現后綴數組排序(不重點講解,重點講解基數排序):
1 public static void DictSort(int[] a, string s, int left, int right) 2 { 3 while (left < right) 4 { 5 int i = Partition(a, s, left, right); 6 DictSort(a, s, left, i - 1); 7 left = i + 1; 8 } 9 } 10 11 public static int Partition(int[] a, string s, int left, int right) 12 { 13 var _tmp = a[left]; 14 string tmp = s.Substring(a[left]); 15 while (left < right) 16 { 17 while (left < right && s.Substring(a[right]).CompareTo(tmp) >= 0) 18 right--; 19 if (left < right) 20 a[left] = a[right]; 21 while (left < right && s.Substring(a[left]).CompareTo(tmp) <= 0) 22 left++; 23 if (left < right) 24 { 25 a[right] = a[left]; 26 right--; 27 } 28 } 29 a[left] = _tmp; 30 return left; 31 }
其中,a為后綴數組,s為源字符串。程序執行完后,a為PAT數組。
2. 使用基數排序來排序后綴數組:
使用該算法的先決條件:輸入的可枚舉,即有限的集合中。而因為漢字跟英文字母可以枚舉
基數排序:也叫“木桶排序”,是一種分配式排序。時間復雜度為O(nlog(r)m),其中r為所采取的基數,而m為堆數。
【例子】假設現在有“1”,“15”,“26”,“55”,“10”這5個數,進行基數排序過程如下:
Step 1: 將個位數放進相應的位置(如下圖)
Step 2, 從0到9依次讀出該數組:10,1,15,55,26
Step 3: 根據Step 2 的結果,再進行一次對十位數的排序(如下圖),【注:“1”=“01”】:
Step 4: 再執行一次Step 2,得到“1”,“10”,“15”,“26”,“55”,結束。
要將該算法運用到字符串中,我們首先得對字符進行編碼:
1. 字符串編碼映射
用整型來表示編碼,對於GB2312編碼,有7445個字符,其中6763個漢字和682個其他字符。漢字的內碼范圍高字節B0-F7,低字節A1-FE。則映射公式可表示為:
Code = (High-176)*94+(Low-161)+Δ;
然而,對於很多應用來說,GB2312編碼不夠用,因為存在一些繁體字,生僻字等未收錄到GB2312編碼中,所以我們采用GBK編碼,GBK共可以表示23940個字符,其中21003個漢字,映射公式可以表示為:
Code =(Heigh-129)*94+(Low-64)+ Δ;
代碼如下:
1 public static int[] GetGBCode(string s) 2 { 3 int len = s.Count(); 4 int[] gbCode = new int[len]; 5 Encoding chs = Encoding.GetEncoding("GBK"); 6 for (var i = 0; i < len; i++) 7 { 8 byte[] bytes = chs.GetBytes(s[i].ToString()); 9 if (bytes.Length == 1) 10 gbCode[i] = bytes[0]; 11 else 12 gbCode[i] = (bytes[0] - 129) * 94 + (bytes[1] - 64); 13 } 14 return gbCode; 15 }
2. 用基數排序來排序編碼后的字符串:
【例】輸入為“張則智”,“是個天才”,“絕對天才”:
Step 1:映射編碼(如下圖)
Step 2: 我們從最后一個字符開始進行基數排序【注:空字符為0】
Sort 1: 張則智,是個天才,絕對天才
Sort 2:是個天才,絕對天才,張則智
Sort 3:絕對天才,是個天才,張則智
Sort 4:絕對天才,是個天才,張則智
排序結束;也就是說,我們需要進行輸入串S.Lengh次線性排序。
代碼片段1:
1 public static void RadixSort(int[, ] A, int[] B, int t, int n, int M) 2 { 3 int[] C; 4 for (int k = n - 1; k >= 0; k--) 5 { 6 C = new int[M]; 7 for (int i = 0; i < n; i++) 8 { 9 C[A[i, k]]++; 10 } 11 for (int m = 1; m < M; m++) 12 { 13 C[m] += C[m - 1]; 14 } 15 for (int j = 0; j < n; j++) 16 { 17 B[--C[A[j, k]]] = j; 18 } 19 } 20 }
上述代碼段,簡單易理解,但是有個問題,當輸入過大時,A這個二維數據太大,會導致內存泄露,所以,進行了改進:
代碼片段2:
1 public static void RadixSort(int[] A, int[] B, int t, int n, int M) 2 { 3 int[] C; 4 int[] tmp = new int[t]; 5 for (var x = 0; x < t; x++) 6 { 7 tmp[x] = x; 8 } 9 for (int k = n - 1; k >= 0; k--) 10 { 11 C = new int[M]; 12 for (int i = 0; i < n; i++) 13 { 14 int z = k + tmp[i]; 15 if (z <= t - 1) 16 { 17 C[A[z]]++; 18 } 19 else 20 { 21 C[0] += n - i; 22 break; 23 } 24 } 25 for (int m = 1; m < M; m++) 26 { 27 C[m] += C[m - 1]; 28 } 29 for (int j = 0; j < n; j++) 30 { 31 int u = k + tmp[j]; 32 if (u <= t - 1) 33 B[--C[A[u]]] = j; 34 else 35 { 36 for (int y = j; j < n; j++) 37 { 38 B[--C[0]] = j; 39 } 40 break; 41 } 42 } 43 } 44 }
程序2做了優化,其中A為對應的編碼,B為后綴數組,t為后綴數組的長度,n為字符串的長度,M為GBK中漢字的個數(本應用中)。
當語料庫增長到百萬級別后,平均花費時間明顯低於快排。
兩個算法可以使用多線程來進行分塊排序在合並,此處未做這方面的優化。
好了,第一部分就先講到這里,如果覺得文章對您有用或者對其他人有幫助,請幫忙點文章下面的“推薦”;如果文章有任何紕漏,歡迎指正,謝謝!