web-dev-qa-db-ja.com

順列関数の時間計算量がO(n!)である理由

次のコードを検討してください。

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!)です。しかし、カウント変数を使用して、アルゴが実際にn!の順に成長することを確認すると、異なる結果が見つかりました。
count ++(forループ内)の2つの長さの文字列の場合は4回実行され、countの3つの長さの文字列の値は15になり、4および5つの長さの文字列の場合は64と325になります。
それはそれがn!より悪くなることを意味します。それでは、なぜこれ(およびパーミュレーションを生成する同様のアルゴ)が実行時間の観点からO(n!)であると言われたのですか?.

10
user5350118

_n!_順列があるため、このアルゴリズムはO(n!)であると言われますが、ここで数えているのは(ある意味で)関数呼び出しです-そして_n!_よりも多くの関数呼び出しがあります:

  • str.length() == nの場合、n呼び出しを行います。
  • str.length() == n - 1を使用したこれらのn呼び出しのそれぞれについて、_n - 1_呼び出しを行います。
  • n * (n - 1)を使用したこれらのstr.length() == n - 2呼び出しのそれぞれについて、_n - 2_呼び出しを行います。
  • .。

長さstrの入力kを使用して_n!/k!_呼び出しを行います1、および長さがnから_0_になるため、呼び出しの総数は次のようになります。

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

しかし、ご存知かもしれませんが:

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

したがって、基本的に、この合計は常に定数eよりも小さい(そして_1_よりも大きい)ので、呼び出しの数はO(e.n!)であると言うことができます。これはO(n!)

多くの場合、実行時の複雑さは理論上の複雑さとは異なります。理論的に複雑な場合、アルゴリズムはおそらくこれらの順列のそれぞれをチェックするため(したがって、事実上_n!_チェックが実行されるため)、人々は順列の数を知りたいと思っていますが、実際にはもっと多くのことが起こっています。

1 この式は、最初の関数呼び出しを考慮してから取得した値と比較して、実際に1つを提供します。

17
Holt

この答えは、e = 1/1!+ 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 <= 2 * leaf

今順列ツリーのために

_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
_

ここでは、合計<3 *リーフ、これは予想どおりであることがわかります(e = 2.718)

1
watashiSHUN