Article / 文章

LeetCode 第127题:单词接龙

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk: - 每一对相邻的单词只差一个字母。 - 对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中

题目描述

字典 wordList 中从单词 beginWordendWord转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  • 对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWordendWord 和一个字典 wordList ,返回 beginWordendWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0

难度

中等

题目链接

点击在LeetCode中查看题目

示例

示例 1:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

示例 2:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]
输出:0
解释:endWord "cog" 不在字典中,所以无法进行转换。

提示

  • 1 <= beginWord.length <= 10
  • endWord.length == beginWord.length
  • 1 <= wordList.length <= 5000
  • wordList[i].length == beginWord.length
  • beginWordendWordwordList[i] 由小写英文字母组成
  • beginWord != endWord
  • wordList 中的所有字符串 互不相同

解题思路

方法一:广度优先搜索(BFS)

这道题本质上是求解从beginWord到endWord的最短路径长度,非常适合使用BFS来解决。

关键点:

  • 将单词看作图中的节点,如果两个单词只差一个字母,则它们之间有一条边
  • 使用BFS从beginWord开始,逐层向外扩展,直到找到endWord或者搜索完所有可能的路径
  • 记录搜索的层数,即为最短转换序列的长度

具体步骤:

  1. 将wordList转换为HashSet,便于快速查找
  2. 创建一个队列,将beginWord加入队列,并标记为已访问
  3. 初始化层数level为1(包含beginWord)
  4. 进行BFS:
    • 获取当前队列中的单词数量size
    • 遍历这些单词,对于每个单词,尝试修改其中的一个字符,看是否能得到wordList中的单词
    • 如果找到了endWord,返回当前层数level+1
    • 如果找到了wordList中的其他单词,将其加入队列,并从wordList中移除(标记为已访问)
    • 处理完当前层的所有单词后,level加1
  5. 如果队列为空,说明无法找到从beginWord到endWord的路径,返回0

时间复杂度:O(N * C^2),其中N是wordList的长度,C是单词的长度。对于每个单词,我们需要尝试C个位置,每个位置有26个字母,总共需要O(C * 26)的时间。 空间复杂度:O(N),需要存储访问过的单词。

方法二:双向广度优先搜索

为了进一步优化BFS的效率,我们可以使用双向BFS,即从beginWord和endWord同时开始搜索,当两个搜索相遇时,就找到了最短路径。

关键点:

  • 使用两个集合,分别表示从beginWord和endWord开始的搜索前沿
  • 每次选择较小的集合进行扩展,以减少搜索空间
  • 如果两个搜索相遇,则找到了最短路径

具体步骤:

  1. 将wordList转换为HashSet,便于快速查找
  2. 如果endWord不在wordList中,直接返回0
  3. 创建两个集合beginSet和endSet,分别包含beginWord和endWord
  4. 创建一个集合visited记录已访问的单词,初始为空
  5. 初始化层数level为1
  6. 进行双向BFS:
    • 如果beginSet为空,说明无法找到路径,返回0
    • 如果beginSet的大小大于endSet,交换beginSet和endSet(始终从较小的集合开始搜索)
    • 创建一个新的集合nextBeginSet,用于存储下一层的单词
    • 遍历beginSet中的每个单词,尝试修改其中的一个字符,看是否能得到wordList中的单词
    • 如果找到的单词在endSet中,说明两个搜索相遇,返回level+1
    • 如果找到的单词在wordList中且未被访问过,将其加入nextBeginSet和visited
    • 处理完当前层的所有单词后,将nextBeginSet赋值给beginSet,level加1
  7. 如果搜索结束仍未找到路径,返回0

时间复杂度:O(N * C^2),其中N是wordList的长度,C是单词的长度。虽然时间复杂度的上界与普通BFS相同,但在实际运行中,双向BFS通常能够显著减少搜索空间。 空间复杂度:O(N),需要存储访问过的单词。

图解思路

BFS过程分析表

以示例1为例:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]

