Big O、それをどのように計算/概算しますか?


881

CSの学位を持つほとんどの人は、Big Oが何を表すのかを確かに知っています。これは、アルゴリズムがどれだけ適切にスケーリングされるかを測定するのに役立ちます。

しかし、私は興味があります。アルゴリズムの複雑さをどのよう計算または概算しますか?


4
アルゴリズムの複雑さを実際に改善する必要はないかもしれませんが、少なくともそれを計算して決定できるはずです...
Xavier Nodet

5
これはBig O、Big Omega、Big Thetaの非常に明確な説明だとわかりました。 xoax.net/comp/sci/algorithms/Lesson6.php
Sam Dutton

33
-1:ため息、BigOhの別の乱用。BigOhは単なる漸近的な上限であり、あらゆるものに使用でき、CSだけに関連しているわけではありません。BigOhについて、ユニークなものがあるかのように話しても意味がありません(線形時間アルゴリズムもO(n ^ 2)、O(n ^ 3)などです)。それが 効率の測定に役立つと言っても誤解を招きます。また、複雑性クラスへのリンクとは何ですか?興味があるのであれば、アルゴリズムの実行時間を計算する手法はありますか?

34
Big-Oは効率を測定しません。これは、アルゴリズムがサイズにどれだけうまく対応しているかを測定します(サイズ以外にも適用できますが、ここではこれに関心があると思われます)。そして、それは漸近的にのみです。 Oが非常に大きい数に達するまで、(Big-Oがサイクルに適用される場合)別のOよりも遅くなることがあります。
ILoveFortran

4
Big-Oの複雑さに基づいてアルゴリズムを選択することは、通常、プログラム設計の重要な部分です。これは、「時期尚早の最適化」のケースではないことは間違いありません。いずれにせよ、これはよく使われる選択的な引用です。
ローン侯爵

回答:


1481

ここでは簡単な言葉で説明するように最善を尽くしますが、このトピックでは最終的に理解するまでに数か月かかることに注意してください。詳細については、Javaのデータ構造とアルゴリズムの第2章を参照してください。


BigOhを取得するために使用できる機械的な手順はありません。

「クックブック」として、コードの一部からBigOhを取得するには、まず、あるサイズの入力を指定して実行される計算のステップ数を数える数式を作成していることを認識する必要があります。

目的は単純です。コードを実行する必要なく、理論的な観点からアルゴリズムを比較することです。ステップ数が少ないほど、アルゴリズムは高速になります。

たとえば、次のコードがあるとします。

int sum(int* data, int N) {
    int result = 0;               // 1

    for (int i = 0; i < N; i++) { // 2
        result += data[i];        // 3
    }

    return result;                // 4
}

この関数は、配列のすべての要素の合計を返します。この関数の計算の複雑さを数える式を作成します。

Number_Of_Steps = f(N)

したがってf(N)、計算ステップの数をカウントする関数があります。関数の入力は、処理する構造のサイズです。これは、この関数が次のように呼び出されることを意味します。

Number_Of_Steps = f(data.length)

パラメータNdata.length値をます。次に、関数の実際の定義が必要f()です。これはソースコードから行われ、各興味深い行には1から4までの番号が付けられています。

BigOhを計算するには多くの方法があります。この時点から、入力データのサイズに依存しないすべての文は定数を取ると仮定しますC数の計算ステップを実行ます。

関数の個々のステップ数を追加します。ローカル変数宣言もreturnステートメントも、 data配列の。

つまり、1行目と4行目はそれぞれCステップずつ実行され、関数は次のようになります。

f(N) = C + ??? + C

次の部分は、forステートメントの値を定義することです。計算ステップの数を数えていることを思い出してください。つまり、forステートメントの本体が実行されNます。これはCN時間を追加するのと同じです。

f(N) = C + (C + C + ... + C) + C = C + N * C + C

体を何回か数える機械的な規則はありません forが実行されません。コードが何を行うかを見て数える必要があります。計算を簡略化するために、forステートメントの変数の初期化、条件、増分の部分は無視しています。

実際のBigOhを取得するには、関数の漸近分析が必要です。これはおおよそ次のように行われます:

  1. すべての定数を取り除く C
  2. から多項式f()取得するstandard form
  3. 多項式の項を分割し、成長率で並べ替えます。
  4. N近づくときに大きくなるものを維持しinfinityます。

私たちにf()は2つの用語があります:

f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1

すべてのC定数と冗長部分を取り除く:

f(N) = 1 + N ^ 1

最後の項はf()無限に近づくと大きくなるものなので(limitsについて考える)、これはBigOh引数であり、sum()関数には次のBigOhがあります。

O(N)

いくつかのトリッキーなものを解決するためのいくつかのトリックがあります:できる限り合計を使用してください。

例として、このコードは合計を使用して簡単に解決できます。

