算法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)
。有关算法复杂性的更多信息,请参见哈希表。
解释
- 将所有四个或更多字符长的字符串放入一个哈希集中(搜索平均复杂度为 O(1))。为了方便起见,我使用了番石榴
ImmutableSet
。使用任何你喜欢的东西。
filter
:仅限于长度为 8 个或更多字符的项目,代表我们的候选项目是列表中其他两个单词的组合。
flatMap
:对于每个候选,计算所有可能的子词对,确保每个子词至少有 4 个字符长。由于可能有多个结果,这实际上是一个列表列表,因此将其展平为一个单深列表。
rangeClosed
:生成所有整数,表示将在我们将检查的对的第一个单词中的字符数。
mapToObj
:使用每个整数与我们的候选字符串组合来输出两个项目的列表(在生产代码中,您可能想要更清晰的东西,例如两个属性值类或适当的现有类)。
filter
:仅限于两者都在列表中的对。
map
: 效果稍微好一点。
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 类就可以了。