我们假设,它有一个字符串数组D
。给定字符串Q
,我想在D
中找到具有最长公共前缀Q
的字符串。
我不想要复杂的数据结构,但它仍然应该比线性扫描更快。
是否有一种解决方案可以巧妙地对D
进行排序,只需进行一次二分查找?
谢谢!
修改
澄清:当然,如果只进行一次,则单次扫描比排序更快。但是,我需要在固定的D
上进行许多此类查找,因此这就是我正在寻找预先计算的数据结构的原因。
答案 0 :(得分:2)
根据D
中的字符
每个node
都包含character
和子node
的列表。
,如果D
是
a
ab
ac
ace
d
然后
a
和d
d
没有孩子a
有2个孩子 - b
和c
b
没有孩子c
有一个孩子 - e
e
没有孩子查找(并添加到树!)基本上是走节点,直到没有匹配的子节点。
例如,假设Q=af
。有一个顶级节点包含Q[0]=a
,但它没有Q[1]=f
的子节点,因此最长的前缀是a
。 a
节点的所有子节点代表D
中具有最长公共前缀Q
的字符串,具体而言,a
,ab
,ac
,ace
。
查找和添加操作在字符串长度上都是线性的,因此创建结构需要O(sum(len(x) for x in D))
时间,查找为O(len(Q))
。
答案 1 :(得分:1)
我用Java编写了一个实现(因为我不知道如何使用打字稿或javascript)。虽然这种方法是可以翻译的,所以我希望这可能会有所帮助。
这是我的思考过程:
D是常数,所以我们想找到一种方法来查找具有公共前缀的所有单词。所以,为此我实现了:
artur
将存储在a
- >中。 r
- > t
- > u
等该方法有一些限制,以便我可以更快地测试它: *仅允许使用小写字母 *在中间存储字符串以避免在查找前缀时遍历树。
因此,对于我的代码,我进行了这些测试,并添加了一些时间来查看会发生什么:
public class CommonPrefixTree {
public static void main(String[] args) {
Node treeRoot = new Node();
index("Artur", treeRoot);
index("ArturTestMe", treeRoot);
index("Blop", treeRoot);
index("Muha", treeRoot);
index("ArtIsCool", treeRoot);
List<String> strings = new ArrayList<>();
char[] chars = "abcdefghijklmnopqrstuvwxyz".toCharArray();
Random r = new Random();
for(int i = 0; i < 500000; i++) {
StringBuffer b = new StringBuffer();
for(int j = 0; j < 20 ; j++) {
b.append(chars[r.nextInt(chars.length)]);
}
strings.add(b.toString());
index(b.toString(), treeRoot);
}
strings.add("art");
strings.add("a");
strings.add("artu");
strings.add("arturt");
strings.add("b");
System.out.println(" ----- Tree search -----");
find("art", treeRoot);
find("a", treeRoot);
find("artu", treeRoot);
find("arturT", treeRoot);
find("b", treeRoot);
// The analog test for searching in a list
System.out.println(" ----- List search -----");
findInList("art", strings);
findInList("a", strings);
findInList("artu", strings);
findInList("arturt", strings);
findInList("b", strings);
}
static class Node {
Node[] choices = new Node[26];
Set<String> words = new HashSet();
void add(String word) {
words.add(word);
}
boolean contains(String word) {
return words.contains(word);
}
}
static List<String> findInList(String prefix, List<String> options) {
List<String> res = new ArrayList<>();
long start = System.currentTimeMillis();
for(String s : options) {
if(s.startsWith(prefix)) res.add(s);
}
System.out.println("Search took: " + (System.currentTimeMillis() - start));
return res;
}
static void index(final String toIndex, final Node root) {
Node tmp = root;
// indexing takes O(n)
for(char c : toIndex.toLowerCase().toCharArray()) {
int val = (int) (c - 'a');
tmp.add(toIndex);
if(tmp.choices[val] == null) {
tmp.choices[val] = new Node();
tmp = tmp.choices[val];
} else {
tmp = tmp.choices[val];
if(tmp.contains(toIndex)) return; // stop, we have seen the word before
}
}
}
static Set<String> find(String prefix, final Node root) {
long start = System.currentTimeMillis();
Node tmp = root;
// step down the tree to all common prefixes, O(n) where prefix defines n
for(char c : prefix.toLowerCase().toCharArray()) {
int val = (int) (c - 'a');
if(tmp.choices[val] == null) {
return Collections.emptySet();
}
else tmp = tmp.choices[val];
}
System.out.println("Search took: " + (System.currentTimeMillis() - start));
return tmp.words;
}
}
树和原始列表搜索的结果
这将导致5次搜索100,10000和500k字符串的这些时间:
100
----- Tree search -----
Search took: 0
Search took: 0
Search took: 0
Search took: 0
Search took: 0
----- List search -----
Search took: 0
Search took: 0
Search took: 0
Search took: 0
Search took: 0
10000
----- Tree search -----
Search took: 0
Search took: 0
Search took: 0
Search took: 0
Search took: 0
----- List search -----
Search took: 2
Search took: 2
Search took: 2
Search took: 2
Search took: 2
500000
----- Tree search -----
Search took: 0
Search took: 0
Search took: 0
Search took: 0
Search took: 0
----- List search -----
Search took: 43
Search took: 27
Search took: 66
Search took: 25
Search took: 24
这个问题的主要问题是创建树(这可能只是我对树的实施或者浪费内存的方式)。所以还有改进的余地。树的创建确实需要花费很多时间。
实验表明,对于使用树的时间消耗而言,查找公共前缀是稳定的。
要考虑的事情可能是:
希望有所帮助 - 有趣的小运动。如果我把它完全塞进来,请告诉我:)
对已排序的输入进行二进制搜索
我也注意到你要求一个不复杂的数据结构,所以我尝试了以下内容:
这导致了这段代码(再次,抱歉,它是Java但它应该很容易翻译:)
static Set<String> getCommonPrefix(final String prefix, final List<String> input) {
long start = System.currentTimeMillis();
int index = Collections.binarySearch(input, prefix, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
// o2 being the prefix
if(o1.startsWith(o2)) return 0;
return o1.compareTo(o2);
}
});
if(index < 0) {
return Collections.emptySet();
}
Set<String> res = new HashSet<>();
res.add(input.get(index));
boolean keepSearching = true;
int tmp = index - 1;
while(keepSearching && tmp > 0) {
if(input.get(tmp).startsWith(prefix)) {
res.add(input.get(tmp));
} else {
keepSearching = false;
}
tmp--;
}
keepSearching = true;
tmp = index + 1;
while(keepSearching && tmp < input.size()) {
if(input.get(tmp).startsWith(prefix)) {
res.add(input.get(tmp));
} else {
keepSearching = false;
}
tmp++;
}
System.out.println("Search took: " + (System.currentTimeMillis() - start));
return res;
}
这个有一个有趣的行为。搜索将采用O(log n)
,其中n
是数组的输入大小。然后,该集合是线性k
,其中k
是公共前缀的数量。
有趣的是,只要前缀相当大,这种方法非常快(与树实现相当),但是一旦你寻找非常少的前缀,这就像字符串的数量要慢一点检索是相当大的。详细的时间是(500万随机字符串):
Search for 'art' took: 1
Found strings: 309
Search for 'artur2' took: 0
Found strings: 1
Search for 'asd' took: 0
Found strings: 265
Search for 'nnb' took: 1
Found strings: 276
Search for 'asda' took: 0
Found strings: 10
Search for 'c' took: 63
Found strings: 192331
我想,从java脚本的角度来看,如果你有一个内置二进制搜索,最后一种方法可能是最容易和最直接的选择,因为构建和维护树更多一点涉及+(对我而言)花了很多时间来索引字符串。