for (i = 0; i < 2*n; i += 2) {  // 1
    for (j=n; j > i; j--) {     // 2
        foo();                  // 3
    }
}

最初に確認する必要があるのは、の実行順序ですfoo()。いつものことですがO(1)、それについては教授に尋ねる必要があります。サイズに関係O(1)なくC、(ほぼ、ほとんど)定数を意味しますN

for文番号1 のステートメントは注意が必要です。インデックスはで終わり2 * Nますが、増分は2ずつ行われます。つまり、最初のステップはステップforのみN実行され、カウントを2で除算する必要があります。

f(N) = Summation(i from 1 to 2 * N / 2)( ... ) = 
     = Summation(i from 1 to N)( ... )

文章番号2は、それがの値に依存するためにもトリッキーですi。見てみましょう:インデックスiは次の値を取ります:0、2、4、6、8、...、2 * N、2番目のfor実行:最初のN倍、2番目、2番目、N-4 3番目... N / 2ステージまで、2番目のステージforは実行されません。

公式では、これは次のことを意味します。

f(N) = Summation(i from 1 to N)( Summation(j = ???)(  ) )

ここでも、ステップ数をカウントしています。そして、定義により、すべての合計は常に1で始まり、1以上の数で終わる必要があります。

f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )

(私たちはそれfoo()がそうであるO(1)と仮定していますCステップを踏みます。)

ここに問題があります:iが値をN / 2 + 1上向きに取ると、内部合計は負の数で終わります!それは不可能で間違っています。合計を2つに分割する必要がありiますN / 2 + 1。これは、瞬間に要となる重要なポイントです。

f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )

回転モーメントなので、i > N / 2内側forは実行されず、その本体は一定のC実行の複雑さを想定しています。

これで、いくつかのアイデンティティルールを使用して合計を簡略化できます。

  1. 合計(wは1からN)(C)= N * C
  2. Summation(w from 1 to N)(A(+/-)B)= Summation(w from 1 to N)(A)(+/-)Summation(w from 1 to N)(B)
  3. Summation(w from 1 to N)(w * C)= C * Summation(w from 1 to N)(w)(Cは定数であり、 w
  4. 合計(wは1からN)(w)=(N *(N + 1))/ 2

代数を適用する:

f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )

f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )

=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )

f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )

=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 = 

   (N / 2 - 1) * (N / 2) / 2 = 

   ((N ^ 2 / 4) - (N / 2)) / 2 = 

   (N ^ 2 / 8) - (N / 4)

f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )

f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )

f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)

f(N) = C * ( N ^ 2 / 4 ) + C * N

f(N) = C * 1/4 * N ^ 2 + C * N

そしてBigOhは:

O(N²)

6
@arthurこれはO(N ^ 2)になります。すべての列を読み取るには1つのループが必要で、特定の列のすべての行を読み取るには1つのループが必要だからです。
Abhishek Dey Das

@arthur:場合によります。それはですO(n)ここでn要素の数、又はあるO(x*y)場合xy配列の次元であるが。Big-ohは「入力に対して相対的」であるため、入力内容によって異なります。
Mooing Duck

1
すばらしい答えですが、私は本当に行き詰っています。Summation(i to 1 to N / 2)(N)はどのようにして(N ^ 2/2)になりますか?
Parsa 2015年

2
@ParsaAkbari原則として、sum(iは1からa)(b)はa * bです。これは、b + b + ...(a回)+ b = a * bの別の言い方です(整数乗算のいくつかの定義の定義による)。
マリオカルネイロ2015年

それほど関連性はありませんが、混乱を避けるために、この文には小さな間違いがあります:「インデックスiは値をとります:0、2、4、6、8、...、2 * N」。インデックスiは実際には2 * N-2まで上昇し、ループはそこで停止します。
アルバート

201

Big Oは、アルゴリズムの時間の複雑さの上限を示します。これは通常、処理データセット(リスト)と組み合わせて使用​​されますが、他の場所でも使用できます。

Cコードでの使用例をいくつか示します。

n個の要素の配列があるとします

int array[n];

配列の最初の要素にアクセスしたい場合、これはO(1)になります。これは、配列の大きさは関係ないため、最初の項目を取得するのに常に同じ一定の時間がかかるためです。

x = array[0];

リストから番号を検索したい場合:

for(int i = 0; i < n; i++){
    if(array[i] == numToFind){ return i; }
}

これはO(n)になります。これは、最大でリスト全体を調べて番号を見つける必要があるためです。Big-Oはアルゴリズムの上限を記述しているため、最初の試行でループを1回実行しても、Big-Oは依然としてO(n)です(omegaは下限、thetaは下限です)。 。

ネストされたループに到達すると:

for(int i = 0; i < n; i++){
    for(int j = i; j < n; j++){
        array[j] += 2;
    }
}

