CSの学位を持つほとんどの人は、Big Oが何を表すのかを確かに知っています。これは、アルゴリズムがどれだけ適切にスケーリングされるかを測定するのに役立ちます。
しかし、私は興味があります。アルゴリズムの複雑さをどのように計算または概算しますか?
CSの学位を持つほとんどの人は、Big Oが何を表すのかを確かに知っています。これは、アルゴリズムがどれだけ適切にスケーリングされるかを測定するのに役立ちます。
しかし、私は興味があります。アルゴリズムの複雑さをどのように計算または概算しますか?
回答:
ここでは簡単な言葉で説明するように最善を尽くしますが、このトピックでは最終的に理解するまでに数か月かかることに注意してください。詳細については、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)
パラメータN
はdata.length
値をます。次に、関数の実際の定義が必要f()
です。これはソースコードから行われ、各興味深い行には1から4までの番号が付けられています。
BigOhを計算するには多くの方法があります。この時点から、入力データのサイズに依存しないすべての文は定数を取ると仮定しますC
数の計算ステップを実行ます。
関数の個々のステップ数を追加します。ローカル変数宣言もreturnステートメントも、 data
配列の。
つまり、1行目と4行目はそれぞれCステップずつ実行され、関数は次のようになります。
f(N) = C + ??? + C
次の部分は、for
ステートメントの値を定義することです。計算ステップの数を数えていることを思い出してください。つまり、for
ステートメントの本体が実行されN
ます。これはC
、N
時間を追加するのと同じです。
f(N) = C + (C + C + ... + C) + C = C + N * C + C
体を何回か数える機械的な規則はありません for
が実行されません。コードが何を行うかを見て数える必要があります。計算を簡略化するために、for
ステートメントの変数の初期化、条件、増分の部分は無視しています。
実際のBigOhを取得するには、関数の漸近分析が必要です。これはおおよそ次のように行われます:
C
。f()
取得するstandard form
。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実行の複雑さを想定しています。
これで、いくつかのアイデンティティルールを使用して合計を簡略化できます。
w
)代数を適用する:
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²)
O(n)
ここでn
要素の数、又はあるO(x*y)
場合x
とy
配列の次元であるが。Big-ohは「入力に対して相対的」であるため、入力内容によって異なります。
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(1)
。例えばC標準APIで、bsearch
本質的であるO(log n)
、strlen
あるO(n)
、とqsort
されるO(n log n)
(技術的には何の保証を持っていない、と自分自身をクイックソートは最悪のケースの複雑さを持っているO(n²)
が、あなたと仮定するとlibc
、著者はバカではないが、その平均的なケースの複雑さがありO(n log n)
、それは使用していますO(n²)
ケースをヒットする確率を減らすピボット選択戦略)。そして、両方bsearch
とqsort
コンパレータ機能が病的である場合に悪化することができます。
特定の問題の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
して、奇数をチェックしませんか?
x & 1
するだけで十分== 1
です。確認する必要はありません。Cでは、演算子の優先順位のおかげx&1==1
で評価されるため、実際にはテストと同じです)。私はあなたが答えを誤解していると思います。カンマではなくセミコロンがあります。偶数/奇数テストにルックアップテーブルが必要だと言っているのではなく、偶数/奇数テストとルックアップテーブルのチェックの両方が操作であると言っています。x&(1==1)
x&1
O(1)
小さなリマインダー:big O
表記を表すために使用される漸近的(問題の大きさは無限に成長するとき、である)複雑さ、及びそれは定数を隠し。
これは、O(n)のアルゴリズムとO(n 2のアルゴリズムの間で)ので、最速が最初のアルゴリズムであるとは限らないことを意味します(サイズ> nの問題の場合、最初のアルゴリズムは最速)。
隠された定数は実装に大きく依存することに注意してください!
また、場合によっては、ランタイムは入力のサイズ nの決定論的関数ではありません。たとえば、クイックソートを使用したソートを考えてみましょう。n要素の配列をソートするのに必要な時間は定数ではなく、配列の開始構成に依存します。
時間の複雑さはさまざまです。
平均的なケース(通常は理解するのがはるかに難しい...)
...
R. SedgewickとP. Flajoletによる「アルゴリズムの分析入門」が良い紹介です。
あなたが言うようにpremature optimisation is the root of all evil
、そして(可能であれば)プロファイリングはコードを最適化するときは常に使用されるべきです。アルゴリズムの複雑さを判断するのにも役立ちます。
私たちは私たちのほとんどは、実際により、アルゴリズムの順序を近似ないと結論づけることができると思い、ここでの回答を見て見て、それであり、例えば、代わりにそれを求めるの常識を使用して、マスターメソッド私たちは大学で考えられていたよう。そうは言っても、教授でさえ私たち(後で)にそれを計算するだけでなく実際にそれについて考えるように勧めたと付け加えなければなりません。
また、私はそれが再帰関数のためにどのように行われるかを追加したいと思います:
(のような関数があるとしますスキームコード)の。
(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つでもあり、私が正しく覚えていれば、この例で使用した階乗よりもはるかに高度なアルゴリズムに使用されました。
もちろん、それはすべて、関数本体の実行時間と再帰呼び出しの数をどれだけ適切に推定できるかに依存しますが、他のメソッドについても同様です。
情報面で考えています。問題は、特定のビット数を学習することで構成されます。
基本的なツールは、ディシジョンポイントとそのエントロピーの概念です。ディシジョンポイントのエントロピーは、それが提供する平均的な情報です。たとえば、プログラムに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)ソートアルゴリズムが可能です。
アルゴリズムのパフォーマンスに関するほぼすべての問題をこの方法で確認できることがわかりました。
最初から始めましょう。
まず、データに対する特定の単純な操作をO(1)
時間内に、つまり入力のサイズに依存しない時間内に実行できるという原則を受け入れます。Cのこれらの基本操作は、
この原則を正当化するには、一般的なコンピューターの機械語命令(基本ステップ)の詳細な調査が必要です。説明されている各操作は、少数の機械語命令で実行できます。多くの場合、1つまたは2つの指示のみが必要です。その結果、Cのいくつかの種類のステートメントをO(1)
時間内に実行できます。つまり、入力に関係なく一定の時間内に実行できます。これらの単純な
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)
実行時間を与えることを確認し
ます。
より実用的な例。
コードを分析するのではなく、経験的にコードの順序を推定したい場合は、nの値を増やして、コードの時間を増やすことができます。タイミングを対数スケールでプロットします。コードがO(x ^ n)の場合、値は勾配nの直線上にあるはずです。
これには、コードを研究するだけの利点がいくつかあります。1つには、ランタイムが漸近順序に近づく範囲内にいるかどうかを確認できます。また、たとえば、ライブラリの呼び出しに時間を費やしているため、O(x)の次数がO(x ^ 2)の次数であると思ったコードが見つかることがあります。
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倍の時間で実行されます。
アルゴリズムをビッグO表記について知っている部分に分解し、ビッグO演算子で結合します。それが私が知っている唯一の方法です。
詳細については、件名のWikipediaページを確認してください。
使用しているアルゴリズム/データ構造の知識、および/または反復ネストのクイック分析。問題は、ライブラリ関数を(場合によっては複数回)呼び出すときです。多くの場合、関数を不必要に呼び出すのか、それともどの実装を使用しているのかわからない場合があります。たぶん、ライブラリ関数には、Big Oであろうと他のメトリックであろうと、ドキュメントやIntelliSenseでさえも利用できる複雑さ/効率性の測定が必要です。
マスターメソッド(またはその専門分野の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);
見落とされがちなのは、アルゴリズムの予想される動作です。アルゴリズムのBig-Oは変更されません「時期尚早の最適化...」というステートメントに関連しています。
アルゴリズムの予想される動作は-非常に馬鹿げています-アルゴリズムが表示する可能性が最も高いデータを処理することを期待できる速度です。
たとえば、リスト内の値を検索する場合、それはO(n)ですが、表示されるほとんどのリストに前もって値があることがわかっている場合、アルゴリズムの一般的な動作はより高速です。
実際にそれを明確にするには、「入力スペース」の確率分布を記述できる必要があります(リストをソートする必要がある場合、そのリストはすでにソートされる頻度ですか?どのくらいの頻度で完全に逆転するか?方法多くの場合、ほとんどがソートされていますか?)それを知っていることは常に可能であるとは限りませんが、時にはそうすることもあります。
すばらしい質問です!
免責事項:この回答には誤った説明が含まれています。下記のコメントを参照してください。
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ループや、データ構造に基づく推論など、簡単なケースでどのような入力が発生し、どのような入力が発生するかを調べるという簡単な問題になります。最悪の場合。
プログラムでこれを解決する方法はわかりませんが、最初に行うことは、実行される操作の数の特定のパターンについてアルゴリズムをサンプリングすることです。たとえば、4n ^ 2 + 2n + 1には2つのルールがあります。
f(x)を簡略化すると、f(x)は実行された操作の数の式(上記で説明した4n ^ 2 + 2n + 1)であり、ビッグO値[O(n ^ 2)が得られます。場合]。しかし、これはプログラムのラグランジュ補間を考慮する必要があり、実装が難しい場合があります。そして、実際のbig-O値がO(2 ^ n)で、O(x ^ n)のようなものがある場合、このアルゴリズムはおそらくプログラムできません。しかし、誰かが私を間違っていると証明した場合は、コードを教えてください。。。。
Big-Oについて少し異なる側面から説明したいと思います。
Big-Oは単にプログラムの複雑さを比較することです。つまり、入力が増加しているときにプログラムがどれだけ速く成長するかを意味し、アクションを実行するために費やされる正確な時間ではありません。
big-O式では、より複雑な式を使用しない方がよい(次のグラフの式をそのまま使用する方がよい)。ただし、他のより正確な式(3 ^ n、n ^ 3など)を使用することもできます。 。)しかし、それ以上の場合は誤解を招くことがあります。できるだけシンプルに保つことをお勧めします。
ここで、アルゴリズムの正確な公式を取得したくないことを再度強調しておきます。入力が増加しているときにそれがどのように成長するかを示し、その意味で他のアルゴリズムと比較したいだけです。それ以外の場合は、ベンチマークなどのさまざまな方法を使用することをお勧めします。