【问题标题】:Why Time complexity of permutation function is O(n!)为什么置换函数的时间复杂度是 O(n!)
【发布时间】:2016-08-24 14:01:11
【问题描述】:

考虑以下代码。

public class Permutations {
    static int count=0;
    static void permutations(String str, String prefix){
        if(str.length()==0){
            System.out.println(prefix);
        }
        else{
            for(int i=0;i<str.length();i++){
                count++;
                String rem = str.substring(0,i) + str.substring(i+1);
                permutations(rem, prefix+str.charAt(i));
            }
        }

    }
    public static void main(String[] args) {
        permutations("abc", "");
        System.out.println(count);
    }

}

这里的逻辑,我认为遵循的是-它将字符串的每个字符视为可能的前缀并置换剩余的 n-1 个字符。
所以按这个逻辑递归关系就出来了

T(n) = n( c1 + T(n-1) )          // ignoring the print time

这显然是 O(n!)。但是当我使用计数变量来查看 wather 算法真的按 n 顺序增长时,我发现了不同的结果。
对于 count++ 的 2 长度字符串(在 for 循环内)运行 4 次,对于 3 长度字符串,count 的值为 15,对于 4 和 5 长度字符串,其值为 64 和 325。
这意味着它比 n! 更糟糕。那么为什么它说这个(以及产生排列的类似算法)在运行时间方面是 O(n!)。

【问题讨论】:

  • 为什么?你能解释一下吗?
  • n! 是排列的数量,如果你在if 的第一个块内增加count,你会得到n!,但你实际计算的是排列的数量调用大于n!permutations
  • @Holt 但这些调用也负责增加算法的运行时间。为什么我们不应该考虑这一点。我知道只能有n!排列,如果我们在 if 块中增加计数,它肯定会打印 n!。
  • @Holt 你的意思是虽然它看起来比 n 大!但它肯定是 n! 的常数倍数。这就是为什么我的计数值更高...您能否将您的陈述建立在一些事实/证明的基础上,即这里的计数值肯定是 n 的常数倍!并且不依赖于 n
  • Holt 的回答是正确的,但实际上排列函数需要 O(N * N!) 时间,或 O((N+1)!) 时间,因为它需要 O(N) 时间打印每个字符串。

标签: algorithm time-complexity permutation


【解决方案1】:

人们说这个算法是O(n!),因为有n! 排列,但你在这里指的是(在某种意义上)函数调用——而且函数调用比n! 更多:

  • str.length() == n 时,你调用n
  • 对于每个带有str.length() == n - 1n 调用,您都进行n - 1 调用;
  • 对于每个带有str.length() == n - 2n * (n - 1) 调用,您都需要进行n - 2 调用;
  • ...

您使用长度为 k1 的输入 str 进行 n! / k! 调用,并且由于长度从 n 变为 0,因此调用总数为:

sum k = 0 ... n (n!/k!) = n!总和 k = 0 ... n (1 / k!)

但你可能知道:

sum k = 0 ... +oo 1 / k! = e1 = e

所以基本上,这个总和总是小于常数e(并且大于1),所以你可以说调用次数是O(e.n!),也就是O(n!)

运行时复杂度通常不同于理论复杂度。在理论上的复杂性中,人们想知道排列的数量,因为算法可能会检查这些排列中的每一个(因此实际上已经完成了n! 检查),但实际上还有更多事情要做。

1 这个公式实际上会给你一个与你得到的值相比的一个值,因为你没有考虑到初始函数调用。

【讨论】:

  • 谢谢!真是一个很好的解释。
  • 不应该是n!/(k-1)!当 str.length() == k 因为当字符串长度为 n 时,我们可以产生 n 个调用? n!/(n-1)! = (n * n-1 * n-2) / (n-1 * n-2 * n-3) = n 次调用
  • @BenyamEphrem 当我说“你用n!/k!调用str.length() == k时,我的意思是有n!/k!str.length() == k调用函数,并不是说您在str.length() == k 的调用中进行n!/k! 调用。而且您只会使用长度为n(初始调用)的字符串调用该方法一次。我承认这不是那么清楚,我花了一些时间才找回自己答案的原意......
  • 啊,晶莹剔透,所以在 0 级(第一次调用)k = n 所以 -> n!/(n)! = 1 次通话。在第 1 级,k = n - 1 所以 -> n!/(n-1)! = (n * n-1 * n-2) / (n-1 * n-2 * n-3) = n 个电话,等等。谢谢!
【解决方案2】:

此答案适用于像我这样不记得 e=1/0!+1/1!+1/2!+1/3!...的人。

我可以用一个简单的例子来解释,假设我们想要"abc"的所有排列

        /    /   \     <--- for first position, there are 3 choices
       /\   /\   /\    <--- for second position, there are 2 choices
      /  \ /  \ /  \   <--- for third position, there is only 1 choice

上面是递归树,我们知道有3! 叶节点,代表"abc"的所有可能排列(这也是我们执行操作的地方结果,即print()),但是由于您要计算所有函数调用,因此我们需要知道总共有多少个树节点(叶子+内部)

如果它是一棵完全二叉树,我们知道有2^n叶节点...有多少内部节点?

x = |__________leaf_____________|------------------------|  
let this represent 2^n leaf nodes, |----| represents the max number of
nodes in the level above, since each node has 1 parent, 2nd last level
cannot have more nodes than leaf
since its binary, we know second last level = (1/2)leaf 
x = |__________leaf_____________|____2nd_____|-----------|
same for the third last level...which is (1/2)sec
x = |__________leaf_____________|____2nd_____|__3rd_|----|

x 可用于表示树节点的总数,由于我们总是在初始 |-----| 上减半,我们知道 total

现在是排列树

x = |____leaf____|------------|
let this represent n! leaf nodes
since its second last level has 1 branch, we know second last level = x 
x = |____leaf____|____2nd_____|-------------|
but third last level has 2 branches for each node, thus = (1/2)second
x = |____leaf____|____2nd_____|_3rd_|-------|
fourth last level has 3 branches for each node, thus = (1/3)third
x = |____leaf____|____2nd_____|_3rd_|_4|--| |
| | means we will no longer consider it

这里我们看到total ,这和预期的一样(e = 2.718)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-24
    • 2023-02-10
    • 2020-12-16
    • 2020-03-21
    • 1970-01-01
    • 1970-01-01
    • 2021-01-01
    • 1970-01-01
    相关资源
    最近更新 更多