これはO(n ^ 2)です。これは、外側のループ(O(n))の各パスについて、リスト全体をもう一度調べる必要があるため、nの乗算によりnの2乗が残るためです。

これはほとんど表面的な傷ではありませんが、より複雑なアルゴリズムを分析するようになると、証明を含む複雑な数学が始まります。これが少なくとも基本に慣れていることを願っています。


素晴らしい説明!それで、誰かが彼のアルゴリズムがO(n ^ 2)の複雑さを持っていると言ったら、それは彼がネストされたループを使用することを意味しますか?
Navaneeth KN、

2
実際にはそうではありませんが、nの2乗につながるすべての側面はn ^ 2と見なされます
asyncwait

@NavaneethKN:関数呼び出しは自分自身で実行できるため、ネストされたループが常に表示されるとは限りませんO(1)。例えばC標準APIで、bsearch本質的であるO(log n)strlenあるO(n)、とqsortされるO(n log n)(技術的には何の保証を持っていない、と自分自身をクイックソートは最悪のケースの複雑さを持っているO(n²)が、あなたと仮定するとlibc、著者はバカではないが、その平均的なケースの複雑さがありO(n log n)、それは使用していますO(n²)ケースをヒットする確率を減らすピボット選択戦略)。そして、両方bsearchqsortコンパレータ機能が病的である場合に悪化することができます。
ShadowRanger

95

特定の問題のBig O時間を把握する方法を知っていると便利ですが、いくつかの一般的なケースを知っていると、アルゴリズムの決定に役立つ場合があります。

以下は、http//en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functionsから抜粋した最も一般的なケースの一部です。

O(1)-数値が偶数か奇数かを判別します。一定サイズのルックアップテーブルまたはハッシュテーブルを使用する

O(logn)-バイナリ検索でソートされた配列内のアイテムを見つける

O(n)-ソートされていないリストでアイテムを検索します。2つのn桁の数字を追加する

O(n 2-2つのn桁の数値に単純なアルゴリズムを掛けます。2つのn×n行列を追加する。バブルソートまたは挿入ソート

O(n 3)-単純なアルゴリズムによる2つのn×n行列の乗算

O(c n)-動的プログラミングを使用して巡回セールスマン問題の(正確な)解を見つける。ブルートフォースを使用して2つの論理ステートメントが同等かどうかを判断する

O(n!)-ブルートフォース検索による巡回セールスマン問題の解決

O(n n)-O(n!)の代わりに使用され、漸近的な複雑さのためのより簡単な式を導出します


を使用x&1==1して、奇数をチェックしませんか?
Samy Bencherif

2
@SamyBencherif:これは一般的な確認方法です(実際には、テストx & 1するだけで十分== 1です。確認する必要はありません。Cでは、演算子の優先順位のおかげx&1==1で評価されるため、実際にはテストと同じです)。私はあなたが答えを誤解していると思います。カンマではなくセミコロンがあります。偶数/奇数テストにルックアップテーブルが必要だと言っているのではなく、偶数/奇数テストルックアップテーブルのチェックの両方が操作であると言っています。x&(1==1) x&1O(1)
ShadowRanger

最後の文の使用法に関する主張については知りませんが、それを行う人は誰でも、同等ではない別のクラスにクラスを置き換えています。クラスO(n!)にはO(n ^ n)が含まれていますが、これは厳密に大きいです。実際の同値はO(n!)= O(n ^ ne ^ {-n} sqrt(n))になります。
conditionalMethod

43

小さなリマインダー:big O表記を表すために使用される漸近的(問題の大きさは無限に成長するとき、である)複雑さ、及びそれは定数を隠し。

これは、O(n)のアルゴリズムとO(n 2のアルゴリズムの間で)ので、最速が最初のアルゴリズムであるとは限らないことを意味します(サイズ> nの問題の場合、最初のアルゴリズムは最速)。

隠された定数は実装に大きく依存することに注意してください!

また、場合によっては、ランタイムは入力のサイズ nの決定論的関数ではありません。たとえば、クイックソートを使用したソートを考えてみましょう。n要素の配列をソートするのに必要な時間は定数ではなく、配列の開始構成に依存します。

時間の複雑さはさまざまです。

  • 最悪の場合(通常は非常に意味があるとは限らないが、通常は把握するのが最も簡単)
  • 平均的なケース(通常は理解するのがはるかに難しい...)

  • ...

R. SedgewickとP. Flajoletによる「アルゴリズムの分析入門」が良い紹介です。

あなたが言うようにpremature optimisation is the root of all evil、そして(可能であれば)プロファイリングはコードを最適化するときは常に使用されるべきです。アルゴリズムの複雑さを判断するのにも役立ちます。


3
数学では、O(。)は上限を意味し、theta(。)は上限と下限があることを意味します。定義はCSで実際に異なりますか、それとも単なる一般的な表記の乱用ですか?数学的な定義では、sqrt(n)はO(n)とO(n ^ 2)の両方であるため、O(n)関数が小さくなるnがあるとは限りません。
Douglas Zare、2015

28

私たちは私たちのほとんどは、実際により、アルゴリズムの順序を近似ないと結論づけることができると思い、ここでの回答を見て見て、それであり、例えば、代わりにそれを求めるの常識を使用して、マスターメソッド私たちは大学で考えられていたよう。そうは言っても、教授でさえ私たち(後で)にそれを計算するだけでなく実際にそれについて考えるように勧めたと付け加えなければなりません。

また、私はそれが再帰関数のためにどのように行われるかを追加したいと思います

(のような関数があるとしますスキームコード)の。

(define (fac n)
    (if (= n 0)
        1
            (* n (fac (- n 1)))))

与えられた数の階乗を再帰的に計算します。

最初のステップは、関数の本体のパフォーマンス特性を試行して決定することです。この場合のみ、本体では特別な処理は行われず、乗算(または値1の戻り)のみが行われます。

だから ボディパフォーマンスはO(1)(定数)です。

次に、これを試して決定します 、再帰呼び出しの数について。この場合、n-1回の再帰呼び出しがあります。

だから 再帰呼び出しのパフォーマンスは次のとおりです。O(n-1)(重要でない部分を破棄するため、順序はnです)。

次に、これら2つを組み合わせると、再帰関数全体のパフォーマンスが得られます。

1 *(n-1)= O(n)


ピーターあなたの提起された問題に答えるために; ここで説明する方法は、実際にこれを非常にうまく処理します。ただし、これはまだ概算であり、完全に数学的に正しい答えではないことに注意してください。ここで説明する方法は、私たちが大学で教えた方法の1つでもあり、私が正しく覚えていれば、この例で使用した階乗よりもはるかに高度なアルゴリズムに使用されました。
もちろん、それはすべて、関数本体の実行時間と再帰呼び出しの数をどれだけ適切に推定できるかに依存しますが、他のメソッドについても同様です。


スヴェン、再帰関数の複雑さを判断するあなたの方法が、バイナリツリーで上から下への検索/合計/何かを行うなど、より複雑な関数で機能するかどうかはわかりません。確かに、簡単な例を考えて答えを出すことができます。しかし、私はあなたが実際に再帰的なもののためにいくつかの数学をしなければならないと思いますか?
ピーター2008

3
再帰のための+1 ...これも美しい:「...教授でさえ私たちに考えるように勧めた...」:)
TT_

はい、これはとても良いです。私はこのように考える傾向があり、O(..)内の項が高いほど、あなたがしている/マシンがしている仕事が多くなります。何かに関連してそれを考えることは近似かもしれませんが、これらの境界もそうです。入力数が増えると、実行する作業がどのように増えるかを説明するだけです。
Abhinav Gauniyal 2015

26

コストが多項式の場合は、乗数を除いて最高次数を維持します。例えば:

O((N / 2 + 1)×(N / 2))= O(N 2 /4 + N / 2)= O(N 2 /4)= O(N 2

これは無限シリーズでは機能しません。一般的なケースには単一のレシピはありませんが、いくつかの一般的なケースでは、次の不等式が適用されます。

O(log N)<O(N)<O(N log N)<O(N 2)<O(N k)<O(e n)<O(n!)


8
確かにO(N)<O(NlogN)
jk。

22

情報面で考えています。問題は、特定のビット数を学習することで構成されます。

基本的なツールは、ディシジョンポイントとそのエントロピーの概念です。ディシジョンポイントのエントロピーは、それが提供する平均的な情報です。たとえば、プログラムに2つの分岐のあるディシジョンポイントが含まれている場合、そのエントロピーは各分岐の確率とlog 2の合計です。確率とその分岐の逆確率のの。それはあなたがその決定を実行することによってどれだけ学ぶかです。

たとえば、 if 2つの分岐を持つステートメントは、どちらも同じように、エントロピーが1/2 * log(2/1)+ 1/2 * log(2/1)= 1/2 * 1 + 1/2 * 1になります。 = 1.したがって、そのエントロピーは1ビットです。

N = 1024のようなNアイテムのテーブルを検索するとします。log(1024)= 10ビットであるため、これは10ビットの問題です。したがって、同様の結果をもたらす可能性のあるIFステートメントで検索できる場合、10の決定を行う必要があります。

それがバイナリサーチで得られるものです。

線形検索を行っているとします。あなたは最初の要素を見て、それがあなたが欲しいものかどうか尋ねます。確率はそれが1/1024であり、そうではない1023/1024です。その決定のエントロピーは1/1024 * log(1024/1)+ 1023/1024 * log(1024/1023)= 1/1024 * 10 + 1023/1024 *約0 =約.01ビットです。ほんの少ししか学んでいない!2番目の決定はそれほど良くありません。これが、線形検索が非常に遅い理由です。実際、学習する必要のあるビット数は指数関数的です。

索引付けを行っているとします。テーブルが多くのビンに事前にソートされており、キーのすべてのビットの一部を使用して、テーブルエントリに直接インデックス付けするとします。1024のビンがある場合、エントロピーはすべての可能な1024の結果に対して1/1024 * log(1024)+ 1/1024 * log(1024)+ ...です。これは1/1024 * 1024の10倍の結果、またはその1つのインデックス付け操作の10ビットのエントロピーです。そのため、インデックス検索は高速です。

次に、ソートについて考えます。N個のアイテムがあり、リストがあります。各アイテムについて、アイテムがリストのどこにあるかを検索し、リストに追加する必要があります。したがって、並べ替えには、基になる検索のステップ数の約N倍がかかります。

したがって、ほぼ同等の結果が得られるバイナリ決定に基づく並べ替えでは、すべてO(N log N)ステップがかかります。索引付け検索に基づいている場合は、O(N)ソートアルゴリズムが可能です。

アルゴリズムのパフォーマンスに関するほぼすべての問題をこの方法で確認できることがわかりました。


ワオ。これについて役立つ参考資料はありますか?これはプログラムの設計/リファクタリング/デバッグに役立つと思います。
Jesvin Jose

3
@aitchnyu:それに値するもののために、私それと他のトピックをカバーする本書きました。絶版から久しぶりですが、お手ごろ価格でコピーできます。私はGoogleBooksにそれを取得させようとしましたが、現時点では誰が著作権を持っているのかを理解するのが少し難しいです。
Mike Dunlavey、2011

21

最初から始めましょう。

まず、データに対する特定の単純な操作をO(1)時間内に、つまり入力のサイズに依存しない時間内に実行できるという原則を受け入れます。Cのこれらの基本操作は、

  1. 算術演算(例:+または%)。
  2. 論理演算(例:&&)。
  3. 比較演算(例:<=)。
  4. 構造体にアクセスする操作(例:A [i]のような配列のインデックス付け、または->演算子を使用したポインター)。
  5. 値を変数にコピーするなどの単純な割り当て。
  6. ライブラリ関数(scanf、printfなど)の呼び出し。

この原則を正当化するには、一般的なコンピューターの機械語命令(基本ステップ)の詳細な調査が必要です。説明されている各操作は、少数の機械語命令で実行できます。多くの場合、1つまたは2つの指示のみが必要です。その結果、Cのいくつかの種類のステートメントをO(1)時間内に実行できます。つまり、入力に関係なく一定の時間内に実行できます。これらの単純な

  1. 式に関数呼び出しを含まない割り当てステートメント。
  2. ステートメントを読み取ります。
  3. 引数を評価するために関数呼び出しを必要としないステートメントを記述します。
  4. ジャンプ文は、式が関数呼び出しを含まない場合に、式を中断、続行、移動、および返します。

Cでは、多くのforループは、インデックス変数をある値に初期化し、ループの周りでその変数を1ずつインクリメントすることによって形成されます。forループは、インデックスが何らかの制限に達すると終了します。たとえば、forループ

for (i = 0; i < n-1; i++) 
{
    small = i;
    for (j = i+1; j < n; j++)
        if (A[j] < A[small])
            small = j;
    temp = A[small];
    A[small] = A[i];
    A[i] = temp;
}

インデックス変数iを使用します。ループを回るたびにiが1ずつ増加し、iがn − 1に達すると反復が停止します。

ただし、当面は、forループの単純な形式に注目してください。最終値と初期値の差を、インデックス変数が増分される量で割ると、ループを何回繰り返すかがわかります。。ジャンプ文を介してループを終了する方法がない限り、その数は正確です。いずれの場合も、それは反復数の上限です。

たとえば、forループは反復します((n − 1) − 0)/1 = n − 1 times。0はiの初期値であるため、n − 1はiが到達する最高値です(つまり、iがn−1に到達すると、ループは停止し、i = n−の反復は発生しません。 1)、ループの各反復で1がiに追加されます。

ループ本体で費やされた時間が各反復で同じである最も単純なケースでは、本体のbig-oh上限にループ周囲の回数を掛けることができます。厳密に言うと、ループインデックスを初期化するためのO(1)時間と、ループインデックスとlimitとの最初の比較のためのO(1)時間を追加する必要があります。ただし、ループをゼロ回実行できない場合を除いて、ループを初期化して制限を1回テストする時間は、合計ルールによって削除できる低次の項です。


今、この例を考えてみましょう:

(1) for (j = 0; j < n; j++)
(2)   A[i][j] = 0;

行(1)にO(1)時間がかかることがわかります。明らかに、ループをn回処理します。これは、ライン(1)で見つかった上限から下限を差し引いて1を加えることで決定できるためです。本体であるライン(2)はO(1)時間かかるため、 jをインクリメントする時間とjをnと比較する時間を無視できます。どちらもO(1)です。したがって、行(1)および(2)の実行時間は、nとO(1)の積であり、ですO(n)

同様に、行(2)から(4)で構成される外部ループの実行時間を制限できます。

(2) for (i = 0; i < n; i++)
(3)     for (j = 0; j < n; j++)
(4)         A[i][j] = 0;

行(3)と(4)のループにO(n)時間かかることはすでに確認済みです。したがって、O(1)時間を無視してiをインクリメントし、各反復でi <nかどうかをテストして、外側のループの各反復にO(n)時間かかると結論付けることができます。

外部ループの初期化i = 0と条件i <nの(n + 1)番目のテストも同様にO(1)時間かかり、無視できます。最後に、外側のループをn回処理し、反復ごとにO(n)時間を取り、合計O(n^2)実行時間を与えることを確認し ます。


より実用的な例。

ここに画像の説明を入力してください


gotoステートメントに関数呼び出しが含まれている場合はどうなりますか?step3のようなもの:if(M.step == 3){M = step3(done、M); } step4:if(M.step == 4){M = step4(M); } if(M.step == 5){M = step5(M); ステップ3に進みます。} if(M.step == 6){M = step6(M); ステップ4に移動します。} return cut_matrix(A、M); では、複雑度はどのように計算されますか?それは加算または乗算でしょうか?step4がn ^ 3で、step5がn ^ 2であることを考慮してください。
タハタリク2018年

14

コードを分析するのではなく、経験的にコードの順序を推定したい場合は、nの値を増やして、コードの時間を増やすことができます。タイミングを対数スケールでプロットします。コードがO(x ^ n)の場合、値は勾配nの直線上にあるはずです。

これには、コードを研究するだけの利点がいくつかあります。1つには、ランタイムが漸近順序に近づく範囲内にいるかどうかを確認できます。また、たとえば、ライブラリの呼び出しに時間を費やしているため、O(x)の次数がO(x ^ 2)の次数であると思ったコードが見つかることがあります。


この答えを更新するためだけに:en.wikipedia.org/wiki/Analysis_of_algorithms、このリンクには必要な式があります。多くのアルゴリズムはパワールールに従いますが、マシンで2つのタイムポイントと2つのランタイムを使用している場合は、ログ-ログプロットで勾配を計算できます。これは、a = log(t2 / t1)/ log(n2 / n1)です。これにより、O(N ^ a)のアルゴリズムの指数が得られました。これは、コードを使用した手動計算と比較できます。
クリストファージョン

1
こんにちは、いい答えです。この経験的方法を一般化するためのライブラリまたは方法論(たとえばpython / Rで作業します)を知っているかどうか疑問に思っていました。ありがとう
agenis

10

基本的に、時間の90%を占めるのはループの分析だけです。シングル、ダブル、トリプルネストループがありますか?O(n)、O(n ^ 2)、O(n ^ 3)の実行時間があります。

ごくまれに(たとえば、.NET BCLやC ++のSTLなどの広範なベースライブラリを備えたプラットフォームを作成している場合を除いて)、ループを見ているだけでは難しいことに遭遇します(ステートメント、while、goto、等...)


1
ループに依存します。
ケララカ2018

8

Big O表記は扱いが簡単であり、不要な複雑化や詳細(不要なものの定義について)を隠すので便利です。分割統治アルゴリズムの複雑さを解決する1つの優れた方法は、ツリー法です。中央値の手順を持つクイックソートのバージョンがあるとしましょう。そのため、毎回、配列を完全にバランスのとれたサブ配列に分割します。

次に、使用するすべての配列に対応するツリーを構築します。ルートには元の配列があり、ルートにはサブ配列である2つの子があります。下部に単一の要素配列ができるまでこれを繰り返します。

O(n)時間で中央値を見つけ、配列をO(n)時間で2つの部分に分割できるため、各ノードで実行される作業はO(k)で、kは配列のサイズです。ツリーの各レベルには(最大で)配列全体が含まれるため、レベルごとの作業はO(n)になります(サブ配列のサイズは合計でnになり、レベルごとにO(k)があるため、これを合計できます)。 。毎回入力を半分にするので、ツリーにはlog(n)レベルしかありません。

したがって、O(n * log(n))によって作業量の上限を設定できます。

ただし、Big Oは一部の詳細を非表示にしますが、無視できないことがあります。でフィボナッチ数列を計算することを検討してください

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

そして、aとbがJavaのBigIntegerか、任意の数を処理できる何かであると仮定します。ほとんどの人は、これは簡単なO(n)アルゴリズムであると言います。その理由は、forループにn回の反復があり、O(1)がループの側で機能するためです。

しかし、フィボナッチ数は大きく、n番目のフィボナッチ数はnの指数関数であるため、格納するだけでnバイトのオーダーになります。大きな整数で加算を実行すると、O(n)の作業量がかかります。したがって、この手順で行われる作業の総量は

1 + 2 + 3 + ... + n = n(n-1)/ 2 = O(n ^ 2)

したがって、このアルゴリズムは4倍の時間で実行されます。


1
数値の格納方法を気にする必要はありません。アルゴリズムがO(n)の上限で大きくなることは変わりません。
mikek3332002 10/07/23



7

使用しているアルゴリズム/データ構造の知識、および/または反復ネストのクイック分析。問題は、ライブラリ関数を(場合によっては複数回)呼び出すときです。多くの場合、関数を不必要に呼び出すのか、それともどの実装を使用しているのかわからない場合があります。たぶん、ライブラリ関数には、Big Oであろうと他のメトリックであろうと、ドキュメントやIntelliSenseでさえも利用できる複雑さ/効率性の測定が必要です。


6

Big Oの「計算方法」については、これは計算複雑度理論の一部です。一部の(多くの)特殊なケースでは、いくつかの単純なヒューリスティック(ネストされたループのループカウントの乗算など)を使用できる場合があります。あなたが望むのは上限の見積もりだけであり、それがあまりにも悲観的であるかどうか気にしないとき-私はおそらくあなたの質問が何であるかと思います。

アルゴリズムの質問に本当に答えたい場合は、理論を適用するのが最善です。単純化された「最悪のケース」の分析に加えて、私は償却分析が実際に非常に役立つことを発見しました。


6

1番目のケースでは、内部ループが実行されるn-i回数なので、実行の合計数は、から(以下、以下ではないため)にi行くための合計です。やっと得たので、0n-1n-in*(n + 1) / 2O(n²/2) = O(n²)

2番目のループの場合、外側のループのi間に0ありn、外側のループに含まれます。次に、jがより大きい場合に内部ループが実行されますがn、これは不可能です。


5

マスターメソッド(またはその専門分野の1つ)を使用することに加えて、アルゴリズムを実験的にテストします。これは、特定の複雑性クラスが達成されたこと証明することはできませんが、数学的分析が適切であることを再確認します。この安心を助けるために、私は実験と併せてコードカバレッジツールを使用して、すべてのケースを確実に実行できるようにします。

非常に単純な例として、.NET Frameworkのリストソートの速度をチェックする必要があるとします。次のように記述し、Excelで結果を分析して、n * log(n)曲線を超えていないことを確認できます。

この例では、比較の数を測定しますが、各サンプルサイズに必要な実際の時間を調べることも賢明です。ただし、アルゴリズムを測定するだけであり、テストインフラストラクチャからのアーティファクトを含めないように、さらに注意する必要があります。

int nCmp = 0;
System.Random rnd = new System.Random();

// measure the time required to sort a list of n integers
void DoTest(int n)
{
   List<int> lst = new List<int>(n);
   for( int i=0; i<n; i++ )
      lst[i] = rnd.Next(0,1000);

   // as we sort, keep track of the number of comparisons performed!
   nCmp = 0;
   lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }

   System.Console.Writeline( "{0},{1}", n, nCmp );
}


// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
   DoTest(n);

4

メモリリソースが限られている場合に懸念の原因となるスペースの複雑さも考慮に入れることを忘れないでください。したがって、たとえば、基本的にアルゴリズムが使用するスペースの量はコード内の要因に依存しないという言い方である、一定のスペースアルゴリズムを望む誰かが聞こえるかもしれません。

複雑さは、呼び出された回数、ループが実行された頻度、メモリが割り当てられた頻度などに起因する場合があります。

最後に、ビッグOは、最悪の場合、最良の場合、および償却の場合に使用できます。一般に、アルゴリズムがどれほど悪いかを説明するために使用されるのは最悪の場合です。


4

見落とされがちなのは、アルゴリズムの予想される動作です。アルゴリズムのBig-Oは変更されません「時期尚早の最適化...」というステートメントに関連しています。

アルゴリズムの予想される動作は-非常に馬鹿げています-アルゴリズムが表示する可能性が最も高いデータを処理することを期待できる速度です。

たとえば、リスト内の値を検索する場合、それはO(n)ですが、表示されるほとんどのリストに前もって値があることがわかっている場合、アルゴリズムの一般的な動作はより高速です。

実際にそれを明確にするには、「入力スペース」の確率分布を記述できる必要があります(リストをソートする必要がある場合、そのリストはすでにソートされる頻度ですか?どのくらいの頻度で完全に逆転するか?方法多くの場合、ほとんどがソートされていますか?)それを知っていることは常に可能であるとは限りませんが、時にはそうすることもあります。


