首先拋出一個問題: 給定300w字符串A, 以後給定80w字符串B, 須要求出 B中的每個字符串, 是不是A中某一個字符串的子串. 也就是拿到80w個bool值
.java
固然, 直觀的看上去, 有一個暴力的解法, 那就是 雙重循環, 再調用字符串德contains
方法, 想法很美好, 現實很殘酷. 若是你真的這麼實現了(是的, 我作了.), 就會發現,效率低到沒法接受.具體的效率測評在後文給出.算法
此時咱們能夠用一個叫Suffix Array
的數據結構來輔助咱們完成這個任務.後端
在計算機科學裏, 後綴數組(英語:suffix array)是一個經過對字符串的全部後綴通過排序後獲得的數組。此數據結構被運用於全文索引、數據壓縮算法、以及生物信息學。後綴數組被烏迪·曼伯爾(英語:Udi Manber)與尤金·邁爾斯(英語:Eugene Myers)於1990年提出,做爲對後綴樹的一種替代,更簡單以及節省空間。它們也被Gaston Gonnet 於1987年獨立發現,並命名爲「PAT數組」。數組
在2016年,李志澤,李建和霍紅衛提出了第一個時間複雜度(線性時間)和空間複雜度(常數空間)都是最優的後綴數組構造算法,解決了該領域長達10年的open problem。微信
讓咱們來認識幾個概念:數據結構
字符串S的子串r[i..j],i<=j,表示S串中從i到j-1這一段,就是順次排列r[i],r[i+1],...,r[j-1]造成的子串。 好比 abcdefg的0-3子串就是 abc.app
後綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字符串r的從第i個字符開始的後綴表示爲Suffix(i),也就是Suffix(i)=S[i...len(S)-1]。好比 abcdefg 的 Suffix(5)
爲 fg.性能
後綴數組 SA 是一個一維數組,它保存1..n 的某個排列SA[1] ,SA[2] ,...,SA[n] ,而且保證Suffix(SA[i])<Suffix(SA[i+1]), 1<=i<n 。也就是將S的n個後綴從小到大進行排序以後把排好序的後綴的開頭位置順次放入SA 中。學習
名次數組 Rank[i] 保存的是 Suffix(i) 在全部後綴中從小到大排列的「名次」優化
看完上面幾個概念是否是有點慌? 不用怕, 我也不會. 咱們要牢記本身是工程師, 不去打比賽, 所以不用實現完美的後綴數組. 跟着個人思路, 用簡易版後綴數組來解決前言中的問題.
首先, 大概的想明白一個道理. A是B的子串, 那麼A就是B的一個後綴的前綴. 好比pl
是apple
的子串. 那麼它是apple
的後綴ple
的前綴pl
.
好的, 正式開始舉栗子.
題目中的A, 有300w字符串.咱們用4個代替一下.
apple orange pear banana
題目中的B, 有80w字符串. 咱們用一個代替一下.
ear
.
咱們的目的是, 找ear
是不是A中四個字符串中的某一個的子串. 求出一個TRUE/FALSE
.
那麼咱們首先求出A中全部的字符串德全部子串.放到一個數組裏.
好比 apple的全部子串爲:
apple pple ple le e
將A中全部字符串的全部子串放到 同一個 數組中, 以後把這個數組按照字符串序列進行排序.
注: 爲了優化排序的效率, 正統的後綴數組進行了大量的工做, 用比較複雜的算法來進行了優化, 可是我這個項目是一個離線項目, 幾百萬排序也就一分鐘不到, 所以我是直接調用的Arrays.sort
.若是有須要, 能夠參考網上的其餘排序方法進行優化排序.
好比只考慮apple的話, 排完序是這樣子的.
apple e le ple pple
爲何要進行排序呢? 爲了應用二分查找, 二分查找的效率是O(logN)
,極其優秀.
接下來是使用待查找字符串進行二分查找的過程, 這裏就不贅述了. 能夠直接去代碼裏面一探究竟.
package com.huyan.sa; import java.util.*; /** * Created by pfliu on 2019/12/28. */ public class SuffixArray { private List<String> array; /** * 用set構建一個後綴數組. */ public static SuffixArray build(Set<String> stringSet) { SuffixArray sa = new SuffixArray(); sa.array = new ArrayList<>(stringSet.size()); // 求出每個string的後綴 for (String s : stringSet) { sa.array.addAll(suffixArray(s)); } sa.array.sort(String::compareTo); return sa; } /** * 求單個字符串的全部後綴數組 */ private static List<String> suffixArray(String s) { List<String> sa = new ArrayList<>(s.length()); for (int i = 0; i < s.length(); i++) { sa.add(s.substring(i)); } return sa; } /** * 判斷當前的後綴數組,是否有以s爲前綴的. * 本質上: 判斷s是不是構建時某一個字符串德子串. */ public boolean saContains(String s) { int left = 0; int right = array.size() - 1; while (left <= right) { int mid = left + ((right - left) >> 1); String suffix = array.get(mid); int compareRes = compare1(suffix, s); if (compareRes == 0) { return true; } else if (compareRes < 0) { left = mid + 1; } else { right = mid - 1; } } return false; } /** * 比較兩個字符串, * 1. 若是s2是s1的前綴返回0. * 2. 其他狀況走string的compare邏輯. * 目的: 爲了在string中使用二分查找,以及知足咱們的,相等就結束的策略. */ private static int compare1(String s1, String s2) { if (s1.startsWith(s2)) return 0; return s1.compareTo(s2); } }
實現的比較簡單,由於是一個簡易的SA. 主要分爲兩個方法:
咱們對性能作一個簡易的評估.
評估使用代碼:
@Test public void perf() throws IOException { // use sa long i = System.currentTimeMillis(); List<String> A = Files.readAllLines(Paths.get("/Users/pfliu/data/old_data/A.txt")); SuffixArray sa = SuffixArray.build(new HashSet<>(A)); int right = 0; int wrong = 0; List<String> B = Files.readAllLines(Paths.get("/Users/pfliu/data/old_data/B.txt")); for (String s : B) { if (sa.saContains(s)) { right++; } else { wrong++; } } log.info("use sa. all={}, right={}, wrong={}. time={}", B.size(), right, wrong, System.currentTimeMillis() - i); // violence wrong = 0; right = 0; //count int count = 0; long time = System.currentTimeMillis(); for (String s : B) { boolean flag = false; for (String s1 : A) { if (s1.contains(s)) { flag = true; right++; break; } } if (!flag) wrong++; if (++count % 1000 == 0) { log.info("use biolence. deal {} word. now right ={}, wrong ={}, time={}", count, right, wrong, System.currentTimeMillis() - time); time = System.currentTimeMillis(); } } }
這裏是輸出部分日誌(我沒有等待暴力解法跑完):
16:29:35.440 [main] INFO com.huyan.sa.SuffixArrayTest - use sa. all=815971, right=433402, wrong=382569. time=35371 16:29:49.748 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 1000 word. now right =855, wrong =145, time=14301 16:30:11.807 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 2000 word. now right =1625, wrong =375, time=22059 16:30:38.272 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 3000 word. now right =2343, wrong =657, time=26465 16:31:07.080 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 4000 word. now right =3019, wrong =981, time=28808 16:31:36.550 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 5000 word. now right =3700, wrong =1300, time=29470 16:32:07.141 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 6000 word. now right =4365, wrong =1635, time=30590 16:32:39.338 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 7000 word. now right =5030, wrong =1970, time=32197 16:33:13.781 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 8000 word. now right =5641, wrong =2359, time=34443 16:33:47.392 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 9000 word. now right =6269, wrong =2731, time=33611 16:34:21.783 [main] INFO com.huyan.sa.SuffixArrayTest - use biolence. deal 10000 word. now right =6878, wrong =3122, time=34391
個人評估集: A=80w. B=310w.
能夠看到, 結果很粗暴.
使用 SA的計算完全部結果,耗時35s.
暴力解法,計算1000個就須要30s. 隨着程序運行, cpu時間更加緊張. 還可能會逐漸變慢.
能夠看出, 在這個題目中, SA的效率相比於暴力解法是碾壓性質的.
須要強調的是, 這個"題目"是我在工做中真實碰到的, 使用暴力解法嘗試以後, 因爲效率過低, 在大佬指點下使用了SA. 30s解決問題.
所以, 對於一些經常使用算法, 咱們不要抱着 "我是工程師,又不去算法比賽,沒用" 的心態, 是的, 咱們不像在算法比賽中那樣分秒必爭, 可是不少算法的思想, 卻能給咱們的工做帶來極大的提高.
https://blog.csdn.net/u013371...
https://zh.wikipedia.org/zh-h...
完。
最後,歡迎關注個人我的公衆號【 呼延十 】,會不按期更新不少後端工程師的學習筆記。
也歡迎直接公衆號私信或者郵箱聯繫我,必定知無不言,言無不盡。
<h4>ChangeLog</h4>
2019-12-28 完成
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客或關注微信公衆號 <呼延十 >------>呼延十