O(log n)アルゴリズムを初めて見たときはかなり奇妙だということに同意する必要があります...いったいその対数はどこから来たのですか?ただし、ログ用語をbig-O表記で表示するにはいくつかの方法があることがわかります。ここにいくつかあります:
定数で繰り返し除算
任意の数nをとります。たとえば、16。1以下の数値を取得する前に、nを2で割り算できる回数は?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つのステップが必要で、ログ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で除算すると、log k nステップで1に到達します。したがって、入力サイズをある分数で繰り返し除算するアルゴリズムでは、終了するために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。nに関してkを解く場合、次のようになります。
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番目(「前半」)の数字を追加します。
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
最後に、最後から4番目(「preantepenultimate」...私は英語が大好きです)の数字を追加します。
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
さて、どれだけの作業をしましたか?数字ごとに合計O(1)の作業(つまり、一定量の作業)を行い、処理が必要なO(max {log n、log m})の合計桁があります。2つの数値の各桁にアクセスする必要があるため、合計O(max {log n、log m})の複雑さが得られます。
多くのアルゴリズムは、いくつかのベースで一度に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番目の質問については、より高度なアプリケーションを探索するこの関連する質問を参照することをお勧めします。ここで説明する問題の一般的な構造を考えると、結果にログ用語があることがわかっている場合は、問題をどのように考えるかをよりよく理解できるようになるため、答えが得られるまで答えを見るのはお勧めしませんいくつかの考え。
お役に立てれば!
O(log n)
問題のサイズを2倍にするn
と、アルゴリズムで必要なステップ数は一定です。