4

すばらしい質問です!

免責事項:この回答には誤った説明が含まれています。下記のコメントを参照してください。

Big Oを使用している場合は、最悪のケースについて話していることになります(これについては後で詳しく説明します)。さらに、平均的なケースには資本シータがあり、最良のケースには大きなオメガがあります。

Big Oの正式な定義については、このサイトをチェックしてください:https : //xlinux.nist.gov/dads/HTML/bigOnotation.html

f(n)= O(g(n))は、すべてのn≥kに対して0≤f(n)≤cg(n)のような正の定数cとkがあることを意味します。cおよびkの値は、関数fに対して固定されている必要があり、nに依存してはなりません。


では、「ベストケース」と「ワーストケース」の複雑さはどういう意味ですか?

これはおそらく例を通して最も明確に説明されます。たとえば、線形検索を使用して並べ替えられた配列内の数値を検索する場合最悪のケースは、配列の最後の要素検索する場合です。これは、配列内の項目と同じ数のステップを実行するためです。最良のケースでは、我々が検索したときになり、最初の要素、我々は最初のチェックの後に行われるからです。

これらすべての形容詞-ケースの複雑さのポイントは、特定の変数のサイズの観点から、仮想的なプログラムが実行されるまでの時間をグラフ化する方法を探しているということです。ただし、多くのアルゴリズムでは、特定のサイズの入力に対して単一の時間は存在しないと主張できます。これは関数の基本的な要件と矛盾することに注意してください。どの入力も1つの出力しか持たないはずです。したがって、複数の、アルゴリズムの複雑さを説明するために関数ます。ここで、サイズnの配列の検索には、配列で何を求めているかによって、またnに比例して、さまざまな時間がかかる場合がありますが、最良の場合、平均の場合を使用して、アルゴリズムの有益な説明を作成できます、最悪の場合のクラス。

