O(log n) アルゴリズムを初めて目にするのはかなり奇妙だということに同意しなければなりません...その対数はいったいどこから来たのでしょうか? ただし、対数項を big-O 表記で表示するには、いくつかの方法があることがわかりました。ここにいくつかあります:
定数除算の繰り返し
任意の数 n を取ります。たとえば、16. n を 2 で割ると、1 以下の数値が得られますか? 16 の場合、
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
これを完了するには 4 つの手順が必要になることに注意してください。興味深いことに、log 2 16 = 4もあります。うーん... 128 はどうですか?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
これには 7 つのステップが必要で、log 2 128 = 7 でした。これは偶然ですか? いいえ!これには正当な理由があります。数 n を 2 で i 回割るとします。次に、数 n / 2 iを取得します。この値が最大で 1 である i の値を解きたい場合、次のようになります。
n / 2 i ≤ 1
n ≤ 2 i
log 2 n ≤ i
つまり、i ≥ log 2 n となる整数 i を選択すると、n を i 回半分に分割した後、最大で 1 の値が得られます。これが保証される最小の i は、おおよそ log 2です。 n であるため、数が十分に小さくなるまで 2 で割るアルゴリズムがある場合、O(log n) ステップで終了すると言えます。
重要な点は、n をどの定数で割るか (1 より大きい限り) は問題ではないということです。定数 k で除算すると、1 に到達するまでに log k n ステップかかります。したがって、入力サイズを分数で繰り返し除算するアルゴリズムは、O(log n) 回の反復を終了する必要があります。これらの反復には多くの時間がかかる可能性があるため、正味実行時間は O(log n) である必要はありませんが、ステップ数は対数になります。
では、これはどこで発生しますか?古典的な例の 1 つは、二分探索です。これは、並べ替えられた配列から値を検索するための高速アルゴリズムです。アルゴリズムは次のように機能します。
- 配列が空の場合、要素が配列に存在しないことを返します。
- さもないと:
- 配列の中央の要素を見てください。
- 探している要素と等しい場合は、成功を返します。
- 探している要素よりも大きい場合:
- 探している要素よりも小さい場合:
たとえば、配列で 5 を検索するには
1 3 5 7 9 11 13
最初に中間要素を見てみましょう。
1 3 5 7 9 11 13
^
7 > 5 であり、配列が並べ替えられているため、5 という数値が配列の後半にあることはあり得ないことがわかっているので、それを破棄することができます。この葉
1 3 5
それでは、ここで中央の要素を見てみましょう。
1 3 5
^
3 < 5 であるため、配列の前半に 5 が表示されないことがわかっているため、前半の配列をスローして終了できます。
5
再び、この配列の中央を見てみましょう。
5
^
これはまさに私たちが探している数であるため、5 が実際に配列にあると報告できます。
では、これはどのくらい効率的ですか?各反復で、残りの配列要素の少なくとも半分を破棄しています。アルゴリズムは、配列が空になるか、必要な値が見つかるとすぐに停止します。最悪の場合、要素がそこにないため、要素がなくなるまで配列のサイズを半分にし続けます。これにはどのくらいかかりますか?ええと、配列を何度も半分に分割し続けるので、実行前に配列を O(log n) 回よりも多く半分に分割することはできないため、多くても O(log n) 回の繰り返しで完了します。配列要素が不足しています。
分割統治法(問題をバラバラに分割し、それらのピースを解決してから、問題を元に戻す)の一般的な手法に従うアルゴリズムは、これと同じ理由で対数項を持つ傾向があります。オブジェクトを分割し続けることはできません。 O(log n) 回の半分以上。この良い例として、マージソートを見たいと思うかもしれません。
一度に 1 桁ずつ値を処理する
10 進数の n は何桁ですか? 数字に k 桁がある場合、最大の桁は 10 kの倍数になります。k 桁の最大の数は 999...9 の k 倍であり、これは 10 k + 1 - 1に等しくなります。したがって、n に k 桁があることがわかっている場合、n の値は次のようになります。せいぜい 10 k + 1 - 1 です。k を n に関して解きたい場合は、次のようになります。
n ≤ 10 k+1 - 1
n + 1 ≤ 10 k+1
log 10 (n + 1) ≤ k + 1
(log 10 (n + 1)) - 1 ≤ k
ここから、k はおおよそ n の 10 を底とする対数であることがわかります。つまり、n の桁数は O(log n) です。
たとえば、大きすぎて機械語に収まらない 2 つの大きな数を加算する複雑さについて考えてみましょう。これらの数値が基数 10 で表されていると仮定し、その数値を m および n と呼びます。それらを追加する 1 つの方法は、小学校の方法を使用することです。数字を一度に 1 桁ずつ書き、右から左に作業します。たとえば、1337 と 2065 を足すには、次のように数字を書き出すことから始めます。
1 3 3 7
+ 2 0 6 5
==============
最後の桁を追加し、1 を運びます。
1
1 3 3 7
+ 2 0 6 5
==============
2
次に、最後から 2 番目 (「最後から 2 番目」) の数字を追加し、1 を繰り上げます。
1 1
1 3 3 7
+ 2 0 6 5
==============
0 2
次に、最後から 3 番目 ("最後から 2 番目") の数字を追加します。
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
最後に、最後から 4 番目の数字 ("preantepenultimate"... I love English) を追加します。
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
さて、私たちはどれだけの仕事をしたでしょうか?1 桁あたり合計 O(1) の作業 (つまり、一定量の作業) を行い、合計で O(max{log n, log m}) の桁を処理する必要があります。これにより、合計で O(max{log n, log m}) の複雑さが得られます。これは、2 つの数値の各桁にアクセスする必要があるためです。
多くのアルゴリズムは、ある基数で一度に 1 桁ずつ処理することで O(log n) 項を取得します。古典的な例は、一度に 1 桁ずつ整数をソートする基数ソートです。基数ソートにはさまざまな種類がありますが、通常は O(n log U) の時間で実行されます。ここで、U はソートされる最大の整数です。この理由は、ソートの各パスに O(n) の時間がかかり、ソートされる最大数の O(log U) 桁のそれぞれを処理するために合計 O(log U) の反復が必要になるためです。Gabow の最短パス アルゴリズムやFord-Fulkerson max-flow アルゴリズムのスケーリング バージョンなど、多くの高度なアルゴリズムは、一度に 1 桁ずつ処理するため、複雑さの中に対数項があります。
その問題をどのように解決するかについての 2 番目の質問については、より高度なアプリケーションを調査するこの関連する質問を参照することをお勧めします。ここで説明されている問題の一般的な構造を考えると、結果に対数項があることがわかっている場合は、問題をどのように考えるかについてより良い感覚を持つことができます。そのため、答えを与えるまでは答えを見ないことをお勧めします。ある考え。
お役に立てれば!