Java indexOf(蛮力方法)对我或其他一些子字符串算法更实用吗?

Posted

技术标签:

【中文标题】Java indexOf(蛮力方法)对我或其他一些子字符串算法更实用吗?【英文标题】:Would Java indexOf (brute force method) be more practical for me or some other substring algorithm? 【发布时间】:2014-04-02 23:10:18 【问题描述】:

我正在寻找在许多短文本行(干草堆)中找到非常短的子字符串(模式、针)。但是,我不太确定在幼稚的蛮力方法之外使用哪种方法。

背景:我正在做一个有趣的附带项目,我收到多个用户的短信聊天日志(2000-15000 行文本和 2-50 个用户),我想找到所有各种模式匹配在基于我想出的预定词的聊天记录中。到目前为止,我正在寻找大约 1600 种模式,但我可能会寻找更多。

例如,我想找出在平均短信日志中使用的与食物相关的词的数量,例如“汉堡包”、“披萨”、“可乐”、“午餐”、“晚餐”、“餐厅” ”、“麦当劳”。虽然我给出了英语示例,但我实际上会在我的程序中使用韩语。这些指定的单词中的每一个都有自己的分数,我将其分别作为键和值放入哈希图中。然后,我会显示食物相关词的得分最高者以及这些用户最常使用的食物词。

我目前的方法是通过空格来消除每一行文本,并使用 haystack 包含模式的 contains 方法(使用 indexOf 方法和朴素的子字符串搜索算法)处理 haystack 中的每个单独的单词。

wordFromInput.contains(wordFromPattern);

举个例子,有 17 个用户在聊天,13000 行文本,1600 种模式,我发现使用这种方法整个程序需要 12-13 秒。而在我正在开发的 android 应用上,处理需要 2 分 30 秒,这太慢了。

最初,我尝试使用哈希映射并仅获取模式而不是在 ArrayList 中搜索它,但后来我意识到这是...

not possible with hash table

我想用子字符串做什么。

我通过 *** 环顾四周,发现了很多有用且相关的问题,例如这两个:

1 和 2。我比较熟悉各种字符串算法(Boyer Moore、KMP 等)

我最初认为天真的方法对于我的情况当然是最糟糕的算法类型,但是找到this question,我意识到我的情况(短模式,短文本)实际上可能更多用朴素的方法有效。但我想知道是否有什么我完全忽略了。

这里是snippet of my code,但如果有人想更具体地了解我的问题。

虽然我删除了大部分代码以简化它,但我用来实际匹配子字符串的主要方法是在 matchWords() 方法中。

我知道那是非常丑陋和糟糕的代码(5 个 for 循环...),所以如果对此有任何建议,我也很高兴听到。

所以要清理它:

聊天记录中的文本行 (2000-10,000+),干草堆 1600 多种图案、针头 主要使用韩语字符,但也包含一些英文 蛮力幼稚方法实在是太慢了,但正在讨论是否有其他替代方案,即使有,考虑到短模式和文本的性质,它们是否实用。

我只是想要一些关于我的思考过程的意见,可能还有一些一般性的建议。但另外,如果可能的话,我想对特定算法或方法提出一些具体建议。

【问题讨论】:

java.util.regex 如何适应? 对不起,因为我删除了很多matchWords()之外的部分,有很多东西可能看起来有点混乱。但我在开始时使用了正则表达式来消除句子中的空格,这样我就可以处理单个单词。编辑:还是你建议我使用 java.util.regex? 据我了解,这个问题可以说更适合程序员,因为它是一个白板讨论(而不是“为什么这段代码不起作用?”)。 我想知道正则表达式是否是一个可能的解决方案,但我还没有完全考虑你的问题。如果一开始就被排除在外,我不想考虑太多。 您是否考虑过使用像 en.wikipedia.org/wiki/… 这样的多模式匹配算法,或者为您的模式集构建一个通用的后缀树? 【参考方案1】:

你可以replace the hashtable with a Trie。

将文本行拆分为单词,使用空格分隔单词。然后检查单词是否在 Trie 中。如果它在 Trie 中,则更新与该词关联的计数器。理想情况下,计数器将集成到 Trie 中。

这个方法是 O(C),其中 C 是文本中的字符数。您不太可能避免至少检查每个字符一次。因此,至少在大 O 方面,这种方法应该尽可能好。

但是,听起来您可能不想列出您正在搜索的所有可能的单词。因此,您可能只想简单地使用您可以从所有单词中构建一个计数 Trie。如果没有别的可能会使您使用的任何模式匹配算法更容易。虽然,它可能需要对 Trie 进行一些修改。

【讨论】:

哈希表的复杂性与 trie 相同,至少概率很高(如果实施正确)。我认为问题在于 OP not 目前正在标记他的字符串,无论出于何种原因 关于复杂性的公平点。虽然,我更多地考虑使用 Trie 更快地拒绝的可能性,但是检查每个字符的需要使它成为一个小的改进。当然,不进行标记化对于复杂性来说会是一个相当大的问题。 @Nuclear:既然你已经在做,我想知道你为什么不建议模式字符串的通用后缀树/Aho-Corasick 自动机,让您解决O(n) 中的查询,但使用真正的子字符串搜索(无标记化)。您还可以计算匹配的聚合,例如匹配计数或与匹配关联的总“分数”(就像 OP 在他的 O(p*n) 实现中所做的那样)。 我不熟悉该算法,也不想花时间将算法扩展为在我编写它时几乎相同的东西,因为它不是很明显,这将是必要的,并且可能需要大量的空间成本,具体取决于它的实施方式。但是,仔细观察一下(在您发布之前不久),它似乎确实非常适合此目的。【参考方案2】:

您所描述的内容听起来像是 Aho-Corasick string-matching algorithm 的绝佳用例。该算法在源字符串中查找一组模式字符串的所有匹配项,并在线性时间(加上报告匹配项的时间)内完成。如果您有一组固定的字符串要搜索,您可以预先对模式进行线性预处理以非常快速地搜索所有匹配项。

这里有一个Java implementation of Aho-Corasick。我还没试过,但它可能是一个很好的匹配。

希望这会有所帮助!

【讨论】:

【参考方案3】:

我很确定 string.contains 已经高度优化,所以用其他东西替换它对你没有多大好处。

所以,我怀疑,要走的路不是在你的聊天词中寻找每一个银行词,而是一次进行多次比较。

第一种方法是创建一个巨大的正则表达式来匹配你所有的银行字。编译它并希望正则表达式包足够高效(机会是 - 它是)。您将有一个相当长的设置阶段(正则表达式编译),但匹配应该快得多。

【讨论】:

我一直很害怕使用正则表达式,因为我不太熟悉它。此外,我有点担心性能可能不会显着提高。 好吧,正则表达式非常简单 - 如果您的银行由“红色”、“蓝色”和“苹果”组成,那么您的正则表达式应该是 red|blue|apple 哦,在你尝试之前你不会知道性能。所以试试看吧。 @zmbq:它针对其用例进行了优化,但不适用于 OP 用例,您希望在字符串中找到多个模式 一旦你找到一个模式,你可以再次查看字符串的其余部分 - 甚至可以使用旧的较慢的方式,因为模式通常不会被发现。跨度> 【参考方案4】:

您可以为需要匹配的单词建立一个索引,并在处理它们时对其进行计数。如果您可以使用 HashMap 来查找每个单词的模式,则成本将为 O(n * m)

您可以对所有可能的词使用 HashMap,然后可以稍后剖析这些词。

例如说你需要匹配red和apple,你可以组合起来

redapple = 1
applered = 0
red = 10
apple = 15

这意味着红色实际上是11(10 + 1),而苹果是16(15 + 1)

【讨论】:

