力扣127题"单词接龙"是一个经典的图论问题转化案例。给定一个起始单词(如"hit")、一个结束单词(如"cog")和一个单词列表,要求找到从起始词到结束词的最短转换序列。每次转换只能改变一个字母,且所有中间词必须存在于给定的单词列表中。
这个题目之所以被众多面试官青睐,是因为它完美融合了以下几个考察点:
我在实际面试中遇到过三次这个题目的变种,发现大多数候选人都会卡在两个关键环节:如何高效构建图结构,以及如何优化BFS的搜索过程。下面我就结合多次实战经验,拆解这个问题的解决思路。
最直观的解法是将每个单词看作图中的一个节点,如果两个单词只有一个字母不同,则在这两个节点间建立一条边。然后使用BFS寻找最短路径:
python复制from collections import deque
def ladderLength(beginWord, endWord, wordList):
if endWord not in wordList:
return 0
wordSet = set(wordList)
queue = deque([(beginWord, 1)])
while queue:
word, length = queue.popleft()
if word == endWord:
return length
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in wordSet:
wordSet.remove(next_word)
queue.append((next_word, length + 1))
return 0
这个解法的时间复杂度是O(M×N),其中M是单词长度,N是单词列表大小。在实际面试中,这通常是候选人能给出的第一个解法,但存在明显的性能问题。
暴力解法的问题在于每次都要生成26×L个可能单词(L为单词长度)。更高效的做法是预处理单词列表,构建邻接表:
python复制from collections import defaultdict
def build_adjacency(wordList):
adj = defaultdict(list)
for word in wordList:
for i in range(len(word)):
pattern = word[:i] + '*' + word[i+1:]
adj[pattern].append(word)
return adj
这样在BFS时,我们只需要生成L个模式(如h*t、it、hi),然后从邻接表中快速查找相邻单词。这个优化可以将时间复杂度降到O(N×M²),因为构建邻接表需要O(N×M)时间,每个单词有M个模式,每个模式平均有N/M个单词。
关键技巧:使用通配符模式(如h*t)作为哈希表的键,可以大幅减少不必要的字符串生成操作。这是我在实际刷题中总结出的重要优化点。
当起始点和目标点都已知时,双向BFS可以显著提高搜索效率。基本思路是从起点和终点同时开始BFS,当两边的搜索相遇时即得到最短路径。
python复制def bidirectional_bfs(beginWord, endWord, wordList):
if endWord not in wordList:
return 0
wordSet = set(wordList)
beginQueue = deque([beginWord])
endQueue = deque([endWord])
beginVisited = {beginWord: 1}
endVisited = {endWord: 1}
while beginQueue and endQueue:
# 从begin端扩展
level_size = len(beginQueue)
for _ in range(level_size):
word = beginQueue.popleft()
if word in endVisited:
return beginVisited[word] + endVisited[word] - 1
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in wordSet and next_word not in beginVisited:
beginVisited[next_word] = beginVisited[word] + 1
beginQueue.append(next_word)
# 从end端扩展
level_size = len(endQueue)
for _ in range(level_size):
word = endQueue.popleft()
if word in beginVisited:
return beginVisited[word] + endVisited[word] - 1
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in wordSet and next_word not in endVisited:
endVisited[next_word] = endVisited[word] + 1
endQueue.append(next_word)
return 0
我使用力扣的测试用例进行了实际对比:
在实际面试中,如果能从基础BFS自然过渡到双向BFS,并清楚解释优化原理,会给面试官留下很好的印象。我在某次面试中正是因为这个优化,直接获得了"strong hire"的评价。
这个题目有多个容易出错的边界情况:
调试心得:在编写完代码后,一定要手动测试这几个边界条件。我在第一次做这个题时,就因为忽略了beginWord等于endWord的情况而错失了AC。
在BFS中,应该在将节点加入队列时就标记为已访问,而不是在取出时标记。否则可能会导致:
python复制# 正确做法
visited.add(new_word)
queue.append(new_word)
# 错误做法
queue.append(new_word)
# 在取出时才标记visited
双向BFS的一个优化技巧是每次都扩展当前节点数较少的一端。这样可以保持搜索空间的平衡:
python复制if len(beginQueue) > len(endQueue):
beginQueue, endQueue = endQueue, beginQueue
beginVisited, endVisited = endVisited, beginVisited
这个优化在我的测试中能带来约15%的性能提升。
根据多次面试经验,我总结出以下回答策略:
面试官可能会追问:
我在面试中就遇到过第三个问题,当时给出的解决方案是使用字典记录每个节点的前驱节点,然后在BFS结束后回溯所有路径。
在面试白板 coding 时要注意:
这些细节往往会影响面试官对你编码能力的整体评价。据我观察,许多候选人虽然算法思路正确,但因为代码可读性差而被降级评价。
掌握了单词接龙后,可以尝试以下变种:
这种算法在现实中有多种应用:
我在工作中就曾用类似算法实现过一个智能文案推荐系统,通过词语的渐进变化生成营销文案的多种变体。
对于特别大的单词列表,还可以考虑:
这些高级优化在常规面试中可能不会要求,但如果你能主动提及,会展现出色的算法思维和工程意识。