【问题标题】:Search for longest prefix搜索最长前缀
【发布时间】:2017-08-06 02:29:55
【问题描述】:

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

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

有没有一种解决方案可以巧妙地对D 进行排序,然后只进行一次二进制搜索?

谢谢!

编辑

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

【问题讨论】:

  • 对我来说没有意义,平均而言,即使最快的排序 O (n log n) 也比线性搜索慢 O (n) .
  • 它必须做很多次,所以这就是为什么我要寻找一个预先计算的数据结构。
  • “有没有解决办法”你试过什么?完成别人的作业并不好玩......
  • 您不能将字符串数组存储在树结构中。那么,对于 Q,您只需逐个字符地遍历树并找到所有常见的前缀字符串吗?或者,对 D 进行排序,然后搜索排序列表而不是随机列表。
  • 请阅读How to Ask。关键词:“搜索和研究”和“解释......任何阻碍你自己解决的困难”。

标签: javascript algorithm typescript


【解决方案1】:

根据D中的字符创建

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

例如,如果D

 a
 ab
 ac
 ace
 d

然后

  • 有2个顶级节点ad
  • d 没有孩子
  • a 有 2 个孩子 - bc
  • b没有孩子
  • c 有 1 个孩子 - 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))

【讨论】:

  • 那么如何从树中获取原始字符串呢?
  • @Pavlo:什么“原始字符串”?
  • 如问题:'我想在 D 中找到与 Q. 有最长公共前缀的字符串'。所以我想找到前缀本身会很容易,但是整个字符串呢?
  • 找到给定前缀对应的结束节点后,该节点的所有个子节点用这个前缀表示D中的字符串。
【解决方案2】:

我用 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 脚本的角度来看,如果你有一个内置的二分搜索,最后一种方法可能是最简单和最直接的选择,因为构建和维护一棵树要多一点涉及+(对我来说)花了很多时间来索引字符串。

【讨论】:

  • 这看起来像 Java,但问题被标记为“javascript”和“typescript”。
  • 确实如此。不过,这个想法很容易翻译。
猜你喜欢
  • 1970-01-01
  • 2013-05-22
  • 1970-01-01
  • 2017-01-23
  • 1970-01-01
  • 2013-01-25
  • 1970-01-01
  • 2018-10-09
  • 1970-01-01
相关资源
最近更新 更多