Python 贪婪递归算法的时间复杂度

Python 贪婪递归算法的时间复杂度,python,recursion,time-complexity,big-o,greedy,Python,Recursion,Time Complexity,Big O,Greedy,我编写了一个贪婪的递归算法,以找到进行给定更改的最小硬币数。现在我需要估计它的时间复杂度。由于算法根据相同的i(n*n)嵌套了“ifs”,内部块将递归调用(log(2)n)减半,我相信正确的答案可能是O(n*log(n)),这是由以下计算得出的: n*log2(n)*O(1) 请告诉我您对我的分析是否正确的想法,并对我的贪婪递归算法提出改进建议 这是我的递归算法: coins = [1, 5, 10, 21, 25] coinsArraySize = len(coins) change = 63

我编写了一个贪婪的递归算法,以找到进行给定更改的最小硬币数。现在我需要估计它的时间复杂度。由于算法根据相同的i(n*n)嵌套了“ifs”,内部块将递归调用(log(2)n)减半,我相信正确的答案可能是O(n*log(n)),这是由以下计算得出的:

n*log2(n)*O(1)

请告诉我您对我的分析是否正确的想法,并对我的贪婪递归算法提出改进建议

这是我的递归算法:

coins = [1, 5, 10, 21, 25]
coinsArraySize = len(coins)
change = 63
pickedCoins = []

def findMin(change, i, pickedCoins):              
    if (i>=0):
        if (change >= coins[i]):               
           pickedCoins.append(coins[i])
           findMin(change - coins[i], i, pickedCoins)
        else:           
           findMin(change, i-1, pickedCoins)
findMin(change, coinsArraySize-1, pickedCoins)

什么是
n
?运行时间取决于金额和具体硬币。例如,假设您有一百万个硬币,从1到1000000,并尝试为1进行更改。在最终找到可以使用的最大硬币(1)之前,代码将经历一百万个递归级别。最后,如果你只有一枚硬币()并试图兑换1000000美元,那么你会立刻找到那枚硬币,但要花一百万次的时间去深拣那枚硬币

这是一个非递归版本,它改进了这两个方面:使用二进制搜索查找下一个可用的硬币,一旦找到合适的硬币,就尽可能多地使用它

def makechange(amount, coins):
    from bisect import bisect_right
    # assumes `coins` is sorted. and that coins[0] > 0
    right_bound = len(coins)
    result = []
    while amount > 0:
        # Find largest coin <= amount.
        i = bisect_right(coins, amount, 0, right_bound)
        if not i:
            raise ValueError("don't have a coin <=", amount)
        coin = coins[i-1]
        # How many of those can we use?
        n, amount = divmod(amount, coin)
        assert n >= 1
        result.extend([coin] * n)
        right_bound = i - 1
    return result
这本质上是
O(n+n log n)
其中
n
len(硬币)

寻找最优解 正如@Stef在一篇评论中指出的,贪婪算法并不总能找到最少数量的硬币。这要难得多。通常的方法是通过,最坏的情况是
O(amt*len(coins))
time。但这也是最好的情况:它“自下而上”工作,找到最小数量的硬币,达到1,然后2,然后3,然后4,…,最后
amt

def mincoins(amt, coins):
    from collections import deque
    coins = sorted(set(coins)) # increasing, no duplicates

    # Map amount to coin that was subtracted to reach it.
    a2c = {amt : None}
    d = deque([amt])
    while d:
        x = d.popleft()
        for c in coins:
            y = x - c
            if y < 0:
                break # all remaining coins too large
            if y in a2c:
                continue # already found cheapest way to get y
            a2c[y] = c
            d.append(y)
            if not y:    # done!
                d.clear()
                break
    if 0 not in a2c:
        raise ValueError("not possible", amt, coins)
    picks = []
    a = 0
    while True:
        c = a2c[a]
        if c is None:
            break
        picks.append(c)
        a += c
    assert a == amt
    return sorted(picks)
因此,我将建议一种不同的方法,使用广度优先树搜索,从初始数量向下计算,直到达到0。最坏情况下的
O()
行为是相同的,但最佳情况下的时间要好得多。对于评论:

