我对标准排列查找算法的运行时复杂性有疑问。考虑列表A,找到(并打印)其元素的所有排列。
这是我的递归实现,其中printperm()打印每个排列:
def printperm(A, p):
if len(A) == len(p):
print("".join(p))
return
for i in range(0, len(A)):
if A[i] != 0:
tmp = A[i] # remember ith position
A[i] = 0 # mark character i as used
p.append(tmp) # select character i for this permutation
printperm(A, p) # Solve subproblem, which is smaller because we marked a character in this subproblem as smaller
p.pop() # done with selecting character i for this permutation
A[i] = tmp # restore character i in preparation for selecting the next available character
printperm(['a', 'b', 'c', 'd'], [])
运行时复杂度似乎是O(n!),其中n是A的大小。这是因为在每个递归级别,工作量都会减少1。因此,最高递归级别是n个工作量,下一个级别是n-1,下一个级别是n-2,依此类推。因此总复杂度为n *(n-1)*(n-2)... = n!
现在的问题是print("".join(p))
语句。每次运行此行时,都会遍历列表,并遍历整个列表,即复杂度n。有n!大小为n的列表的排列数。因此,这意味着print("".join(p))
语句完成的工作量为n!* n。
是否存在print("".join(p))
语句,然后将运行时复杂度增加到O(n * n!)?但这似乎不对,因为我没有在每个递归调用上运行print语句。我获取O(n * n!)的逻辑在哪里分解?
答案 0 :(得分:1)
您基本上是对的!可能的混淆出现在您的“ ...下一个级别是n-2
,依此类推”中。 “依此类推”掩盖了递归的最底层,您不是在做O(1)
工作,而是在O(n)
做印刷工作。所以总复杂度与
n * (n-1) * (n-2) ... * 2 * n
等于n! * n
。请注意,.join()
对此并不重要。 O(n)
的工作也仅需print(p)
。
编辑:但这不是真的,因为另一个原因。在print
级别以上的所有级别,您都在做
for i in range(0, len(A)):
和len(A)
不变。因此,每个级都在执行O(n)
工作。可以肯定的是,该级别越深,A
中的零就越多,因此循环的工作量就越少,但是O(n)
仍然只迭代range(n)
。
答案 1 :(得分:0)
可以使用分治法从组合中生成置换,以实现更好的时间复杂度。但是,生成的输出不正确。
假设我们要生成n = 8 {0,1,2,3,4,5,6,7}的排列。 以下是排列实例 01234567 01234576 03215654 30126754 请注意,每个排列中的前4个项目具有相同的集合{0,1,2,3},而后4个项目具有相同的集合{4,5,6,7} 这意味着从A给定的集合A = {0,1,2,3} B = {4,5,6,7}的排列是4!从B开始就是4!
因此,从A获得一个排列实例,从B获得一个排列实例,我们从通用集{0,1,2,3,4,5,6,7}中得到一个正确的排列实例。
给定集合A和B的总置换,使得A和B等于通用集合且B交集为零集合给出通用集合为2 * A!* B! (通过交换A和B顺序 在这种情况下,我们得到2 * 4!* 4! = 1156。
因此,要在n = 8时生成所有排列,我们只需要生成满足前面说明的条件的集合A和B的可能组合即可。 生成按字典顺序一次取4的8个元素的组合非常快。我们需要生成分别分配给相应集合A的前半部分组合,并使用集合操作找到集合B。 对于每对A和B,我们应用分而治之算法。
A和B的大小不必相等,并且可以将递归应用于较高的n值。
代替使用8! = 40320步 我们可以使用8!/(2 * 4! 4!)(4!+4!+1)= 1750步。在合并集合A和集合B排列时,我忽略了叉积运算,因为它是直接运算。
在实际实现中,此方法的运行速度比某些幼稚算法快约20倍,并且可以利用并行性。例如,可以将集合A和B的不同对进行分组,并在不同线程上进行处理。