每日leetcode:电话号码的字母组合(python)&回溯法以及递归的理解

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

我首先的想法是对于每一个新读入的数字对应的每一个字母,需要在前面已经存在的每一个字符串的末尾加上该字母。也就是说,如果新读入数字之前字符串有m个,新读入的数字对应的字母有n个,那么新的结果就应该有m*n个字符串。

于是我考虑的就是将读入新数字之前的字符串复制n次,然后依次在每个字符串后面添加新的字母。

但是这样就会出现一个错位问题,解决办法:①跳跃添加,也就是每隔m个字符串就在后面添加一个字母,如23,读入3之前要对2对应的字符串复制3次,也就是[a,b,c,a,b,c,a,b,c],然后在第一个a后面加上d,隔3个字符串,也就是在第二个a后面加上e。②排序,先排序然后逐个添加新字母,如[a,b,c,a,b,c,a,b,c]排序之后就是[a,a,a,b,b,b,c,c,c],直接依照顺序循环添加def就可以

很明显①的效率更高,事实上跑完之后结果确实是这样

    def letterCombinations(self, digits: str) -> List[str]:
        dic = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }
        ret = []
        if len(digits) == 0:
            return []
        firstChara = dic[digits[0]]
        for i in range(len(firstChara)):
            ret.extend(firstChara[i])
        for i in range(1,len(digits)):
            chara = dic[digits[i]]
            num = len(chara)
            numPre = len(ret)
            temp_ret = ret.copy()
            for j in range(num-1):
                ret.extend(temp_ret.copy())
            ret.sort()
            for j in range(numPre):
                for k in range(num):
                    ret[j * num + k] += chara[k]
        return ret       

②:在①的基础上稍微修改就行

for j in range(num-1):
    ret.extend(temp_ret.copy())
for j in range(numPre):
    for k in range(num):
    ret[k * numPre + j] += chara[k]     

后来通过答案中的直接在原来的ret基础上添加尾部字符而不复制,可以将代码的逻辑与实现进一步简化:直接将①中的第二个大的for循环替换为以下代码

for i in range(len(digits)):
    ret_temp = ret.copy()
    chara = dic[digits[i]]
    ret = []
    for j in range(len(chara)):
        for k in range(len(ret_temp)):
            ret.append(ret_temp[k]+chara[j])
return ret

速度反而下降,可以理解,毕竟之前是直接复制,然后逐个在尾部添加需要插入的字母,现在是直接在一个空的List中逐个添加新的字符串,而且形成新的字符串的步骤没变;简单来讲就是之前的复制变成了逐个添加。(需要注意的是ret_temp = ret.copy()这一句是没有必要copy的,可以改成ret_temp = ret,因为在这一句之后紧跟的ret = [],已经表明不会修改原来的内容了,修改这一句之后运行时间变成76ms)

然后继续优化:依靠索引遍历太慢,因为遍历索引之后还要再根据索引从List中取出索引对应的内容,如果可以直接遍历内容的话应该会快很多

ret = [""]
if not digits:
    return []
firstChara = dic[digits[0]]
for i in range(len(digits)):
    ret_temp = ret
    ret = []
    for add in dic[digits[i]]:
        for temp in ret_temp:
            ret.append(temp+add)
    ret_temp = ret
return ret

看来确实遍历索引再逐个取出元素的过程确实是一个可以优化的地方

最后我看到标准解答中官方给出了回溯这个算法,我打算系统学习一下这个算法,回溯这个算法是可以用递归和非递归的方式来实现的

    def letterCombinations(self, digits: str):
        dic = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }
        ret = [""]
        n = len(digits)
        if n == 0:
            return []
        def helper(i):
            nonlocal ret
            if i == n:
                return
            else:
                charas = dic[digits[i]]
                temp_ret = []
                for chara in charas:
                    for cur_ret in ret:
                        temp_ret.append(cur_ret+chara)
                ret = temp_ret
                helper(i+1)
        helper(0)
        return ret

这个应该不算严格意义上的回溯,因为这个更像是BFS而非DFS

严格的应该这样写

    def letterCombinations(self, digits: str):
        dic = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }
        if not digits:
            return []
        ret = []
        n = len(digits)
        def helper(i, curStr):
            nonlocal ret
            if i == n:
                ret.append(curStr)
                return
            else:
                for chara in dic[digits[i]]:
                    helper(i+1,curStr+chara)
        helper(0,"")
        return ret

但是运行速度太低,大概要80ms,改进了一下会快一点

        if not digits:
            return []
        ret = [""]
        n = len(digits)
        def helper(i, curStr):
            nonlocal ret
            if i == n:
                return
            else:
                ret.pop()
                for chara in dic[digits[i]]:
                    ret.append(curStr+chara)
                    helper(i+1,curStr+chara)
        helper(0,"")
        return ret

再次改进

        if not digits:
            return []
        ret = [""]
        n = len(digits)
        def helper(i, curStr):
            nonlocal ret
            if i == n:
                return
            else:
                ret.pop()
                for chara in dic[digits[i]]:
                    curStr += chara
                    ret.append(curStr)
                    helper(i+1,curStr)
                    curStr = curStr[:-1]
        helper(0,"")
        return ret

但是我找不到原因。。。(个人认为是因为在递归算法中,在越深层做的操作越少,那么运行效率就会越高)

后来我又尝试了用非递归的方式来实现这个算法,发现回溯的非递归实现范式是针对结果可能是任何一个节点或几个节点的组合的这种问题的,像排列这种同一行的节点之间不可能出现在同一个结果中的问题要用非递归来实现并不容易,具体的难点有:循环退出的条件是什么,递归实现运行到return或者函数的最后一行就自动出栈,在非递归中不存在这种条件,普通的非递归实现算法是通过可能的解的数量变为0为退出条件,而这个问题不存在可能的解:每一个都是解。再其次,就是存储结果的结构了,总的来说就是一般的回溯只需要一个...算了应该是我太菜了,学习一阵子再来填这个坑吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值