在一个巨大的集合中找到两个字符串的所有串联

时间:2018-09-10 04:42:54

标签: java algorithm string-algorithm

给定一组50k个字符串,我需要找到所有对(s, t),这样sts + t都包含在该集合中。

我尝试过的

,还有一个附加约束:s.length() >= 4 && t.length() >= 4。这样就可以按长度4个前缀和后缀分别对字符串进行分组。然后,对于每个长度至少为8的字符串composed,我使用s的前四个字符和composed的候选集查找t的候选集使用最后四个字符。这可行,但是需要寻找3000万个候选对(s, t)才能找到7k的结果。

这个令人惊讶的大量候选人来自于以下事实:字符串是有限词汇量中的(大部分是德语)单词,而单词的开始和结束通常相同。它仍然比尝试所有2.5G对要好得多,但比我希望的要差得多。

我需要什么

随着附加约束的放开和集合的增加,我正在寻找更好的算法。

“遗漏”问题

有人抱怨我没有问问题。因此,缺少的问号在下一个句子的末尾。 如何在不使用约束的情况下更有效地做到这一点?

4 个答案:

答案 0 :(得分:5)

算法1:测试对,而非单打

一种方法可能是,不是从所有可能的对到包含这些对的所有可能的复合字符串工作,而是从所有可能的复合字符串看是否包含对。这将问题从n^2查找(其中n是字符串数== 4个字符)变为m * n查找(其中m是所有字符串的平均长度> = 8个字符,减去7,现在n是> = 8个字符的字符串数)。这是一种实现方式:

int minWordLength = 4;
int minPairLength = 8;

Set<String> strings = Stream
   .of(
      "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
      "bear", "hug", "bearhug", "cur", "curlique", "curl",
      "down", "downstream", "stream"
   )
   .filter(s -> s.length() >= minWordLength)
   .collect(ImmutableSet.toImmutableSet());

strings
   .stream()
   .filter(s -> s.length() >= minPairLength)
   .flatMap(s -> IntStream
      .rangeClosed(minWordLength, s.length() - minWordLength)
      .mapToObj(splitIndex -> ImmutableList.of(
         s.substring(0, splitIndex),
         s.substring(splitIndex)
      ))
      .filter(pair ->
          strings.contains(pair.get(0))
          && strings.contains(pair.get(1))
      )
   )
   .map(pair ->
      pair.get(0) + pair.get(1) + " = " + pair.get(0) + " + " + pair.get(1)
   )
   .forEach(System.out::println);

给出结果:

downstream = down + stream

如上所述,它的平均算法复杂度为m * n。因此,实际上O(n)。在最坏的情况下,O(n^2)。有关算法复杂性的更多信息,请参见hash table

说明

  1. 将所有字符串四个或更多字符放入一个哈希集中(用于搜索的平均O(1)复杂度)。为了方便起见,我使用了番石榴的ImmutableSet。使用任何您喜欢的东西。
  2. filter:仅限于长度为八个或更多字符的项目,代表我们的候选对象是列表中另外两个单词的组合。
  3. flatMap:对于每个候选项,计算所有可能的子词对,确保每个词的长度至少为4个字符。由于可以有多个结果,因此实际上是一个列表列表,因此将其展平为一个深列表。
    1. rangeClosed:生成所有整数,这些整数表示将在我们要检查的单词对中第一个单词中的字符数。
    2. mapToObj:将每个整数与我们的候选字符串结合使用以输出两个项目的列表(在生产代码中,您可能希望更清楚一些东西,例如两属性值类或适当的现有类) 。
    3. filter:仅限于两个都在列表中的对。
  4. map:将结果略显一些。
  5. forEach:输出到控制台。

算法选择

此算法已调整为比列表中的项目数短得多的单词。如果列表很短并且单词很长,那么切换回撰写任务而不是分解任务会更好。鉴于此列表的大小为50,000个字符串,而长单词的德语单词不太可能超过50个字符,因此采用此算法的比例为1:1000。

反之,如果您有50个字符串,平均长度为50,000个字符,那么使用另一种算法将效率更高。

算法2:对候选列表进行排序并保存

我考虑了一段时间的一种算法是对列表进行排序,但要知道,如果一个字符串代表一个对的开始,那么可能是其对之一的所有候选字符串将紧随其后,在以该字符串开头的一组项目中。在上面对我棘手的数据进行排序,并添加一些混杂因素(downer, downs, downregulate),我们得到:

