哈希算法题目:单词接龙
字数 1919 2025-12-10 09:56:18
哈希算法题目:单词接龙
题目描述
给你两个单词 beginWord 和 endWord,以及一个单词列表 wordList。需要找出从 beginWord 到 endWord 的最短转换序列的长度。转换规则如下:
- 每次只能转换一个字母。
- 转换过程中的每个单词都必须是
wordList中的单词(endWord也必须在wordList中)。 - 如果无法完成转换,则返回 0。
示例:
输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,”dot”,”dog”,”lot”,”log”,”cog”]
输出:5
解释:最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”,长度为 5。
解题思路
我们可以把每个单词看作图中的一个节点。如果两个单词之间只有一个字母不同,那么它们之间就存在一条无向边。这样,问题就转化为了在无向图中,求从起点节点(beginWord)到终点节点(endWord)的最短路径问题。对于最短路径问题,广度优先搜索(BFS) 是最合适的方法,因为它会按层探索,首次遇到目标节点时的路径长度就是最短的。
哈希表在这个问题中扮演着关键角色:
- 快速查找与去重:我们需要快速判断一个单词是否在
wordList中,以及是否已经被访问过,使用HashSet可以实现 O(1) 时间复杂度的查找。 - 虚拟节点优化(核心技巧):直接比较任意两个单词是否只差一个字母需要 O(L) 时间(L 为单词长度),如果对每个单词都和其余单词比较,会非常慢(O(N²L))。我们可以为每个单词创建 L 个“虚拟节点”。例如单词 “hot”,它的三个虚拟节点是:
*ot,h*t,ho*。那么,所有能通过变换一个字母得到的单词(如 “dot”, “lot”)都会共享同一个虚拟节点。这样,我们只需要为当前单词生成所有虚拟节点,然后查找哪些真实单词也对应这些虚拟节点,即可快速找到下一层的节点。管理“单词到其虚拟节点列表”以及“虚拟节点到对应单词列表”的关系,使用哈希表(HashMap)是最佳选择。
循序渐进讲解
第 1 步:基础检查与数据结构准备
首先进行边界条件检查,并初始化几个核心的哈希集合/映射。
// 1. 将单词列表转换为哈希集合,方便快速查找和去重
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) {
return 0; // 终点不在列表中,无法转换
}
// 移除起点,避免重复处理(如果它在列表中的话)
wordSet.remove(beginWord);
// 2. 使用队列进行BFS
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
// 3. 记录已访问过的单词和其对应的路径长度(层次)
Map<String, Integer> visited = new HashMap<>();
visited.put(beginWord, 1);
第 2 步:构建虚拟节点的映射关系(预处理)
这是优化性能的关键一步。我们为 wordList 中的每个单词(包括 beginWord 和 endWord)构建其所有可能的虚拟模式,并建立从“虚拟模式”到“所有能匹配该模式的真实单词”的映射。
// key: 虚拟模式,如 “*ot”
// value: 能匹配这个模式的所有真实单词列表,如 [“hot”, “dot”, “lot”]
Map<String, List<String>> patternToWords = new HashMap<>();
for (String word : wordList) {
for (int i = 0; i < word.length(); i++) {
// 生成第i个字符被替换为‘*’的模式
String pattern = word.substring(0, i) + "*" + word.substring(i + 1);
// 将该单词加入该模式对应的列表中
patternToWords.computeIfAbsent(pattern, k -> new ArrayList<>()).add(word);
}
}
// 注意:beginWord 也需要加入这个映射,因为转换可能从它开始
for (int i = 0; i < beginWord.length(); i++) {
String pattern = beginWord.substring(0, i) + "*" + beginWord.substring(i + 1);
patternToWords.computeIfAbsent(pattern, k -> new ArrayList<>()).add(beginWord);
}
第 3 步:执行广度优先搜索(BFS)
BFS 会逐层探索,每深入一层,路径长度(即转换序列长度)加 1。
while (!queue.isEmpty()) {
String currentWord = queue.poll();
int currentLevel = visited.get(currentWord); // 当前单词所在的层次(路径长度)
// 遍历当前单词的每一个字符位置,生成所有可能的虚拟模式
for (int i = 0; i < currentWord.length(); i++) {
String pattern = currentWord.substring(0, i) + "*" + currentWord.substring(i + 1);
// 通过哈希表,快速获取所有与当前单词“相邻”(差一个字母)的单词
for (String neighbor : patternToWords.getOrDefault(pattern, new ArrayList<>())) {
// 如果邻居就是终点,直接返回结果(当前层数+1)
if (neighbor.equals(endWord)) {
return currentLevel + 1;
}
// 如果这个邻居单词没有被访问过,且存在于单词集中
if (!visited.containsKey(neighbor) && wordSet.contains(neighbor)) {
// 标记为已访问,并记录其层次
visited.put(neighbor, currentLevel + 1);
// 将其加入队列,以便下一层探索
queue.offer(neighbor);
}
}
}
}
第 4 步:返回结果
如果 BFS 结束都没有遇到 endWord,说明不存在转换序列。
return 0; // 无法到达终点
算法复杂度分析
- 时间复杂度:O(N * L²)。其中 N 是单词列表中的单词数量,L 是每个单词的长度。主要开销在于:1) 构建模式映射 O(N * L);2) BFS 过程中,每个单词会被处理一次,每次处理需要生成 L 个模式,并通过哈希表 O(L) 时间获取邻居(因为构建映射时,每个单词的每个模式都对应一个列表,获取列表是 O(1),但遍历列表中的单词是 O(N) 的潜在风险?实际上,由于每个单词只被访问一次,且每条边(虚拟节点连接的单词对)也只会被遍历一次,总操作数与边数成正比,最坏情况下为 O(N²L)。但使用虚拟节点后,平均情况远好于直接的逐对比较。
- 空间复杂度:O(N * L)。存储模式映射需要 O(N * L) 的空间(每个单词有 L 个模式),BFS 队列和访问记录需要 O(N) 的空间。
关键点总结
- 问题转化:将单词转换问题抽象为无向图的最短路径问题。
- 核心算法:使用 BFS 来保证找到的路径是最短的。
- 哈希表的妙用:
HashSet(wordSet) 用于 O(1) 时间判断单词有效性。HashMap(visited) 用于记录访问状态和路径长度。HashMap(patternToWords) 是性能优化的核心,它通过“虚拟节点”的概念,将寻找邻居的操作从 O(N*L) 降到了 O(L)。我们不再需要比较当前单词和列表中的所有其他单词,而是直接通过其虚拟模式找到所有可能的下一跳。
这种方法高效地解决了“单词接龙”问题,是图论 BFS 与哈希表数据结构结合的经典应用。