题目难度: 中等

****

今天继续更新程序员面试金典系列, 大家在公众号 算法精选 里回复 面试金典 就能看到该系列当前连载的所有文章了, 记得关注哦~

题目描述

给定字典中的两个词,长度相等。写一个方法,把一个词转换成另一个词, 但是一次只能改变一个字符。每一步得到的新词都必须能在字典中找到。

编写一个程序,返回一个可能的转换序列。如有多个可能的转换序列,你可以返回任何一个。

示例 1:

  • 输入:
    • beginWord = "hit",
    • endWord = "cog",
    • wordList = ["hot","dot","dog","lot","log","cog"]
  • 输出:
    • ["hit","hot","dot","lot","log","cog"]

示例 2:

  • 输入:
    • beginWord = "hit"
    • endWord = "cog"
    • wordList = ["hot","dot","dog","lot","log"]
  • 输出: []
    • 解释: endWord "cog" 不在字典中,所以不存在符合要求的转换序列。

题目思考

  1. 如何找到可以直接转换的有效单词?
  2. 如何得到转换序列?

解决方案

思路

  • 分析题目, 我们需要判断各个单词是否在字典中, 所以我们可以先将给定的单词字典(列表)转成集合, 优化查找的时间复杂度
  • 接下来就是找可以转换的单词, 这里有两种思路:
    • 思路 1: 遍历整个字典(假设有 N 个单词), 找所有与当前单词只相差一个字符的单词 (假设单词平均长度为 C), 时间复杂度是 O(NC)
    • 思路 2: 从当前单词出发, 遍历其每个字符下标, 并将其替换成剩余 25 个字母, 从而构造出新的单词, 然后如果该单词在字典中则说明它有效, 时间复杂度是 O(25*C)
  • 通过比较, 显然思路 2 的方案更优, 它的效率跟字典的单词个数无关
  • 找到了可以转换的单词, 接下来就是确定符合要求的转换序列
  • 这里我们可以采用 BFS 的思想, 结合一个 visit 集合来避免重复遍历
  • 注意由于我们需要构造出整个转换序列, 所以这里需要将 visit 集合升级成一个前向映射字典
  • 该字典的 key 是当前单词, value 是前一个单词, 这样它就兼具了避免重复遍历, 以及构造出转换序列的功能
  • 具体做法就是找到终点单词后, 根据前向映射字典依次找到其前面的单词, 直到达到起点单词为止, 对应的序列的逆序即为最终要求的转换序列
  • 下面代码有详细的注释, 方便大家理解

复杂度

  • 时间复杂度 O(NC): 假设字典的单词数目是 N, 每个单词的平均长度是 C, 那么每个单词最多遍历一次, 这部分时间是 O(N), 而找邻居单词需要遍历整个单词并替换成其他 25 个字母, 这部分时间是 O(25*C), 所以总时间复杂度是 O(NC)
  • 空间复杂度 O(NC): 维护了一个最多有 N 个单词的映射字典

代码

class Solution:
    def findLadders(self, beginWord: str, endWord: str, wordList: List[str]) -> List[str]:
        # BFS+前向映射字典
        # 先将给定的单词字典转成集合, 优化查找的时间复杂度
        ws = set(wordList)
        if endWord not in ws:
            # 终点单词不在字典中, 无法转换
            return []
        q = [beginWord]
        # 记录转换序列中当前单词到前一个单词的映射关系
        # 例如a->b->c, 则字典存储{c:b}和{b:a}
        preDict = {}

        def getNextWord(w):
            # 找到每个单词的所有只更改一个字符的邻居单词
            # 例如abc更改第一个字符的邻居单词就是[bbc,cbc,...,zbc]
            for i in range(len(w)):
                # 尝试更改第i个下标的字符
                for o in range(26):
                    c = chr(o + ord("a"))
                    if c != w[i]:
                        # 需要保证该字符不等于原始字符
                        nex = w[:i] + c + w[i + 1 :]
                        yield nex

        for w in q:
            for nex in getNextWord(w):
                if nex in ws and nex not in preDict:
                    # 如果相邻字符在字典中, 且不在前向映射字典中(表示该单词尚未遍历过), 则可以将其加入转换序列中
                    # 更新前向映射字典
                    preDict[nex] = w
                    if nex == endWord:
                        # 该单词恰好等于终点单词, 说明找到了一个有效转换序列
                        # 然后利用前向映射字典, 重建整个转换序列
                        res = []
                        while True:
                            res.append(nex)
                            if nex == beginWord:
                                # 达到起点单词, 跳出循环
                                break
                            # 找转换序列的前一个单词
                            nex = preDict[nex]
                        # 注意加入单词的顺序是从终点单词开始, 所以最终结果需要逆序
                        return res[::-1]
                    q.append(nex)
        return []

大家可以在下面这些地方找到我~😊

我的 GitHub

***********

*******

我的知乎专栏

我的头条号

我的牛客网博客

我的公众号: 算法精选, 欢迎大家扫码关注~😊

算法精选 - 微信扫一扫关注我