a
abc
abcdef
bear
bearhug
cur
curl
curlique
def
down ---------\
downs         |
downer        | not far away now!
downregulate  |
downstream ---/
hug
shine
stream
sun
sunshine

因此,如果保留要检查的所有项目的运行集合,我们可以在每个单词基本上恒定的时间内找到候选组合,然后直接针对其余单词探查哈希表:

int minWordLength = 4;

Set<String> strings = Stream
   .of(
      "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
      "bear", "hug", "bearhug", "cur", "curlique", "curl",
      "down", "downs", "downer", "downregulate", "downstream", "stream")
   .filter(s -> s.length() >= minWordLength)
   .collect(ImmutableSet.toImmutableSet());

ImmutableList<String> orderedList = strings
   .stream()
   .sorted()
   .collect(ImmutableList.toImmutableList());
List<String> candidates = new ArrayList<>();
List<Map.Entry<String, String>> pairs = new ArrayList<>();

for (String currentString : orderedList) {
   List<String> nextCandidates = new ArrayList<>();
   nextCandidates.add(currentString);
   for (String candidate : candidates) {
      if (currentString.startsWith(candidate)) {
         nextCandidates.add(candidate);
         String remainder = currentString.substring(candidate.length());
         if (remainder.length() >= minWordLength && strings.contains(remainder)) {
            pairs.add(new AbstractMap.SimpleEntry<>(candidate, remainder));
         }
      }
   }
   candidates = nextCandidates;
}
pairs.forEach(System.out::println);

结果:

down=stream

此算法的复杂性稍微复杂一点。我认为搜索部分是O(n)的平均值,其中O(n^2)是最坏的情况。最昂贵的部分可能是排序,这取决于所使用的算法和未排序数据的特征。因此,将它与一粒盐一起使用,但是有可能。在我看来,这比从庞大的数据集中构建Trie的方式要便宜得多,因为您只需全面地对其进行一次探查,而不会得到任何摊销成本。

此外,这次我选择了一个Map.Entry来保持一对。您的操作方式完全是任意的。制作自定义Pair类或使用一些现有的Java类就可以了。

答案 1 :(得分:1)

您可以通过避免使用String视图创建大部分子CharBuffer并更改其位置和限制来改善Erik’s answer

Set<CharBuffer> strings = Stream.of(
    "a", "abc", "abcdef", "def", "sun", "sunshine", "shine",
    "bear", "hug", "bearhug", "cur", "curlique", "curl",
    "down", "downstream", "stream"
 )
.filter(s -> s.length() >= 4) // < 4 is irrelevant
.map(CharBuffer::wrap)
.collect(Collectors.toSet());

strings
    .stream()
    .filter(s -> s.length() >= 8)
    .map(CharBuffer::wrap)
    .flatMap(cb -> IntStream.rangeClosed(4, cb.length() - 4)
        .filter(i -> strings.contains(cb.clear().position(i))&&strings.contains(cb.flip()))
        .mapToObj(i -> cb.clear()+" = "+cb.limit(i)+" + "+cb.clear().position(i))
    )
    .forEach(System.out::println);

这是相同的算法,因此不会改变时间复杂度,除非您合并了隐藏字符数据复制成本,否则这是另一个因素(乘以平均字符串长度)。

当然,仅当您使用与打印火柴不同的终端操作时,差异才会变得很明显,因为安静的打印是一项昂贵的操作。同样,当源是大文件上的流时,I / O将主导操作。除非您进入一个完全不同的方向,例如使用内存映射并重构此操作以在ByteBuffer s上进行操作。

答案 2 :(得分:0)

可能的解决方法是这样。 您从第一个字符串作为前缀开始,第二个字符串作为后缀开始。 您遍历每个字符串。如果字符串以第一个字符串开头,则检查它是否以第二个字符串结尾。并继续进行到最后。为了节省时间检查字母是否相同,您可以进行长度检查。 这几乎就是您所做的,但是通过增加长度检查,您也许可以删节一些。至少这是我的看法。

答案 3 :(得分:0)

不确定这是否比您的解决方案更好,但我认为值得尝试。

构建两个Tries,一个具有正常顺序的候选者,另一个具有相反的单词。

从深度Trie向内向前走4,然后使用叶子的其余部分确定后缀(或类似的后缀),并在后退Trie中进行查找。 / p>

我过去在https://stackoverflow.com/a/9320920/823393此处发布了一个Trie实现。