1. 力扣139题「单词拆分」问题解析
第一次看到这个题目时,我正坐在星巴克刷力扣周赛。题目描述很简单:给定一个非空字符串s和一个包含非空单词列表的字典wordDict,判断s是否可以被空格拆分为一个或多个字典中出现的单词。比如s="leetcode",wordDict=["leet","code"],显然可以拆分成"leet code"。
这类字符串匹配问题在面试中很常见,特别是来自Facebook和Google的面试官特别喜欢考察。我记录下解题过程中遇到的几个关键点和优化思路,希望能帮助到正在准备面试的你。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与暴力解法
2.1 问题重述与示例
给定:
- 字符串s = "catsanddog"
- 字典wordDict = ["cat","cats","and","sand","dog"]
需要判断s是否能被完全拆分成字典中的单词。这个例子中,可以拆分为"cats and dog"或"cat sand dog",所以返回true。
2.2 递归思路与实现
最直观的想法是递归尝试所有可能的拆分方式:
python复制def wordBreak(s, wordDict):
wordSet = set(wordDict)
def helper(s):
if not s: return True
for i in range(1, len(s)+1):
if s[:i] in wordSet and helper(s[i:]):
return True
return False
return helper(s)
这个解法虽然正确,但时间复杂度高达O(2^n),当s="aaaaaaaaab",wordDict=["a","aa","aaa"]时会超时。
3. 动态规划解法优化
3.1 DP状态定义
定义dp[i]表示s的前i个字符能否被拆分。我们需要计算dp[n],n为s的长度。
初始化:
- dp[0] = True (空字符串可以被拆分)
状态转移方程:
对于每个i,检查所有j < i,如果dp[j]为True且s[j:i]在字典中,则dp[i]=True。
3.2 DP实现代码
python复制def wordBreak(s, wordDict):
wordSet = set(wordDict)
n = len(s)
dp = [False]*(n+1)
dp[0] = True
for i in range(1, n+1):
for j in range(i):
if dp[j] and s[j:i] in wordSet:
dp[i] = True
break
return dp[n]
时间复杂度O(n^2),空间复杂度O(n)。
3.3 优化技巧
- 字典预处理:将wordDict转为set,查询时间从O(m)降到O(1)
- 提前终止:内层循环可以从i-max_len开始,max_len是字典中最长单词长度
- 记忆化搜索:递归解法中加入memoization也能达到类似DP的效果
4. BFS解法与性能对比
4.1 BFS思路
将问题看作图搜索,每个节点代表一个拆分位置。从位置0开始,尝试所有可能的单词匹配,将新的位置加入队列。
python复制from collections import deque
def wordBreak(s, wordDict):
wordSet = set(wordDict)
visited = set()
queue = deque([0])
max_len = max(len(w) for w in wordDict) if wordDict else 0
while queue:
start = queue.popleft()
if start == len(s):
return True
for end in range(start+1, min(start+max_len+1, len(s)+1)):
if end not in visited and s[start:end] in wordSet:
visited.add(end)
queue.append(end)
return False
4.2 性能对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 递归 | O(2^n) | O(n) | 小规模数据 |
| DP | O(n^2) | O(n) | 通用解法 |
| BFS | O(n^2) | O(n) | 字典单词长度差异大时更优 |
5. 常见错误与调试技巧
5.1 典型错误案例
- 忘记处理空字符串情况
- 字典未去重导致重复计算
- DP数组初始化错误(长度应为n+1)
- 边界条件处理不当(如s="", wordDict=[""])
5.2 调试建议
- 先手动计算小测试用例
- 打印DP表观察状态转移
- 使用assert检查中间结果
- 对于超时问题,先分析最坏情况
6. 题目变种与扩展
6.1 返回所有可能的拆分
这是力扣140题的要求。需要在DP基础上进行回溯:
python复制def wordBreak(s, wordDict):
wordSet = set(wordDict)
memo = {}
def dfs(s):
if s in memo: return memo[s]
if not s: return [""]
res = []
for word in wordSet:
if s.startswith(word):
for substr in dfs(s[len(word):]):
res.append(word + (" " + substr if substr else ""))
memo[s] = res
return res
return dfs(s)
6.2 字典树(Trie)优化
当字典很大时,可以用Trie加速前缀匹配:
python复制class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Solution:
def wordBreak(self, s, wordDict):
root = TrieNode()
for word in wordDict:
node = root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.is_word = True
n = len(s)
dp = [False]*(n+1)
dp[0] = True
for i in range(n):
if dp[i]:
node = root
for j in range(i, n):
if s[j] not in node.children:
break
node = node.children[s[j]]
if node.is_word:
dp[j+1] = True
return dp[n]
7. 面试实战技巧
- 先确认问题细节:字典是否为空?字符串是否为空?大小写敏感?
- 从暴力解法开始,逐步优化
- 画图说明DP状态转移过程
- 讨论时间/空间复杂度时考虑最坏情况
- 主动提出可能的优化方向(如字典预处理)
- 准备好测试用例:空输入、极端情况、正常情况
我在面试候选人时,最看重的是解题思路的清晰度和沟通能力。即使一开始没有想到最优解,能够一步步分析问题并优化方案,往往比直接写出完美代码更重要。
