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<String, Set<Integer>>
并用主题词和包含它的聊天消息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(蛮力方法)对我或其他一些子字符串算法更实用吗?的主要内容,如果未能解决你的问题,请参考以下文章