我有点困惑。所以我需要匹配的单词来自文本。因此,在这种情况下,red 和 apple 来自用户文本(haystack),而不是模式(needle)。但是从我必须从一个句子中制作的所有单词组合中进行缩放,这不是很困难吗?我可能误解了你的回答。 @Nopiforyou 你只需要你看到的所有单词,然后你对这些匹配进行模式匹配。那么你只需要考虑唯一词的数量,而不是所有的词。【参考方案5】:

我不懂韩语,所以我想用韩语修改字符串的策略不一定像英语那样可行,但也许这种伪代码策略可以用你的韩语知识应用到让它起作用。 (Java当然还是一样,但是例如,在韩语中,字母“ough”是否仍然很有可能是连续的?甚至还有字母“ough”吗?但是话虽如此,但希望原理可以应用

我会使用 String.toCharArray 创建一个二维数组(如果需要可变大小,则使用 ArrayList)。

if (first letter of word matches keyword's first letter)//we have a candidate
    skip to last letter of the current word //see comment below
    if(last letter of word matches keyword's last letter)//strong candidate
        iterate backwards to start+1 checking remainder of letters

我建议跳到最后一个字母的原因是,从统计上来说,一个单词的前两个字母的“辅音、元音”非常高,尤其是名词,因为任何食物都是名词(您给出的几乎所有关键字示例都与辅音元音的结构相匹配)。而且由于只有 5 个元音(加 y),第二个字母“i”出现在关键字“pizza”中的可能性本来就很高,但在那之后,这个词仍然很有可能变成不匹配。

但是,如果您知道第一个字母和最后一个字母匹配,那么您可能有一个更强大的候选者,然后可以反向迭代。我认为在更大的数据集上,这将比按顺序检查字母更快地消除候选人。基本上你会让太多的假候选人通过第二次迭代,从而增加你的整体条件操作。这听起来可能很小,但在这样的项目中,有很多重复,所以微优化会很快积累。

如果这种方法可以在结构上可能与英语非常不同的语言中应用(尽管我在这里说的是无知),那么我认为它可能会为您提供一些效率,无论您是否通过迭代 char 数组来实现它或使用扫描仪,或任何其他构造。

【讨论】:

我认为这种优化会受到韩语的限制,尽管它可能适用于英语。结尾的很多字符,尤其是动词,很难区分单词本身。【参考方案6】:

诀窍是要意识到,如果您可以将正在搜索的字符串描述为正则表达式,那么根据定义,您也可以使用状态机来描述它。

在消息中的每个字符处,为 1600 个模式中的每一个启动一个状态机,并将字符传递给它。这听起来很可怕,但相信我,它们中的大多数无论如何都会立即终止,所以你并没有真正做大量的工作。请记住,状态机通常可以在每一步使用简单的开关/外壳或ch == s.charAt 进行编码,因此它们接近于轻量级。

显然,当您的一台搜索机器在搜索结束时终止时,您知道该怎么做。任何在完全匹配之前终止的都可以立即丢弃。

private static class Matcher 
    private final int where;
    private final String s;
    private int i = 0;

    public Matcher ( String s, int where ) 
        this.s = s;
        this.where = where;
    

    public boolean match(char ch) 
        return s.charAt(i++) == ch;
    

    public int matched() 
        return i == s.length() ? where: -1;
    


// Words I am looking for.
String[] watchFor = new String[] "flies", "like", "arrow", "banana", "a";
// Test string to search.
String test = "Time flies like an arrow, fruit flies like a banana";

public void test() 
    // Use a LinkedList because it is O(1) to remove anywhere.
    List<Matcher> matchers = new LinkedList<> ();
    int pos = 0;
    for ( char c : test.toCharArray()) 
        // Fire off all of the matchers at this point.
        for ( String s : watchFor ) 
            matchers.add(new Matcher(s, pos));
        
        // Discard all matchers that fail here.
        for ( Iterator<Matcher> i = matchers.iterator(); i.hasNext(); ) 
            Matcher m = i.next();
            // Should it be removed?
            boolean remove = !m.match(c);
            if ( !remove ) 
                // Still matches! Is it complete?
                int matched = m.matched();
                if ( matched >= 0 ) 
                    // Todo - Should use getters.
                    System.out.println("    "+m.s +" found at "+m.where+" active matchers "+matchers.size());
                    // Complete!
                    remove = true;
                
            
            // Remove it where necessary.
            if ( remove ) 
                i.remove();
            
        
        // Step pos to keep track.
        pos += 1;
    

打印

flies found at 5 active matchers 6
like found at 11 active matchers 6
a found at 16 active matchers 2
a found at 19 active matchers 2
arrow found at 19 active matchers 6
flies found at 32 active matchers 6
like found at 38 active matchers 6
a found at 43 active matchers 2
a found at 46 active matchers 3
a found at 48 active matchers 3
banana found at 45 active matchers 6
a found at 50 active matchers 2

有几个简单的优化。通过一些简单的预处理,最明显的是使用当前字符来确定可能适用的匹配器。

【讨论】:

更有趣的是实际上一次匹配所有模式的 DFA。这正是Aho-Corasick 自动机所做的。 此外,您的实现必须为每个模式管理多达 O(m) 匹配器,其中 m 是模式大小。所以你基本上做一个O(n*m)字符串匹配一个非常高的常数因子(很多分配),而O(n)匹配很容易做到(例如使用String.contains)。你需要在自动机中构建一个失败函数,这样你至少每个模式只需要一个(如果实施得好,这会给你 Knuth-Morris-Pratt 算法) @NiklasB。 - 你是对的 - 有更好的算法。这种实现并不是最优的,它主要是为了演示将每个字符呈现给匹配器嵌套的技术,而不是要求每个匹配器依次搜索文本。【参考方案7】:

这是一个相当宽泛的问题,所以我不会详细介绍,但大致如下:

使用诸如广泛的lemmatizer 之类的东西对干草堆进行预处理,以创建消息的“仅主题词”版本,方法是注意其中的所有词都涵盖了哪些主题。例如,“汉堡包”、“披萨”、“可乐”、“午餐”、“晚餐”、“餐厅”或“麦当劳”的任何出现都会导致为该消息收集“主题”词“食物” .有些词可能有多个主题,例如“麦当劳”可能在主题“食物”和“商业”中。大多数单词没有任何主题。

在此过程之后,您将拥有仅包含“主题”字词的干草堆。然后创建一个Map&lt;String, Set&lt;Integer&gt;&gt; 并用主题词和包含它的聊天消息ID 集填充它。这是包含它的聊天消息的主题词的reverse index。

查找包含所有 n 个单词的所有文档的运行时代码非常简单且超快 - 接近 O(#terms):

private Map<String, Set<Integer>> index; // pre-populated

Set<Integer> search(String... topics) 
    Set<Integer> results = null;
    for (String topic : topics) 
        Set<Integer> hits = index.get(topic);
        if (hits == null)
            return Collections.emptySet();
        if (results == null)
            results = new HashSet<Integer>(hits);
        else
            results.retainAll(hits);
        if (results.isEmpty())
            return Collections.emptySet(); // exit early
    
    return results;

这将在 O(1) 附近执行,并且告诉你哪些消息共享所有搜索词。如果您只想要数字,请使用返回的Set 的微不足道的size()

【讨论】:

以上是关于Java indexOf(蛮力方法)对我或其他一些子字符串算法更实用吗?的主要内容,如果未能解决你的问题,请参考以下文章

java 最大公约数蛮力方法

Java每日一题——>739. 每日温度(蛮力法,栈方法)

Java每日一题——>739. 每日温度(蛮力法,栈方法)

Java每日一题——>739. 每日温度(蛮力法,栈方法)

Java问题中的蛮力数独求解器算法

Java 负数 indexOf (从末尾计数 [length()] )