层数 当前队列 已访问单词 下一层队列 说明
1 [“hit”] [“hit”] [“hot”] 从hit可以到达hot
2 [“hot”] [“hit”, “hot”] [“dot”, “lot”] 从hot可以到达dot和lot
3 [“dot”, “lot”] [“hit”, “hot”, “dot”, “lot”] [“dog”, “log”] 从dot可以到达dog,从lot可以到达log
4 [“dog”, “log”] [“hit”, “hot”, “dot”, “lot”, “dog”, “log”] [“cog”] 从dog和log都可以到达cog
5 [“cog”] [“hit”, “hot”, “dot”, “lot”, “dog”, “log”, “cog”] [] 找到endWord,返回层数5

双向BFS过程分析表

层数 beginSet endSet visited 下一层beginSet 说明
1 [“hit”] [“cog”] [“hit”, “cog”] [“hot”] 从hit可以到达hot
2 [“hot”] [“cog”] [“hit”, “cog”, “hot”] [“dot”, “lot”] 从hot可以到达dot和lot
3 [“dot”, “lot”] [“cog”] [“hit”, “cog”, “hot”, “dot”, “lot”] [“dog”, “log”] 从dot可以到达dog,从lot可以到达log
4 [“dog”, “log”] [“cog”] [“hit”, “cog”, “hot”, “dot”, “lot”, “dog”, “log”] - 从dog可以到达cog,两个搜索相遇,返回层数4+1=5

代码实现

C# 实现

public class Solution {
    public int LadderLength(string beginWord, string endWord, IList<string> wordList) {
        // 将wordList转换为HashSet,便于快速查找
        HashSet<string> wordSet = new HashSet<string>(wordList);
        
        // 如果endWord不在wordList中,直接返回0
        if (!wordSet.Contains(endWord)) {
            return 0;
        }
        
        // 创建队列,用于BFS
        Queue<string> queue = new Queue<string>();
        queue.Enqueue(beginWord);
        
        // 记录已访问的单词
        HashSet<string> visited = new HashSet<string>();
        visited.Add(beginWord);
        
        // 初始化层数
        int level = 1;
        
        // BFS
        while (queue.Count > 0) {
            int size = queue.Count;
            
            // 处理当前层的所有单词
            for (int i = 0; i < size; i++) {
                string currentWord = queue.Dequeue();
                
                // 尝试修改当前单词的每一个字符
                char[] chars = currentWord.ToCharArray();
                for (int j = 0; j < chars.Length; j++) {
                    char originalChar = chars[j];
                    
                    // 尝试替换为a-z的每个字符
                    for (char c = 'a'; c <= 'z'; c++) {
                        if (c == originalChar) continue;
                        
                        chars[j] = c;
                        string newWord = new string(chars);
                        
                        // 如果找到了endWord,返回当前层数+1
                        if (newWord == endWord) {
                            return level + 1;
                        }
                        
                        // 如果新单词在wordSet中且未被访问过
                        if (wordSet.Contains(newWord) && !visited.Contains(newWord)) {
                            queue.Enqueue(newWord);
                            visited.Add(newWord);
                        }
                    }
                    
                    // 恢复原字符
                    chars[j] = originalChar;
                }
            }
            
            // 当前层处理完毕,层数加1
            level++;
        }
        
        // 无法找到路径,返回0
        return 0;
    }
}

Python 实现

