创建关键字过滤器的最快方法?

时间:2014-01-18 17:17:22

标签: java algorithm string-matching

我正在尝试根据关键字过滤器过滤推文。过滤器可以有10个字或更多。因此,如果推文包含关键字,则会传递。我唯一能想到的是将推文的文本分成标记。然后我会循环过滤单词并将每个标记与过滤器中的每个单词进行比较。然而这种方式似乎很慢。假设关键字过滤器有N个关键字且令牌数为M,那么它需要O(N * M)。

有更好的方法吗?

6 个答案:

答案 0 :(得分:5)

这个问题有许多有趣的方面以及解决问题的方法。他们每个人都有权衡。


当人们继续讨论HashMaps并且这样做是O(1)时,他们仍然缺少一些可以完成的编译时优化。在编译时知道单词集将允许您将其放入Enum,然后允许您使用鲜为人知的EnumMapdoc)和EnumSet({ {3}})。 Enum为您提供了一个序数类型,然后允许您调整后备阵列或位域的大小,从不担心扩展它。同样,枚举的散列是它的序数值,因此您没有复杂的散列查找(尤其是非互联字符串)。 EnumSet是一种类型安全的位域。

import java.util.EnumSet;

public class Main {
    public static void main(String[] args) {
        EnumSet<Words> s = EnumSet.noneOf(Words.class);

        for(String a : args) {
            s.clear();
            for(String w : a.split("\\s+")) {
                try {
                    s.add(Words.valueOf(w.toUpperCase()));
                } catch (IllegalArgumentException e) {
                    // nothing really
                }
            }
            System.out.print(a);
            if(s.size() == 4) { System.out.println(": All!"); }
            else { System.out.println(": Only " + s.size()); }
        }
    }

    enum Words {
        STACK,
        SOUP,
        EXCHANGE,
        OVERFLOW
    }
}

在命令行上运行一些示例字符串时:

"stack exchange overflow soup foo"
"stack overflow"
"stack exchange blah"

获得结果:

stack exchange overflow soup foo: All!
stack overflow: Only 2
stack exchange blah: Only 2

你已将匹配的核心语言移动到核心语言,希望它得到很好的优化。结果看起来它最终只是一个Map<String,T>(和doc它的HashMap深藏在Class类中。)。


你有一个字符串。将它拆分成某种标记是不可避免的。需要检查每个令牌以查看它是否匹配。但是将它们与所有标记进行比较,就像你注意到的那样昂贵。

然而,“恰好匹配这些字符串”的语言是常规语言。这意味着我们可以使用正则表达式来过滤掉不匹配的单词。正则表达式在O(n)时间内运行(参见digging even further)。

这并没有摆脱O(wordsInString * keyWords),因为这仍然是最糟糕的情况(这是O()所代表的),但它确实意味着对于无法匹配的单词,你只花了O(charsInWord)消除它。

package com.michaelt.so.keywords;

import java.util.EnumSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    final static Pattern pat = Pattern.compile("S(?:TACK|OUP)|EXCHANGE|OVERFLOW", Pattern.CASE_INSENSITIVE);
    public static void main(String[] args) {
        EnumSet<Words> s = EnumSet.noneOf(Words.class);
        Matcher m = pat.matcher("");
        for(String a : args) {
            s.clear();
            for(String w : a.split("\\s+")) {
                m.reset(w);
                if(m.matches()) {
                    try {
                        s.add(Words.valueOf(w.toUpperCase()));
                    } catch (IllegalArgumentException e) {
                        // nothing really
                    }
                } else {
                    System.out.println("No need to look at " + w);
                }
            }
            System.out.print(a);
            if(s.size() == 4) { System.out.println(": All!"); }
            else { System.out.println(": Only " + s.size()); }
            System.out.println();
        }
    }

    enum Words {
        STACK,
        SOUP,
        EXCHANGE,
        OVERFLOW
    }
}

这给出了输出:

No need to look at foo
stack exchange overflow soup foo: All!

stack overflow: Only 2

No need to look at blah
stack exchange blah: Only 2

现在,大失望了。尽管如此,Java可能仍然更快地计算字符串的哈希值并在哈希中查找它以查看它是否存在。

这里唯一更好的是制作一个匹配所有字符串的正则表达式。如上所述, 是一种常规语言。

(?:stack\b.+?\bexchange\b.+?\bsoup\b.+?\boverflow)|(?:soup\b.+?\bexchange\b.+?\bstack\b.+?\boverflow) ...

上述正则表达式将匹配字符串stack exchange pea soup overflow

这里有四个字,意思是4个! (s1)|(s2)|(s3)|...(s24)的部分以这种方式接近10个关键字的正则表达式将是(s1)|...|(s3628800),这可能被认为是非常不切实际的。可能虽然有些引擎可能会扼杀那么大的正则表达式。不过,它会将其减少到O(n),其中n是你得到的字符串的长度。

进一步请注意,这是所有过滤器,而不是任何过滤器或某些过滤器。

如果您想匹配十个中的一个关键字,那么正则表达式只有十个组长。如果你想匹配十个中的两个关键字,那么它只有90个组长(位长,但引擎可能不会阻塞它)。这个正则表达式可以以编程方式生成。

这将让你回到O(N)时间,其中N是推文的长度。不需要拆分。

答案 1 :(得分:0)

我想要解决此问题的一种方法是创建一个HashSet并将所有推文的文本标记放在其中。然后我会遍历单词过滤器中的单词并检查它们是否全部在HashSet

答案 2 :(得分:0)

如果你有足够的时间进行预处理,你可以构建一个索引:所有推文中包含的所有单词的列表(在一些易于搜索的数据结构中,如树或哈希表)。每个单词都附有包含此单词的推文ID。

然后,您可以在索引中查找关键字并计算ID的交集。

此技术称为inverted index

答案 3 :(得分:0)

在HashMap中搜索或多或少是O(1)所以如果你将密钥存储在HashMap中(例如),你只需要检查M次,所以它将是O(M)。

答案 4 :(得分:0)

我认为你可以使用带有O(M + N)的HashSet来做到这一点,但是如果需要节省一些空间,你也可以尝试使用布隆过滤器,它会以低概率给出误报。

答案 5 :(得分:0)

取决于:

  • 是实时过滤吗?
  • 您打算使用不同的单词重新运行过滤吗?

如果它是实时的 - 它还取决于作品的数量。你可以使用contains方法或构建正则表达式,并希望它会很快。

如果它是我们想要做的离线工作,如果你不打算改变你可以使用实时方法的作品集,如果你认为你要改变过滤器,你会想要的建立下一个索引。

对于每个工作,保存哈希,其中键是推文id(值有点) 使用过滤词找到所有推文,翻阅单词并与每个单词的推文ID相交