我用 Java 编写了一个实现(因为我不知道如何打字或 javascript)。不过,这种方法是可翻译的,所以我希望这可能会有所帮助。
这是我的思考过程:
D 是常数,所以我们想找到一种方法来查找所有具有共同前缀的单词。所以,为此我实现:
- 一种树状结构,它根据字符串的字符索引字符串。这意味着字符串
artur 将存储在a -> r -> t -> u 等中
- 这会将索引 D 置于 O(n) 的时间复杂度中,其中 n 是字符串的长度。
- 这使得搜索共享公共前缀的单词的时间为 O(n),其中 n 是我们正在寻找的前缀的长度
该方法有一些限制,因此我可以更快地对其进行测试:
* 只允许小写字母
* 在两者之间存储字符串以避免在查找前缀时遍历树。
所以,对于我的代码,我有这些测试,还添加了一些时间来看看会发生什么:
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
这样做的主要问题是创建树(这可能只是我对树的 hacky 实现或我浪费内存的方式)。所以还有改进的余地。树的创建确实需要相当多的时间。
实验表明,使用树来查找公共前缀在时间消耗方面是稳定的。
需要考虑的事情可能是:
- 数据结构的稀疏数组。
- 不存储实际字符串,而是遍历树以查找所有公共前缀
希望对您有所帮助 - 有趣的小练习。让我知道我是否完全把它塞满了:)
对已排序的输入进行二分搜索
我还注意到你要求一个不复杂的数据结构,所以我尝试了以下方法:
- 对输入的字符串列表进行排序
- 二分查找与我们要查找的前缀匹配的第一个索引
- 收集左右前缀
这会导致这段代码(再次,抱歉,它是 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 脚本的角度来看,如果你有一个内置的二分搜索,最后一种方法可能是最简单和最直接的选择,因为构建和维护一棵树要多一点涉及+(对我来说)花了很多时间来索引字符串。