搜索最长的前缀

时间:2017-03-15 11:19:22

标签: javascript algorithm typescript

我们假设,它有一个字符串数组D。给定字符串Q,我想在D中找到具有最长公共前缀Q的字符串。

我不想要复杂的数据结构,但它仍然应该比线性扫描更快。

是否有一种解决方案可以巧妙地对D进行排序,只需进行一次二分查找?

谢谢!

修改

澄清:当然,如果只进行一次,则单次扫描比排序更快。但是,我需要在固定的D上进行许多此类查找,因此这就是我正在寻找预先计算的数据结构的原因。

2 个答案:

答案 0 :(得分:2)

根据D中的字符

创建

每个node都包含character和子node的列表。

,如果D

 a
 ab
 ac
 ace
 d

然后

  • 有2个顶级节点ad
  • d没有孩子
  • a有2个孩子 - bc
  • b没有孩子
  • c有一个孩子 - e
  • e没有孩子

查找(并添加到树!)基本上是走节点,直到没有匹配的子节点。

例如,假设Q=af。有一个顶级节点包含Q[0]=a,但它没有Q[1]=f的子节点,因此最长的前缀是aa节点的所有子节点代表D中具有最长公共前缀Q的字符串,具体而言,aabacace

查找和添加操作在字符串长度上都是线性的,因此创建结构需要O(sum(len(x) for x in D))时间,查找为O(len(Q))

答案 1 :(得分:1)

我用Java编写了一个实现(因为我不知道如何使用打字稿或javascript)。虽然这种方法是可以翻译的,所以我希望这可能会有所帮助。

这是我的思考过程:

D是常数,所以我们想找到一种方法来查找具有公共前缀的所有单词。所以,为此我实现了:

  • 一种树状结构,它根据字符索引字符串。意味着字符串artur将存储在a - >中。 r - > t - > u
  • 这使索引D的时间复杂度为O(n),其中n是String的长度。
  • 这会将搜索共享前缀的单词放到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

这个问题的主要问题是创建树(这可能只是我对树的实施或者浪费内存的方式)。所以还有改进的余地。树的创建确实需要花费很多时间。

实验表明,对于使用树的时间消耗而言,查找公共前缀是稳定的。

要考虑的事情可能是:

  • 数据结构的稀疏数组。
  • 不存储实际的字符串,而是遍历树以查找所有公共前缀

希望有所帮助 - 有趣的小运动。如果我把它完全塞进来,请告诉我:)

对已排序的输入进行二进制搜索

我也注意到你要求一个不复杂的数据结构,所以我尝试了以下内容:

  • 对字符串的输入列表进行排序
  • 二进制搜索与我们查找的前缀相匹配的第一个索引
  • 左右收集前缀

这导致了这段代码(再次,抱歉,它是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脚本的角度来看,如果你有一个内置二进制搜索,最后一种方法可能是最容易和最直接的选择,因为构建和维护树更多一点涉及+(对我而言)花了很多时间来索引字符串。