每天学习一点算法 2026/05/14
题目:单词接龙
字典 wordList 中从单词 beginWord 到 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk:
每一对相邻的单词只差一个字母。
对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
sk == endWord
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。
这题很简单我们可以将所有的序列看成一个树结构,beginWord 为根节点,子节点是只差一个字母的单词,层序遍历就能找到最短的转换序列。
我们要思考一下如何判断两个单词只差一个字母,最简单的办法是写双重循环做判断,但是如果每个单词都要这样的对比话时间复杂就很高了,这里我们直接把当前单词的每一位,从 a 改到 z,生成所有可能的变体,再看变体是否在字典里。
例子:cat
- 第 0 位:*at → aat, bat, cat, dat...zat
- 第 1 位:c*t → cat, cbt, cct...czt
- 第 2 位:ca* → caa, cab, cac...caz
typescript
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const set = new Set(wordList) // 使用 set 存储 wordList 便于查找相邻单词
if (!set.has(endWord)) return 0 // 如果集合没有结尾单词直接返回 0
const queue: Array<[string, number]> = [] // 队列用于层序遍历
queue.push([beginWord, 1]) // 初始放入 beginWord 作为根节点
// 进行层序遍历
while (queue.length > 0) {
const [word, level] = queue.shift() // 队首元素出队列
// 寻找相邻单词
for (let i = 0; i < word.length; i++) {
// 替换当前节点单词的每以为字母,寻找匹配的相邻单词
for (let code = 97; code <= 122; code++) {
const newChar = String.fromCharCode(code) // 生成字母
if (newChar === word[i]) continue // 原单词直接跳过
const newWord = word.slice(0, i) + newChar + word.slice(i + 1) // 替换字母后的单词
if (newWord === endWord) return level + 1 // 如果是结尾单词直接返回 level + 1
// 如果是集合里的单词
if (set.has(newWord)) {
queue.push([newWord, level + 1]) // 作为下一层的节点push进队列
set.delete(newWord) // 从集合中移除,避免重复使用
}
}
}
}
return 0 // 遍历完毕还没有遇到结尾单词表示没有比配序列
};
这道题我们还可以利用双向广度搜索的方法来减少搜索事件。
双向广度搜索就是,从起点开始往外扩散 + 从终点开始往外扩散,两边在中间相遇时,就是最短路径。
核心思想:
-
用两个集合:
-
beginSet:从 beginWord 扩散出来的层 -
endSet:从 endWord 扩散出来的层
-
-
关键点 :每次 选更小的集合扩展 ,减少搜索量
-
扩展时生成新单词,如果新单词出现在对面集合,说明相遇了
-
总步数 = 起点步数 + 终点步数 + 1
typescript
function ladderLength(beginWord: string, endWord: string, wordList: string[]): number {
const set = new Set(wordList) // 使用 set 存储 wordList 便于查找相邻单词
if (!set.has(endWord)) return 0 // 如果集合没有结尾单词直接返回 0
let beginSet = new Set([beginWord]) // 开始单词集合
let endSet = new Set([endWord]) // 结束单词集合
set.delete(endWord) // 在集合中删除结束单词
let level = 1 // 初始化层数为 1
// 进行双向广度搜索
while (beginSet.size > 0) {
const nextSet: Set<string> = new Set() // 用于存储本次扩展的单词
// 将较小的集合作为扩展的目标
if (beginSet.size > endSet.size) {
[beginSet, endSet] = [endSet, beginSet]
}
// 对集合中的单词进行相邻单词扩展
for (let word of beginSet) {
for (let i = 0; i < word.length; i++) {
for (let c = 97; c <= 122; c++) {
const char = String.fromCharCode(c);
if (char === word[i]) continue;
const newWord = word.slice(0, i) + char + word.slice(i + 1); // 扩展单词
if (endSet.has(newWord)) return level + 1 // 如果扩展单词出现在另一个集合中,两边相遇,找到最短路径
if (set.has(newWord)) {
set.delete(newWord) // 移除已使用单词
nextSet.add(newWord) // 记录扩展单词
}
}
}
}
beginSet = nextSet // 下一层集合赋值给扩展的集合(如果 set 为空,nextSet比为空,此时会跳出循环)
level++
}
return 0
};
题目来源:力扣(LeetCode)