LeetCode 472「连接词」这道题目初看像是一道普通的字符串处理题,但实际上它考察的是对字典和动态规划/深度优先搜索的综合运用能力。题目要求我们从一个不包含重复单词的数组中,找出所有能够由数组中至少两个更短单词拼接而成的连接词。
在实际工程场景中,这类问题非常常见。比如在搜索引擎中,我们需要将用户输入的复合词拆分为更基础的词汇进行检索;在自然语言处理中,分词系统需要判断一个长词是否由多个词典中的基础词组成;在用户输入校验系统中,我们需要检查某个输入是否由多个黑名单词汇组合而成。
这道题的核心难点在于:
最直观的解决思路可以分解为以下几步:
然而,这种朴素的方法存在几个严重问题:
为了解决上述问题,我们需要引入几个关键优化:
预处理排序:将所有单词按长度从小到大排序。这样可以确保在处理长词时,所有可能用于拼接它的短词都已经在字典中了。
动态构建字典:在处理每个单词前,先检查它是否能被当前字典中的词拼接,然后再将它加入字典。这样可以避免使用自己拆分自己的情况。
高效判断方法:对于单个单词能否被拼接的判断,我们有两种主流选择:
在Swift实现中,DFS配合记忆化的方法通常更直观且易于实现,这也是我们最终选择的方案。
swift复制class Solution {
func findAllConcatenatedWordsInADict(_ words: [String]) -> [String] {
// 按长度排序,短词优先
let sortedWords = words.sorted { $0.count < $1.count }
var wordSet = Set<String>()
var result: [String] = []
for word in sortedWords {
if word.isEmpty { continue }
if canForm(word, wordSet) {
result.append(word)
}
wordSet.insert(word)
}
return result
}
}
主函数的逻辑非常清晰:
swift复制private func canForm(_ word: String, _ dict: Set<String>) -> Bool {
if dict.isEmpty { return false }
let chars = Array(word)
var memo = Array(repeating: false, count: chars.count)
func dfs(_ start: Int) -> Bool {
if start == chars.count {
return true
}
if memo[start] {
return false
}
var current = ""
for i in start..<chars.count {
current.append(chars[i])
if dict.contains(current) {
if dfs(i + 1) {
return true
}
}
}
memo[start] = true
return false
}
return dfs(0)
}
这个函数是算法的核心,它使用DFS配合记忆化来判断一个单词是否能被字典中的词拼接:
排序的重要性:
记忆化技术:
DFS与DP的选择:
设:
对于单个单词的DFS判断:
整体复杂度:
在题目给定的约束条件下(总字符数≤1e5),这个复杂度是完全可接受的。
主要空间消耗来自:
因此总空间复杂度为O(n * L),在合理范围内。
在实际编码面试或工程实现中,还可以考虑以下优化:
提前终止条件:
双指针优化:
并行处理:
swift复制let solution = Solution()
let words1 = [
"cat","cats","catsdogcats",
"dog","dogcatsdog",
"hippopotamuses",
"rat","ratcatdogcat"
]
print(solution.findAllConcatenatedWordsInADict(words1))
// 预期输出: ["catsdogcats", "dogcatsdog", "ratcatdogcat"]
let words2 = ["cat","dog","catdog"]
print(solution.findAllConcatenatedWordsInADict(words2))
// 预期输出: ["catdog"]
空输入测试:
swift复制print(solution.findAllConcatenatedWordsInADict([]))
// 预期输出: []
包含空字符串:
swift复制print(solution.findAllConcatenatedWordsInADict(["", "cat", "dog", "catdog"]))
// 预期输出: ["catdog"]
无连接词情况:
swift复制print(solution.findAllConcatenatedWordsInADict(["a", "b", "c"]))
// 预期输出: []
重复使用同一单词:
swift复制print(solution.findAllConcatenatedWordsInADict(["a", "aa", "aaa"]))
// 预期输出: ["aa", "aaa"]
对于大规模数据测试,可以构造如下用例:
swift复制// 生成10000个随机短词
var largeInput = (1...10000).map { _ in
String((0..<3).map { _ in "abcdefghij".randomElement()! })
}
// 添加一些由这些短词组成的长词
largeInput += [
largeInput[0] + largeInput[1],
largeInput[2] + largeInput[3] + largeInput[4],
largeInput[5] + largeInput[6]
]
// 测试算法性能
let start = Date()
_ = solution.findAllConcatenatedWordsInADict(largeInput)
let duration = Date().timeIntervalSince(start)
print("处理10000个单词耗时: \(duration)秒")
在搜索引擎系统中,这类算法可以用于:
在NLP领域,这种技术可以用于:
在安全领域,这种算法可以:
常见原因包括:
解决方法:
如果需要考虑更复杂的情况:
如果需要支持:
解决方案:
在实际工程中,我们有时需要支持:
这类问题通常需要:
处理不同语言时需要考虑:
解决方案:
当词典规模非常大时:
实现要点: