You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python字典get与in操作性能差异致LeetCode代码超时问题排查

问题背景

针对 LeetCode 1155 掷骰子等于目标和的方法数 题目,两段仅缓存判断逻辑存在差异的动态规划代码出现了显著性能差距:一段提交时超出时间限制,另一段可以正常通过。

超时实现代码

class Solution:
    def numRollsToTarget(self, dices: int, faces: int, target: int) -> int:
        
        dp = {}
        def ways(t, rd):
            if t == 0  and rd == 0: return 1
            if t <= 0 or rd <= 0: return 0
            if  dp.get((t,rd)): 
                return dp[(t,rd)]
            dp[(t,rd)] = sum(ways(t-i, rd-1) for i in range(1,faces+1))
            return dp[(t,rd)]
        
        return ways(target, dices) % (10**9 + 7)

可正常通过的实现代码

class Solution:
    def numRollsToTarget(self, dices: int, faces: int, target: int) -> int:
        
        dp = {}
        def ways(t, rd):
            if t == 0  and rd == 0: return 1
            if t <= 0 or rd <= 0: return 0
            if  (t,rd) in dp: 
                return dp[(t,rd)]
            dp[(t,rd)] = sum(ways(t-i, rd-1) for i in range(1,faces+1))
            return dp[(t,rd)]
        
        return ways(target, dices) % (10**9 + 7)

核心疑问

两段代码的唯一差异为缓存命中判断逻辑:超时版本使用dp.get(tuple)做判断,通过版本使用tuple in dp做判断。已知两种操作的平均时间复杂度均为O(1),为何会产生如此大的性能差异?

  • 初始猜想:调用dp.get()时如果存在大量哈希冲突,内部需要遍历冲突链表才能返回None,但不确定in操作为何不会产生同样的链表遍历开销。
  • 补充验证:将元组类型的key替换为-分隔的字符串作为字典key测试,性能差异仍然存在,可排除元组key的影响。
  • 附加问题:这类性能问题有什么通用调试方法?

问题解答

性能差异根本原因

性能差异和哈希冲突没有关系,核心是超时版本的判断逻辑存在bug,导致缓存部分失效,时间复杂度从多项式级退化为指数级:

  1. 逻辑错误:将值的真值判断等同于key存在判断
    dp.get(key)的行为是key存在时返回对应value,不存在时返回None。直接把返回值放在if条件中,本质是判断返回值的布尔真假,而非判断key是否存在。
    本题中大量合法状态的缓存值为0(比如2个6面骰子凑出13点的方法数就是0),而Python中整数0是假值。这种场景下dp.get(key)返回0,if条件判定为不成立,代码会误以为该状态从未计算过,重新进入递归展开计算,直接把记忆化递归O(dices * target)的时间复杂度打回无缓存的指数级,必然超时。
  2. 额外常数开销
    就算缓存值不为0,dp.get(key)已经完成一次哈希查询拿到了结果,后续return dp[(t,rd)]会再做一次哈希查询取同一个值,平白多了一倍字典操作开销,不过这是次要因素,不会直接导致超时。

而(t,rd) in dp是直接判断key是否存在于字典中,和对应value是0还是其他数值完全无关,所有计算过的状态都会正常命中缓存,不会出现重复计算,自然可以顺利通过。把key换成字符串后性能差异仍然存在,也正好印证了问题和key类型、哈希冲突无关,是判断逻辑本身的bug。

如果要修正get写法的逻辑,判断条件需要写成if dp.get((t, rd)) is not None:,但这种写法仍然比in版本多一次后续的字典取值操作,性能更差。

这类性能问题的通用调试方法

  • 调用计数:在递归函数入口加全局计数器,统计函数被调用的总次数。正常记忆化递归的调用次数应该和状态总数(即dices*target)处于同一量级,如果计数远大于这个值,就能直接定位到缓存失效问题。
  • 性能分析工具:用Python标准库的cProfile运行测试用例,输出函数调用次数、各函数耗时占比,一眼就能看出递归函数被异常重复调用的问题。
  • 小用例单步调试:用极小的测试用例(比如dices=2、faces=6、target=13)单步执行,观察缓存分支的触发逻辑,很容易发现值为0时缓存分支未命中的问题。
  • 避免手写缓存:写Python记忆化逻辑时优先用functools.lru_cache装饰器,不会出现这类判断逻辑错误,原生实现的性能也普遍好于手写字典缓存。

内容的提问来源于stack exchange,提问作者sprksh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 11:33:13