Big O表記法と、アルゴリズムの記述方法に基づいてそれを計算する方法について詳しく学びました。アルゴリズムのBig O表記を計算するための興味深い「ルール」のセットに出会い、自分が正しい方向に進んでいるか、それとも道を外れているかを確認したかった。
Big O表記:N
function(n) {
For(var a = 0; i <= n; i++) { // It's N because it's just a single loop
// Do stuff
}
}
Big O表記:N2
function(n, b) {
For(var a = 0; a <= n; a++) {
For(var c = 0; i <= b; c++) { // It's N squared because it's two nested loops
// Do stuff
}
}
}
Big O表記:2N
function(n, b) {
For(var a = 0; a <= n; a++) {
// Do stuff
}
For(var c = 0; i <= b; c++) { // It's 2N the loops are outside each other
// Do stuff
}
}
Big O表記:NLogN
function(n) {
n.sort(); // The NLogN comes from the sort?
For(var a = 0; i <= n; i++) {
// Do stuff
}
}
私の例とそれに続く表記は正しいですか?注意すべき追加の表記法はありますか?
正式には、big-O表記は 次数 の複雑さを表します。
Big-O表記を計算するには:
2N² + 3N
_2N²
_N²
_つまり、2つのループが入れ子になっていて、もう1つのループが入れ子になっていない場合、次の3つのループは[〜#〜] o [〜#〜](N²)になります。
もちろんこれは、ループ内にあるものが単純な命令であることを前提としています。たとえば、ループ内にsort()
がある場合、ループの複雑さを、基礎となる言語/ライブラリが使用しているsort()
実装の複雑さで乗算する必要があります。
これらのアルゴリズムを分析する場合は、// dostuffを定義する必要があります。これにより、結果が実際に変わる可能性があります。 dostuffが定数O(1)操作の数を必要とすると仮定します。
この新しい表記の例をいくつか示します。
最初の例では、線形トラバーサル:これは正しいです!
オン):
for (int i = 0; i < myArray.length; i++) {
myArray[i] += 1;
}
なぜ線形(O(n))なのですか?入力(配列)に要素を追加すると、発生する操作の量は、追加する要素の数に比例して増加します。
したがって、メモリのどこかで整数をインクリメントするために1つの操作が必要な場合、ループがf(x) = 5x = 5つの追加の操作で行う処理をモデル化できます。20の追加の要素に対して、 20の追加操作。配列の1つのパスは線形になる傾向があります。バケットの並べ替えのようなアルゴリズムも同様で、データの構造を利用して配列の1つのパスで並べ替えを実行できます。
2番目の例も正しく、次のようになります。
O(N ^ 2):
for (int i = 0; i < myArray.length; i++) {
for (int j = 0; j < myArray.length; j++) {
myArray[i][j] += 1;
}
}
この場合、最初の配列iの追加要素ごとに、jのすべてを処理する必要があります。 iに1を追加すると、実際には(jの長さ)がjに追加されます。したがって、あなたは正しいです!このパターンはO(n ^ 2)です。または、この例では実際にはO(i * j)(またはi == jの場合はn ^ 2です)。これは、行列演算や正方形のデータ構造の場合によく見られます。
3番目の例は、物事に応じて物事が変化する場所です。コードが書かれているとおりで、実行内容が定数の場合、実際にはO(n)のみです。これは、サイズnの配列の2つのパスがあり、2nがnに減少するためです。ループお互いの外側にいることは、2 ^ nコードを生成できる重要な要素ではありません。以下は、2 ^ nである関数の例です。
var fibonacci = function (n) {
if (n == 1 || n == 2) {
return 1;
}
else {
return (fibonacci(n-2) + fibonacci(n-1));
}
}
この関数は2 ^ nです。関数を呼び出すたびに、関数(フィボナッチ)が2回追加で呼び出されるためです。関数を呼び出すたびに、実行する必要のある作業量は2倍になります。これは、ヒドラの頭を切り落とし、毎回2つの新しいものが発芽するように、超急速に成長します!
最後の例として、merge-sortのようなnlgnソートを使用している場合、このコードはO(nlgn)になることは間違いありません。ただし、データの構造を利用して、特定の状況(1-100のような既知の限られた範囲の値など)でより高速なソートを開発することができます。ただし、最高次数のコードが優先するという考えは正しいです。したがって、O(nlgn) sortがO(nlgn)時間よりも短い時間を要する操作の隣にある場合、合計時間の複雑さはO(nlgn )。
JavaScript(少なくともFirefoxの場合)では、Array.prototype.sort()のデフォルトのソートは確かにMergeSortなので、最終的なシナリオとしてO(nlgn)を確認しています。
2番目の例(0からnまでの外部ループ、0からbまでの内部ループ)はO(nb)であり、O(-ではありませんn2)。ルールは、何かを計算しているということですn回、そしてそれらのそれぞれについて、何かを計算しているb回、したがって、この関数の成長は、 n * b。
3番目の例はO(n)です-nで成長しないため、すべての定数を削除できます。成長とはBig-O表記のすべてです。
最後の例については、はい、Big-O表記は確かに、比較に基づいている場合(通常の場合)、最も効率的な形式のO(ん *ログン)。
これは、ランタイムのおおよその表現であることを思い出してください。 「経験法則」は不正確であるため概算ですが、評価の目的で適切な1次近似を提供します。
実際の実行時間は、ヒープスペースの量、プロセッサの速度、命令セット、プレフィックスまたはポストフィックスのインクリメント演算子の使用などに依存します。yadda。適切なランタイム分析により、受け入れの判断が可能になりますが、基本的な知識があると、最初からプログラムを作成できます。
Big-Oが教科書から実用的なアプリケーションにどのように合理化されるかを理解するための正しい方向に進んでいることに私は同意します。それは克服するのが難しいハードルかもしれません。
漸近的な成長率は、大規模なデータセットや大規模なプログラムでは重要になるため、典型的な例では、適切な構文やロジックほど重要ではないことを示します。