首先抛出一个问题: 给定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
更多学习笔记见我的博客或关注微信公众号 <呼延十 >------>呼延十