元の記事の量が多いため、私はそれを部分に分割しましたが、そのうち合計4つになります。
私は(いつものように)翻訳の品質の改善に関するPMのコメントに非常に感謝します。
以前に公開された:
パート1
難易度
前の部分から、これらの装飾定数をすべて破棄できる場合、プログラム命令をカウントするための関数の漸近的な動作について話すことは非常に簡単になると結論付けることができます。 実際、ループを含まないプログラムには
f( n ) = 1
があります。この場合、一定数の命令が必要なためです(もちろん、再帰がない場合-以下を参照)。
1
から
n
までの単一サイクルは、サイクルの前後で不変の数のコマンドを実行し、サイクル内の一定数の命令が
n
回実行されるため、漸近
f( n ) = n
n
を与えます。
そのような考慮事項に導かれるのは、毎回指示を読むよりも面倒ではないので、この資料を統合するためのいくつかの例を見てみましょう。 次のPHPプログラムは
A
サイズ
n
配列
A
特定の値が含まれているかどうかを確認します。
<?php $exists = false; for ( $i = 0; $i < n; ++$i ) { if ( $A[ $i ] == $value ) { $exists = true; break; } } ?>
配列内の値を見つけるこの方法は、 線形検索と呼ばれます 。 プログラムには
f( n ) = n
があるため、これは有効な名前です(より正確には「線形」を意味します。次のセクションで検討します)。
break
使用すると、1回の反復の後でも、プログラムをより早く終了できます。 ただし、配列
A
特定の値がまったく含まれていない最も不利なシナリオに関心があることを思い出してください。 したがって、
f( n ) = n
は以前と同じです。
演習2
最も有害な場合に上記のPHPプログラムに必要な命令数を体系的に分析し、その漸近性を導き出します(最初の部分でJavascriptでプログラムを分析した方法と同様)。
になるはずです。
f( n ) = n
になるはずです。
配列から2つの値を追加し、その結果を新しい変数に書き込むPythonプログラムを見てみましょう。
v = a[ 0 ] + a[ 1 ]
ここには一定数の命令があるため、
f( n ) = 1
です。
次のC ++プログラムは
A
サイズ
n
ベクトル(元の配列)
A
2つの同一の値が含まれているかどうかを確認します。
bool duplicate = false; for ( int i = 0; i < n; ++i ) { for ( int j = 0; j < n; ++j ) { if ( i != j && A[ i ] == A[ j ] ) { duplicate = true; break; } } if ( duplicate ) { break; } }
ネストされた2つのサイクルは、
f( n ) = n
2の形の漸近を与えます。
実用的な推奨事項 :単純なプログラムは、ネストされたサイクルの数をカウントすることで分析できます。
n
回の繰り返しでの単一サイクルにより、
f( n ) = n
ます。 ループ内のループは
f( n ) = n
2です。 サイクル内のサイクル内のサイクルは
f( n ) = n
3です。 などなど。
ループ本体のプログラムで関数が呼び出され、その中で実行された命令の数がわかっている場合、プログラム全体のコマンドの総数を簡単に判断できます。 例として次のCコードを検討してください。
int i; for ( i = 0; i < n; ++i ) { f( n ); }
f( n )
が正確に
n
命令を実行することがわかっている場合、
f( n )
は
n
回呼び出されるため、プログラム全体の命令数は漸近的に
n
2に近づくと言えます。
実用的な推奨事項 :一連のforループが連続している場合、プログラムの漸近的な動作がそれらの中で最も遅いループを決定します。 1つに続く2つのネストされたループは、ネストされたループ自体と漸近的に同じです。 ネストされたループは、単一ループを支配すると言われています。
次に、理論家が使用する興味深い表記法に切り替えましょう。
f
の正確な漸近性を見つけると、プログラムは
Θ( f( n ) )
と言います。 たとえば、上記の例では、プログラム
Θ( 1 )
、
Θ( n
2
)
および
Θ( n
2
)
がそれぞれプログラムです。
Θ( n )
「theta from n」
Θ( n )
発音されます。
f( n )
(定数を含む命令をカウントする元の関数)は
Θ( - )
と言うこともあります。 たとえば、
f( n ) = 2n
は
Θ( n )
関数であると言えます。 一般的に、新しいものは何もありません。 また、
2n ∈ Θ( n )
と発音することもできます。これは「two nはthen from the n」に属します。 このような表記法と混同しないでください。プログラムに必要なコマンドの数を数えて
2n
にすると、このアルゴリズムの漸近的な動作は
n
(定数を破棄することでわかります)として記述されているだけです。 この表記法を使用して、いくつかの真の数学的ステートメントを示します。
- n 6 + 3n∈Θ(n 6 )
- 2 n + 12∈Θ(2 n )
- 3 n + 2 n∈Θ(3 n )
- n n + n∈Θ(n n )
ところで、最初のパートの1つを実行することにした場合、これは彼の正解です。
この関数(つまり
Θ( )
を記述するもの)を時間の複雑さ 、または単にアルゴリズムの複雑さと呼びます。 したがって、
Θ( n )
アルゴリズムの複雑さは
n
です。
Θ( 1 )
、
Θ( n )
、
Θ( n
2
)
および
Θ( log( n ) )
にも特別な名前があり
Θ( log( n ) )
。これらは非常に一般的です。 彼らは、
Θ( 1 )
は一定時間アルゴリズム、
Θ( n )
は線形 、
Θ( n
2
)
は2次 、
Θ( log( n ) )
は対数であると言います(何がわからなくても心配しないでください
Θ( log( n ) )
対数-これについてはすぐに説明します)。
実用的な推奨事項 :
Θ
が大きいプログラムは、
Θ
が小さいプログラムよりも実行が遅くなります。
「big O」という表記
実際には、上記で検討した方法でアルゴリズムの正確な動作を見つけるのは難しい場合があります。 特に複雑な例の場合。 しかし、私たちのアルゴリズムの振る舞いは決して境界を越えることはありません。 これにより、定数を無視しても(以前のように)アルゴリズムの速度が明確に示されない場合があるため、作業が楽になります。 必要なのはこの境界を見つけることであり、その方法は例で説明する方が簡単です。
学習アルゴリズムで使用される最もよく知られているタスクはソートです。 サイズ
n
の配列
A
n
(おなじみのように聞こえますか?)。そして、それをソートするプログラムを書くように求められます。 ここでの関心は、そのような必要性が実際のシステムでしばしば生じることです。 たとえば、ファイルブラウザーは、ユーザーがファイルを簡単にナビゲートできるように、ファイルを名前で並べ替える必要があります。 または別の例:ビデオゲームでは、仮想世界でのプレイヤーの視点からの距離に応じて、画面に表示されている3Dオブジェクトを並べ替えるタスクが発生する場合があります。 目的:それらのうちのどれが彼に見え、どれが見えないかを決定します(これは可視性問題と呼ばれます)。 ソートも興味深いものです。なぜなら、ソートには多くのアルゴリズムがあり、そのいくつかは他のものよりも悪いからです。 このタスクは、定義と説明も簡単です。 それでは、配列をソートするコードを書きましょう。
b = [] n.times do m = a[ 0 ] mi = 0 a.each_with_index do |element, i| if element < m m = element mi = i end end a.delete_at( mi ) b << m end
Rubyで配列の並べ替えを実装する完全に非効率的な方法を次に示します。 (もちろん、Rubyは、使用すべき組み込み関数を使用した配列のソートをサポートしています。これらは、説明のためだけに示した上記のコードよりも間違いなく高速です。)
このメソッドは、選択によるソートと呼ばれます 。 まず、配列の最小要素が検出され(上記のコードでは
a
。最小値は
m
、そのインデックスは
mi
として示されます)、新しい配列の最後に配置され(この場合は
b
)、元の配列から削除されます。 次に、残りの値の中で、最小値が再び検出され、新しい配列(現在2つの値を含む)に追加され、古い配列から削除されます。 すべての要素が元の配列から新しい配列に転送されるまで、このプロセスが繰り返されます。これは、ソートの終了を意味します。 この例では、2つのネストされたループがあります。 外側のループは
n
回「実行」され、内側のループは配列
a
各要素に対して1回実行されます。 最初に
a
は
n
要素があり、各反復でそれらの1つを削除してから、最初に内部ループが
n
回スクロールし、次に
n - 1
、
n - 2
など、最後の反復まで内部ループが1回だけ通過します。
合計
1 + 2 + ... + (n - 1) + n
を考慮すると、このコードの複雑さを見つけるのはやや問題になります。 しかし、それに対する「上限」を見つけることができます。 これを行うには、プログラムを変更して(コードに触れることなく精神的にできます)、それを悪化させてから、何が起こったかの複雑さを導き出します。 そうすれば、元のプログラムの動作がひどく、または(ほとんどの場合)良くなっていると自信を持って言うことができます。
次に、プログラムの複雑さの結論を単純化するために、プログラムを変更する方法について考えてみましょう。 忘れないでください:それを悪化させる(たとえば、コードに新しい命令を追加する)だけで、元のプログラムに対して評価が意味を持つようにすることができます。 明らかに、プログラムの内部ループを変更して、各反復で
n
回評価を強制することができます。 これらの繰り返しのいくつかは役に立たないでしょうが、結果のアルゴリズムの複雑さを分析するのに役立ちます。 この小さな変更を行うと、新しいアルゴリズムには
Θ( n
2
)
が含まれます。これは、それぞれが正確に
n
回実行される2つのネストされたループを取得するためです。 もしそうなら、元のアルゴリズムには
O( n
2
)
ます。
O( n
2
)
「
n
からの大きなOの2乗」
)
発音されます。 これは、プログラムが漸近的に
n
2より悪くないことを示唆しています。 彼女は良くも良くも働くでしょう。 ところで、コードに実際に
Θ( n
2
)
場合、それはまだ
O( n
2
)
であると言います。 これをよりよく理解するために、元のプログラムを変更してもそれほど変わらず、ほんの少しだけ悪くなると想像してください。 無駄な命令をプログラムの先頭に追加するようなもの。 これは、命令カウント関数の定数のみを変更しますが、漸近では無視します。 したがって、プログラムの
Θ( n
2
)
も
O( n
2
)
です。
しかし、その逆は必ずしも真実ではありません。 たとえば、
Θ( n )
持つコード
Θ( n )
、
O( n
)
に加えて
O( n )
ます。
Θ( n )
プログラムが
n
回実行さ
for
単なる
for
ループであると想像するなら、別の
for
insideを追加し、
n
回繰り返すことで悪化させることができます。 これにより、
f( n ) = n
2のプログラムが得られます。 要約:
Θ( a )
持つプログラムは、
b
よりも
b
O( b )
ほど悪い
O( b )
です。 また、変更は意味をなさないか、元のコードに似たコードを作成する必要がないことに注意してください。 彼に必要なのは、与えられた
n
に関して命令の数を増やすことだけです。 結果を使用して、問題を解決するのではなく、指示をカウントします。
したがって、プログラムに
O( n
2
)
があると言っても、安全です。アルゴリズムの分析により、
n
2よりも悪くなることはありませんでした。 これにより、プログラムの速度を適切に見積もることができます。 新しい表記でより良くなるように、いくつかの例を解いてみましょう。
演習3
次のうち、正しいものはどれですか?
-
Θ( n )
のアルゴリズムにはO( n )
-
Θ( n )
のアルゴリズムにはO( n
2)
-
Θ( n
2)
のアルゴリズムにはO( n
3)
-
Θ( n )
のアルゴリズムにはO( 1 )
-
O( 1 )
のアルゴリズムにはΘ( 1 )
-
O( n )
のアルゴリズムにはΘ( 1 )
解決策
- 元のプログラムには
Θ( n )
があるため、真です。 したがって、プログラムを変更せずにO( n )
を実現できます。 -
n
2はn
よりも悪いので、これは本当です -
n
3はn
2より悪いので、これは本当です -
1
n
より悪くないので、これは嘘です。 プログラムがn
命令(線形数)を漸近的に使用する場合、1
(定数)の命令のみを漸近的に必要とするように悪化させることはできません。 - 両方の困難は同じであるため、本当です。
- それは本当かもしれないし、そうでないかもしれません、それはアルゴリズムに依存します。 一般的な場合、これは嘘です。 アルゴリズムが
Θ( 1 )
場合、それは確かにO( n )
です。 ただし、O( n )
場合、Θ( 1 )
ではない可能性があります。 たとえば、Θ( n )
はO( n )
ですが、Θ( 1 )
はそうではありません。
演習4
算術級数のメンバーの合計を使用して、上記のプログラムが
2
だけでなく
2
でもあることを証明します。 等差数列がわからない場合は、 ウィキペディアをご覧ください -難しくありません。
O( n
2
)
だけでなく
Θ( n
2
)
でもあることを証明します。 等差数列がわからない場合は、 ウィキペディアをご覧ください -難しくありません。
アルゴリズムの
複雑度は、その真の複雑度の上限であるため、順番に
Θ
で表されるため、
Θ
が正確な推定値を与えると言うことがあります。 見つけた境界が正確でないことがわかっている場合は、小文字の
を使用してそれを示すことができます。 たとえば、アルゴリズムが
Θ( n )
場合、その正確な複雑度は
n
です。 したがって、このアルゴリズムは同時に
O( n )
と
O( n
2
)
です。 アルゴリズムは
Θ( n )
、
O( n )
は境界をより正確に定義します。 そして、
O( n
2
)
を
( n
2
)
(発音:「nの小さなo」と読みます
)
書くと、境界の非剛直性を知っていることを示すことができます。 もちろん、アルゴリズムの動作に関する詳細な情報を得るために、アルゴリズムの正確な境界を見つけることができる方が良いですが、残念ながら、これは必ずしも簡単ではありません。
演習5
次の境界のどれが厳密で、どれが厳密ではないかを判断します。
-
O( n )
を上限とするΘ( n )
アルゴリズム -
Θ( n
2)
O( n
3)
が上限として見つかっ)
アルゴリズム -
Θ( 1 )
O( n )
を上限として見つけΘ( 1 )
アルゴリズム -
O( 1 )
を上限として見つけたΘ( n )
アルゴリズム -
O( 2n )
を上限とするΘ( n )
アルゴリズム
解決策
- この場合、
Θ
複雑度とO
複雑度は同じであるため、境界は厳密です - ここで、
O
Θ
よりも大きいスケールの複雑さであるため、この境界は厳密ではありません。 実際、ここでの厳密な境界はO( n
2)
です。 アルゴリズムがo( n
3)
と書くことができるように - 繰り返しますが、
O
はΘ
よりも大きなスケールの複雑さであり、境界は厳密ではないと結論付けます。O( 1 )
厳密であり、O( n )
はo( n )
書き換えることができます - この境界は間違っているため、この境界の導出に間違いがありました。
Θ( n )
n
は1
よりも複雑なのでΘ( n )
アルゴリズムは上限O( 1 )
持つことができません。 忘れないでください、O
は上限を示します - 非厳密な境界線があるように見えるかもしれませんが、実際にはそうではありません。 実際、境界線は厳密です。 漸近
2n
とn
は同じであり、O
とΘ
は漸近のみに関連付けられていることを思い出してください。O( 2n ) = O( n )
であるため、複雑さはΘ
であるため、境界は厳密です。
実用的な推奨事項 :アルゴリズムの
O
複雑度を見つけることは、その
Θ
複雑度よりも簡単です。
今では、これらすべての新しい表記法と混同されているかもしれませんが、次の例に進む前に、さらに2つの文字を理解しましょう。 上記で、プログラムを変更して、O表記を作成するよりも悪化させました(命令数を増やし、実行時間を増やしました)。
は、コードが特定の制限より遅くなることは決してないことを教えてくれます。 これから、評価の基礎が得られます。プログラムは十分ですか? 反対のことを行い、既存のコードを改善し 、何が起こるかの複雑さを見つける場合、Ω表記を使用します。 したがって、
Ω
は複雑さをもたらしますが、これはプログラムを改善することはできません。 プログラムが遅いか、アルゴリズムが悪いことを証明したい場合に便利です。 また、この特定の場合に使用するにはアルゴリズムが遅すぎると言う場合にも使用できます。 たとえば、アルゴリズムが
Ω( n
3
)
ということは、アルゴリズムが
n
3より良くないことを意味します。
Θ( n
3
)
、または
Θ( n
4
)
、またはそれより悪い場合もありますが、その「良さ」の限界はわかります。 したがって、
Ω
は、アルゴリズムの複雑さの下限を示します。
ο
と同様に、この制限が厳密でないことがわかっている場合は、
ω
書くことができます。 たとえば、
Θ( n
3
)
アルゴリズムは
ο( n
4
)
および
ω( n
2
)
です。
Ω( n )
「nから大きいオメガ」
Ω( n )
発音され、
ω( n )
「nから小さいオメガ」
ω( n )
発音されます。
演習6
次のΘ難易度については、厳密なO制限と厳密でないO制限、および必要に応じて厳密な制限と厳密でないΩ制限(存在する場合)を記述します。
- Θ(1)
- Θ(√n)
- Θ(n)
- Θ(n 2 )
- Θ(n 3 )
解決策
これは、上記の定義を直接使用する演習です。
- 厳密な境界は
O( 1 )
およびΩ( 1 )
ます。 非厳密なO境界はO( n )
です。 Oには上限があることを思い出してください。n
は1
より大きいスケールにあるため、これは厳密な制限ではなく、o( n )
書くこともできます。 ただし、1
未満の関数はないため、Ω
厳密でない制限を見つけることはできません。 だから、あなたは厳格な国境に対処する必要があります - 厳密な制限はΘ複雑度と同じです。 O(√n)およびΩ(√n)、それぞれ。 非厳密な制限の場合、
n
√nより大きいためO( n )
n
ます。 そして、この境界は厳密ではないので、o( n )
書くことができます。 下の非厳密な境界として、単にΩ( 1 )
(またはω( 1 )
)を使用します - 厳密な制限は
O( n )
およびΩ( n )
です。 非厳密には、ω( 1 )
およびo( n
3)
ます。 どちらも元の複雑さからはほど遠いので、最高の境界線ではありませんが、私たちの定義に合っています - 厳密な境界は
O( n
2)
とΩ( n
2)
です。 非厳密な境界として、前の例のようにω( 1 )
とo( n
3)
を使用します - 厳密な境界は、それぞれ
O( n
3)
およびΩ( n
3)
です。 2つの非厳密な境界は、ω(√nn2)とo(√nn3)です。 これらの制限はまだ厳密ではありませんが、上記で推測した制限よりも優れています。
Θ
代わりに
O
と
Ω
を使用する理由は、見つかった境界の精度が不明な場合や、アルゴリズムを深く掘り下げたくない場合があるためです。
さまざまな表記法をすべて覚えていなくても心配する必要はありません。 いつでも戻って情報を更新できます。 最も重要な文字は
O
と
Θ
です。
また、
Ω
は関数の動作に下限を与えます(つまり、プログラムを改善して命令の計算を少なくする)が、最悪の場合の分析を参照していることに注意してください。 これは、最悪のデータセットをプログラムにフィードし、その動作を分析するためです。
次の表は、上記で示した記号と、数値を比較するための通常の数学アイコンとの関係をまとめたものです。 通常の数学表記の代わりにギリシャ文字を使用する理由は、通常ではなく漸近推定値の比較を扱っていることを示す必要があるからです。
漸近推定の比較演算子 | 数値比較演算子 |
---|---|
アルゴリズムはo (何か)です | 数< |
O ( - ) | ≤ - |
Θ ( - ) | = - |
Ω ( - ) | ≥ - |
ω ( - ) | > - |
: ,
O
,
o
,
Ω
,
ω
Θ
,
O
, ,
Θ
, ,
Ω
.