mincoins(10000, [1, 2000, 3000])
这种情况下,在找到最佳的4硬币解决方案之前,它会查看不到20个节点。因为这是一个广度优先的搜索,它知道不可能有更短的路径到达根,所以可以立即停止

举个最坏的例子,试试看

mincoins(1000001, range(2, 200, 2))
所有的硬币都是偶数,所以不可能收集到任何一个硬币的总和都是奇数。树必须扩展到50万层深才能意识到0是不可访问的。但是,虽然高级别的分支因子是
O(len(coins))
,但整个扩展树中的节点总数以
amt+1为界(鸽子洞原则:dict最多只能有
amt+1
键,因此超出该值的任何数量的节点都必然是重复的目标,因此在生成后都会被丢弃)。因此,实际上,在这种情况下,树很快变宽,但很快就会变窄,变深

还要注意的是,这种方法可以很容易地重新构建总计为
amt
的最小硬币集合

def mincoins(amt, coins):
    from collections import deque
    coins = sorted(set(coins)) # increasing, no duplicates

    # Map amount to coin that was subtracted to reach it.
    a2c = {amt : None}
    d = deque([amt])
    while d:
        x = d.popleft()
        for c in coins:
            y = x - c
            if y < 0:
                break # all remaining coins too large
            if y in a2c:
                continue # already found cheapest way to get y
            a2c[y] = c
            d.append(y)
            if not y:    # done!
                d.clear()
                break
    if 0 not in a2c:
        raise ValueError("not possible", amt, coins)
    picks = []
    a = 0
    while True:
        c = a2c[a]
        if c is None:
            break
        picks.append(c)
        a += c
    assert a == amt
    return sorted(picks)
def mincoins(金额,硬币):
从集合导入deque
硬币=分类(设置(硬币))#增加,无重复
#地图上的数字等于减去后的硬币数。
a2c={amt:None}
d=deque([amt])
而d:
x=d.popleft()
对于硬币中的c:
y=x-c
如果y<0:
打破#所有剩余的硬币都太大了
如果a2c中为y:
继续#已经找到了最便宜的方式获得y
a2c[y]=c
d、 附加(y)
如果不是y:#完成!
d、 清除()
打破
如果0不在a2c中:
提高值错误(“不可能”,金额,硬币)
选择=[]
a=0
尽管如此:
c=a2c[a]
如果c为无:
打破
picks.append(c)
a+=c
断言a==amt
返回已排序(拾取)

每个递归调用将更改至少减少1,并且没有分支(也就是说,您的递归树实际上是一条直线,因此实际上不需要递归)。您的运行时间是
O(n)

没那么简单:
其他分支根本不改变
改变
;相反,它减少了
i
。一个普通的
n
在这里没有意义,因为
改变
和硬币数量都在起作用。@TimPeters是,也不是。因为OP要求运行时间作为的函数de>n
,我假设硬币的数量是固定的,这意味着它只影响常数。@IgorRivin如果你将此算法转换为迭代算法,它将有嵌套的循环,例如,这些ifs可能会变成whiles。我看不到这个问题的复杂性更低。@PedroCoelho“内部循环”将最多运行不同硬币的数量,这是一个常量(问题的措辞)。如果硬币的数量也是一个输入(称之为
m
),则有一个明显的
O(nm)
bound,常数取决于硬币的面额。但是,除非硬币的数量和金额有某种关联,否则我看不到任何日志术语。谢谢你的透彻解释,蒂姆。考虑到硬币数组是[1,5,10,21,25],复杂度会是多少变化是63?考虑到在这个问题上我们已经有了这些信息,只有一种复杂性,而不是最坏和最好的情况,对吗?再一次,什么是
n
?如果所有的输入都是固定的,没有什么变化。它需要花费任何时间,最坏、最好和平均的情况都是一样的-只有一种情况;-)如果你保持硬币固定并改变传入的数量,这就是
n
,那么我的算法最坏的情况是
O(n)
,但是如果你使用dict输出格式,最坏的情况是
O(log2(n))
。实际上,如果硬币集是固定的,那么dic