我已经看了之前的帖子,我仍然在努力寻找这两个递归算法的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)
,因为它是二分搜索,但是整个列表没有被分成两半,只是大部分列表。
任何帮助都将不胜感激。
答案 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]
第一行是assert
加compare
加len
。 len
操作是列表和元组的常量时间(但它可能不适用于其他一些数据结构!注意!),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
内置而不是像这样自己滚动。