申し訳ありませんが、これは非常によく書かれておらず、多くの技術情報が不足しています。しかし、うまくいけば、時間の複雑さのクラスを考えやすくなるでしょう。これらに慣れると、プログラムを解析して、配列サイズに依存するforループや、データ構造に基づく推論など、簡単なケースでどのような入力が発生し、どのような入力が発生するかを調べるという簡単な問題になります。最悪の場合。


1
これは誤りです。ビッグOは、ワーストケースではなく、「上限」を意味します。
Samy Bencherif 2017

1
big-Oが最悪の場合を指すのはよくある誤解です。OとΩは、最悪の場合と最良の場合の関係を教えてください。
Bernhard Barker 2017

1
これは誤解を招くものです。Big-Oは、関数f(n)の上限を意味します。Omegaは、関数f(n)の下限を意味します。最良のケースや最悪のケースとはまったく関係ありません。
タスニームハイダー

1
Big-Oは、最良または最悪の場合の上限として使用できますが、それ以外は関係ありません。
Samy Bencherif

2

プログラムでこれを解決する方法はわかりませんが、最初に行うことは、実行される操作の数の特定のパターンについてアルゴリズムをサンプリングすることです。たとえば、4n ^ 2 + 2n + 1には2つのルールがあります。

  1. 用語の合計がある場合は、成長率が最大の用語が保持され、他の用語は省略されます。
  2. 複数の要素の積がある場合、定数要素は省略されます。

