为什么在Python中,反向递归执行速度比正向递归更快

16 投票
2 回答
2180 浏览
提问于 2025-04-18 05:43

我写了一个用Python编写的算法,用来计算用不同面额的硬币凑成一定金额的方法有多少种:

@measure
def countChange(n, coin_list):
    maxIndex = len(coin_list)
    def count(n, current_index):        
        if n>0 and maxIndex>current_index:
            c = 0
            current = coin_list[current_index]
            max_coeff = int(n/current)      
            for coeff in range(max_coeff+1):
                c+=count(n-coeff*current, current_index+1)
        elif n==0: return 1
        else: return 0
        return c
    return count(n, 0)

我的算法使用一个索引来获取硬币的面额,正如你所看到的,我的索引在每次进入一个新的调用栈时是逐渐增加的。我意识到这个算法也可以这样写:

@measure
def countChange2(n, coin_list):
    maxIndex = len(coin_list)
    def count(n, current_index):        
        if n>0 and 0<=current_index:
            c = 0
            current = coin_list[current_index]
            max_coeff = int(n/current)      
            for coeff in range(max_coeff+1):
                c+=count(n-coeff*current, current_index-1)
        elif n==0: return 1
        else: return 0
        return c
    return count(n, maxIndex-1)

这次,索引在每次进入一个新的调用栈时是逐渐减少的。我比较了这两个函数的执行时间,发现它们之间有很大的差别:

print(countChange(30, range(1, 31)))
print(countChange2(30, range(1, 31)))

>> Call to countChange took 0.9956174254208345 secods.
>> Call to countChange2 took 0.037631815734429974 secods.

为什么这两个算法的执行时间差别这么大,即使我并没有缓存结果呢?索引的增加顺序为什么会影响执行时间?

2 个回答

8

三个数字的组合并不多。

原因是,当你向前探索时,你需要考虑每一种可能性;但是当你向后推导时,你可以不计算就排除掉很多无效的解。

向前推算时,你的代码会调用计数函数500,000次。

而向后推算时,你的代码只需要调用计数函数30,000次……

你可以通过记忆化调用(也就是保存之前的结果)来加快这两个过程,或者改变你的算法,避免重复调用。

12

这其实和动态规划没什么关系,按照我的理解。单纯地反转索引并不会让东西变得“动态”。

这里发生的事情是这个算法对输入非常“敏感”。你可以试着把输入反过来给它。例如,

print(countChange(30, list(reversed(range(1, 31)))))
print(countChange2(30, list(reversed(range(1, 31)))))

就像有些排序算法在处理已经排好序的数据时非常快,而在处理反向排序的数据时却很慢,这里也是这种情况。

当输入是递增的时候,countChange 需要更多的迭代才能得到最终的答案,所以看起来就慢很多。然而,当输入是递减的时候,性能特点就反过来了。

撰写回答