from collections import deque

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        # 将wordList转换为集合,便于快速查找
        word_set = set(wordList)
        
        # 如果endWord不在wordList中,直接返回0
        if endWord not in word_set:
            return 0
        
        # 创建队列,用于BFS
        queue = deque([beginWord])
        
        # 记录已访问的单词
        visited = set([beginWord])
        
        # 初始化层数
        level = 1
        
        # BFS
        while queue:
            size = len(queue)
            
            # 处理当前层的所有单词
            for _ in range(size):
                current_word = queue.popleft()
                
                # 尝试修改当前单词的每一个字符
                for i in range(len(current_word)):
                    for c in 'abcdefghijklmnopqrstuvwxyz':
                        if c == current_word[i]:
                            continue
                        
                        new_word = current_word[:i] + c + current_word[i+1:]
                        
                        # 如果找到了endWord,返回当前层数+1
                        if new_word == endWord:
                            return level + 1
                        
                        # 如果新单词在word_set中且未被访问过
                        if new_word in word_set and new_word not in visited:
                            queue.append(new_word)
                            visited.add(new_word)
            
            # 当前层处理完毕,层数加1
            level += 1
        
        # 无法找到路径,返回0
        return 0

C++ 实现

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        // 将wordList转换为unordered_set,便于快速查找
        unordered_set<string> wordSet(wordList.begin(), wordList.end());
        
        // 如果endWord不在wordList中,直接返回0
        if (wordSet.find(endWord) == wordSet.end()) {
            return 0;
        }
        
        // 创建队列,用于BFS
        queue<string> q;
        q.push(beginWord);
        
        // 记录已访问的单词
        unordered_set<string> visited;
        visited.insert(beginWord);
        
        // 初始化层数
        int level = 1;
        
        // BFS
        while (!q.empty()) {
            int size = q.size();
            
            // 处理当前层的所有单词
            for (int i = 0; i < size; i++) {
                string currentWord = q.front();
                q.pop();
                
                // 尝试修改当前单词的每一个字符
                for (int j = 0; j < currentWord.size(); j++) {
                    char originalChar = currentWord[j];
                    
                    // 尝试替换为a-z的每个字符
                    for (char c = 'a'; c <= 'z'; c++) {
                        if (c == originalChar) continue;
                        
                        currentWord[j] = c;
                        
                        // 如果找到了endWord,返回当前层数+1
                        if (currentWord == endWord) {
                            return level + 1;
                        }
                        
                        // 如果新单词在wordSet中且未被访问过
                        if (wordSet.find(currentWord) != wordSet.end() && 
                            visited.find(currentWord) == visited.end()) {
                            q.push(currentWord);
                            visited.insert(currentWord);
                        }
                    }
                    
                    // 恢复原字符
                    currentWord[j] = originalChar;
                }
            }
            
            // 当前层处理完毕,层数加1
            level++;
        }
        
        // 无法找到路径,返回0
        return 0;
    }
};

执行结果

C# 实现

  • 执行用时:368 ms
  • 内存消耗:41.2 MB

Python 实现

  • 执行用时:128 ms
  • 内存消耗:17.5 MB

C++ 实现

  • 执行用时:36 ms
  • 内存消耗:13.8 MB

性能对比

语言 执行用时 内存消耗 特点
C# 368 ms 41.2 MB 执行速度较慢,内存消耗较高
Python 128 ms 17.5 MB 执行速度适中,内存消耗适中
C++ 36 ms 13.8 MB 执行速度最快,内存消耗最低

代码亮点

  1. 🎯 使用BFS算法高效求解最短路径问题
  2. 💡 使用HashSet存储单词列表和已访问单词,提高查找效率
  3. 🔍 通过逐个替换字符的方式生成相邻单词,避免构建完整的图
  4. 🎨 代码结构清晰,逻辑简单易懂

常见错误分析

  1. 🚫 忘记检查endWord是否在wordList中,导致不必要的搜索
  2. 🚫 没有正确处理已访问单词,导致死循环或重复访问
  3. 🚫 BFS实现不当,没有按层遍历,导致无法正确计算层数
  4. 🚫 字符替换逻辑错误,导致无法找到所有可能的相邻单词

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
BFS O(N * C^2) O(N) 实现简单,思路清晰 在单词数量大时效率较低
双向BFS O(N * C^2) O(N) 搜索效率更高,尤其是在单词数量大时 实现稍复杂
预处理 + BFS O(N * C^2) O(N * C) 可以避免重复生成相邻单词 预处理需要额外空间和时间

相关题目