f(x)を簡略化すると、f(x)は実行された操作の数の式(上記で説明した4n ^ 2 + 2n + 1)であり、ビッグO値[O(n ^ 2)が得られます。場合]。しかし、これはプログラムのラグランジュ補間を考慮する必要があり、実装が難しい場合があります。そして、実際のbig-O値がO(2 ^ n)で、O(x ^ n)のようなものがある場合、このアルゴリズムはおそらくプログラムできません。しかし、誰かが私を間違っていると証明した場合は、コードを教えてください。。。。


2

コードAの場合、外側のループはn+1何度も実行されます。「1」の時間は、まだ要件を満たしているかどうかを確認するプロセスを意味します。そして、内側のループはn何度も実行されn-2ます0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)

コードBの場合、内側のループはステップインしてfoo()を実行しませんが、内側のループは、外側のループの実行時間(O(n))に応じて、n回実行されます。


1

Big-Oについて少し異なる側面から説明したいと思います。

Big-Oは単にプログラムの複雑さを比較することです。つまり、入力が増加しているときにプログラムがどれだけ速く成長するかを意味し、アクションを実行するために費やされる正確な時間ではありません。

big-O式では、より複雑な式を使用しない方がよい(次のグラフの式をそのまま使用する方がよい)。ただし、他のより正確な式(3 ^ n、n ^ 3など)を使用することもできます。 。)しかし、それ以上の場合は誤解を招くことがあります。できるだけシンプルに保つことをお勧めします。

ここに画像の説明を入力してください

ここで、アルゴリズムの正確な公式を取得したくないことを再度強調しておきます。入力が増加しているときにそれがどのように成長するかを示し、その意味で他のアルゴリズムと比較したいだけです。それ以外の場合は、ベンチマークなどのさまざまな方法を使用することをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.