求和算法的时间复杂度

时间:2016-03-20 19:38:26

标签: python algorithm big-o

我已经看了之前的帖子,我仍然在努力寻找这两个递归算法的T(n)和大O,每个算法都以一系列数字作为参数,并对所有数字进行求和。列表(最后一项除外)然后将总和添加到最后一项。任何人都可以请光一点。

def sum(numberSequence):
    assert (len(numberSequence) > 0)
    if (len(numberSequence) == 1):
        return numberSequence[0]
    else:
        return sum(numberSequence[-1]) + numberSequence[:-1]

(我相信bigO是O(n),因为在最坏的情况下,该函数被称为n-1次,但不确定当它只是对列表的一部分求和时会发生什么。我有T(n) = n x n-1 + n = O(n)它似乎不对。)

def binarySum(numberSequence):
    assert (len(numberSequence) > 0)
    breakPoint = int(len(numberSequence)/2)
    if (len(numberSequence) == 1):
        return numberSequence[0]
    else:
        return binarySum(numberSequence[:breakPoint]) + binarySum(numberSequence[breakPoint:])

我在这一点上迷失了,我认为大O是O(log2 n),因为它是二分搜索,但是整个列表没有被分成两半,只是大部分列表。

任何帮助都将不胜感激。

3 个答案:

答案 0 :(得分:1)

您按任何顺序汇总任意大小的N个数字列表。

如果没有一些限制,你不会找到一种更聪明的方法来更快地做到这一点。

总是Ω(N)(下限是N加法运算 - 你不会比这更好。)

作为下面提到的评论者你的算法实际上可能更糟 - 它只是不能更好。

答案 1 :(得分:1)

已编辑根据有关 O(n) [/]的效果的评论进行更正。

TL; DR:可能是 O(n),,但你的版本是 O(n²)。

请记住,所有大O符号都假设“时间不变”。也就是说, O(n)实际上意味着 O(k * n),而 O(log n)实际上意味着 O( k * log n)

让我们看看你的第一个例子:

def sum(numberSequence):
    assert (len(numberSequence) > 0)
    if (len(numberSequence) == 1):
        return numberSequence[0]
    else:
        return sum(numberSequence[-1]) + numberSequence[:-1]

第一行是assertcomparelenlen操作是列表和元组的常量时间(但它可能不适用于其他一些数据结构!注意!),compare是一个常量时间,而assert实际上是一个恒定时间,因为如果它曾经失败了整个事情爆发,我们停止计算。所以我们只需要调用assert函数调用加上一个比较加一个返回。

现在,这个函数被调用了多少次?好吧,终止条件显然代表一次,并且每隔一次它在一个比前一个列表短的列表上递归。因此,该函数将被称为len(numberSequence)次,对于我们的目的而言是n

所以我们有

  1 * call (for the user calling us)
+ n * assert 
+ n * len 
+ n * compare

接下来,我们有if语句,用于标记递归的终止条件。显然,这个陈述只会成功一次(这是终止条件,对吧?只发生在最后......)所以这是每次比较,每次总和一次就是常数的回报索引。

  n * compare
+ 1 * constant index
+ 1 * return

最后,有else:分支。我很确定你有一个错误,它应该是这个(注意冒号的位置):

        return sum(numberSequence[:-1]) + numberSequence[-1]

在这种情况下,返回常量负索引查找和切片的递归函数调用之和。只有当它不是递归的结束时才会这样做,所以n-1次。

  (n - 1) * constant negative index lookup
+ (n - 1) * slice
+ (n - 1) * recursive call
+ (n - 1) * return

但是等等!如果你四处寻找有关如何制作列表副本的人,你会发现一个常见的Python成语是copy = orig[:]。原因是 slice 操作会复制它正在切片的列表的子范围。所以,当你说numberSequence[:-1]你真正说的是copy = [orig[i] for i in range(0, len(orig)-1)]时。

这意味着slice操作是 O(n),,但在正面,它是用C语言编写的。所以常量是一个小得多的常量。

让我们补充一下:

  1 * call
+ n * assert 
+ n * len
+ n * compare
+ n * compare
+ 1 * constant index 
+ 1 * return
+ (n - 1) * constant negative index lookup
+ (n - 1) * (c * n) slice
+ (n - 1) * recursive call
+ (n - 1) * return

如果我们假设常量索引常量负索引需要同时,我们可以合并它们。我们显然可以合并返回和调用。这让我们留下:

   n * call
 + n * assert 
 + n * len
 + n * compare
 + n * compare
 + n * constant (maybe negative) index 
 + n * return
 + (n - 1) * (c * n) slice

现在根据“规则”,这是 O(n²)。这意味着 O(n)行为的所有细节都倾向于支持那个大的,胖的 O(n²)。

但是:

如果len操作不是 O(1) - 即常数时间 - 则该函数可能会变为 O(n²),因为这一点。

如果index操作不是 O(1),由于底层实现细节,该函数可能变为 O(n²)因此而O(n log n)

所以你已经使用一个本身就是 O(n)本身的Python运算符实现了一个可以 O(n)的算法。您的实现是“固有的” O(n²)。但它可以修复。即使是固定的,你控制之外的东西也可能使你的代码变慢。 (但是,这超出了你的控制范围,所以......忽略它!)

我们如何修复你的代码 O(n)?通过摆脱切片!你不需要那个,对吧?您只需要跟踪范围。

def sum(numberSequence, start=0, end=None):
    assert (len(numberSequence) > 0)
    if end is None:
        end = len(numberSequence) - 1
    if end == start:
        return numberSequence[start]
    else:
        return sum(numberSequence, start, end-1) + numberSequence[end]

在这段代码中,我做的几乎和你做的一样,有两个 差异。首先,我添加了一个特殊情况来处理由最终用户调用,只有序列作为参数。第二,当然,没有切片。除此之外,代码不再具有 O(n²)。

您可以对其他示例执行相同的数学运算并进行相同的更改,但它更复杂。但是,我会提醒你,对于i = 0..n-1,2 i 的总和是2 n - 1.正如@lollercoaster指出的那样,有一个'没有免费的午餐:你必须加上所有数字。

答案 2 :(得分:0)

从技术上讲,我认为算法的实际运行时间可能都比O(n)差。切片操作是O(length_of_slice),因为它复制了列表的相关部分。也就是说,由于这种情况发生在引擎盖下,你可能不会注意到它的表现。

我是否在自己算法的运行时中计算了这个事实,因为如果你实现了这个,例如在C中使用指针算术而不是带切片的Python,这些都是O(n)

两个旁注:

  • sum函数中,您将错误的序列切片(应为return sum(numberSequence[:-1]) + numberSequence[-1])。
  • 在实践中,您应该只使用sum内置而不是像这样自己滚动。