运行时间实际上是 O(n*2n)。简单的解释是,这是一种渐近最优算法,因为它所做的总工作主要是通过创建直接在算法的最终输出中具有特征的子集来支配,生成的输出的总长度为 O(n*2n)。我们还可以分析伪代码的注释实现(在 JavaScript 中),以更严格地显示这种复杂性:
function powerSet(S) {
if (S.length == 0) return [[]] // O(1)
let e = S.pop() // O(1)
let pSetWithoutE = powerSet(S); // T(n-1)
let pSet = pSetWithoutE // O(1)
pSet.push(...pSetWithoutE.map(set => set.concat(e))) // O(2*|T(n-1)| + ||T(n-1)||)
return pSet; // O(1)
}
// print example:
console.log('{');
for (let subset of powerSet([1,2,3])) console.log(`\t{`, subset.join(', '), `}`);
console.log('}')
其中T(n-1) 表示递归调用 n-1 个元素的运行时间,|T(n-1)| 表示递归调用返回的幂集中子集的数量,||T(n-1)|| 表示总数量递归调用返回的所有子集的元素。
在这些术语中表示的具有复杂性的行对应于伪代码步骤2. 的第二个要点:返回没有元素e 的幂集的联合,以及与每个子集s 联合的相同幂集e:
(1) U ((2) = {s in (1) U e})
这个联合是通过 push 和 concat 操作来实现的。 push 在|T(n-1)| 时间将(1) 与(2) 合并,因为|T(n-1)| 新子集被合并到幂集中。 concat 操作的映射负责通过在|T(n-1)| + ||T(n-1)|| 时间将e 附加到pSetWithoutE 的每个元素来生成(2)。这第二种复杂性对应于|T(n-1)| 子集pSetWithoutE(根据定义)中存在||T(n-1)|| 元素,并且每个子集的大小都增加1。
然后我们可以将输入大小n 的运行时间表示为:
T(n) = T(n-1) + 2|T(n-1)| + ||T(n-1)|| + 1; T(0) = 1
可以通过归纳证明:
|T(n)| = 2n
||T(n)|| = n2n-1
产生:
T(n) = T(n-1) + 2*2<sup>n-1</sup> + (n-1)2<sup>n-2</sup> + 1; T(0) = 1
当你解析地解决这个递归关系时,你会得到:
T(n) = n + 2<sup>n</sup> + n/2*2<sup>n</sup> = O(n2<sup>n</sup>)
这与最佳幂集生成算法的预期复杂度相匹配。递推关系的解也可以直观理解:
n 次迭代中的每一次 O(1) 都在生成幂集的新子集之外工作,因此最终表达式中的 n 项。
就生成幂集的每个子集所做的工作而言,每个子集在通过 concat 生成后被推送一次。推送了 2n 个子集,产生了 2n 项。每个子集的平均长度为 n/2,组合长度为 n/2*2n,对应于所有 concat 操作的复杂性。因此,总时间为 n + 2n + n/2*2n。