私は大きなO表記についてかなりたくさんの質問があることを知っています、私はすでにチェックしました:
いくつか例を挙げると。
n
、n^2
、n!
などの計算方法を「直感」で知っていますが、のアルゴリズムの計算方法に完全に迷っています。 log n
、n log n
、n log log n
など。
つまり、クイックソートは(平均して)n log n
であることがわかっていますが、なぜ?マージ/コームなども同様です。
誰かが私を数学的にあまり説明しませんか?これをどのように計算しますか?
主な理由は、イムが大きなインタビューをしようとしていることであり、彼らがこの種のものを要求すると確信しています。私は数日間調査しましたが、バブルソートがn ^ 2である理由の説明か、 Wikipedia の(私にとって)読めない説明のどちらかを誰もが持っているようです。
対数は、べき乗の逆演算です。べき乗の例は、各ステップでアイテムの数を2倍にする場合です。したがって、対数アルゴリズムは、多くの場合、各ステップでアイテムの数を半分にします。たとえば、バイナリ検索はこのカテゴリに分類されます。
多くのアルゴリズムでは、対数の大きなステップが必要ですが、大きなステップごとにO(n)作業単位が必要です。マージソートはこのカテゴリに分類されます。
通常、これらの種類の問題は、バランスの取れた二分木として視覚化することで識別できます。たとえば、マージソートは次のとおりです。
6 2 0 4 1 3 7 5
2 6 0 4 1 3 5 7
0 2 4 6 1 3 5 7
0 1 2 3 4 5 6 7
一番上は、ツリーの葉としての入力です。アルゴリズムは、その上の2つのノードを並べ替えることにより、新しいノードを作成します。平衡二分木の高さはO(log n)であることがわかっているため、O(log n)の大きなステップがあります。ただし、新しい各行の作成にはO(n)作業が必要です。O(logn)の大きなステップO(n)作業は、マージソートがOであることを意味します。 (n log n)全体。
一般に、O(log n)アルゴリズムは次の関数のようになります。各ステップでデータの半分を破棄します。
def function(data, n):
if n <= constant:
return do_simple_case(data, n)
if some_condition():
function(data[:n/2], n / 2) # Recurse on first half of data
else:
function(data[n/2:], n - n / 2) # Recurse on second half of data
O(n log n)アルゴリズムは、次の関数のようになります。また、データを半分に分割しますが、両方を考慮する必要があります。
def function(data, n):
if n <= constant:
return do_simple_case(data, n)
part1 = function(data[n/2:], n / 2) # Recurse on first half of data
part2 = function(data[:n/2], n - n / 2) # Recurse on second half of data
return combine(part1, part2)
Do_simple_case()はO(1)時間かかり、combine()はO(n)時間しかかかりません。
アルゴリズムは、データを正確に半分に分割する必要はありません。彼らはそれを3分の1と3分の2に分割することができ、それは問題ないでしょう。平均的なパフォーマンスの場合、平均で半分に分割するだけで十分です(QuickSortのように)。 (n /何か)と(n-n /何か)の断片で再帰が行われる限り、それは問題ありません。それを(k)と(n-k)に分割している場合、ツリーの高さはO(n)であり、O(log n)ではありません。
通常、実行するたびにスペース/時間が半分になるアルゴリズムのlognを要求できます。この良い例は、任意のバイナリアルゴリズム(バイナリ検索など)です。左または右のいずれかを選択すると、検索しているスペースが半分になります。半分を繰り返し行うパターンはlognです。
一部のアルゴリズムでは、直感で実行時間を厳密に制限することはほぼ不可能です(たとえば、O(n log log n)
の実行時間を直感的に理解できるとは思わないので、だれも疑っています。あなたがそうすることを期待するでしょう)。 CLRSアルゴリズム入門テキスト を手に入れることができれば、完全に不透明になることなく適切に厳密な漸近表記のかなり徹底的な処理を見つけることができます。
アルゴリズムが再帰的である場合、境界を導出する簡単な方法の1つは、再帰を書き出してから、反復的に、または マスター定理 またはその他の方法を使用してそれを解決することです。たとえば、それほど厳密に考えたくない場合、QuickSortの実行時間を取得する最も簡単な方法は、マスター定理を使用することです。QuickSortでは、配列を2つの比較的等しいサブ配列に分割する必要があります(これを確認するのはかなり直感的です。これはO(n)
)であり、これら2つのサブ配列でQuickSortを再帰的に呼び出します。次に、T(n)
に実行時間を示すと、T(n) = 2T(n/2) + O(n)
が得られます。これは、マスターメソッドではO(n log n)
です。
ここにある「電話帳」の例を確認してください: 「BigO」表記のわかりやすい英語の説明は何ですか?
Big-Oはすべてscaleであることに注意してください。データセットが大きくなるにつれて、このアルゴリズムにはどれだけ多くの操作が必要になりますか?
O(log n)は通常、反復ごとにデータセットを半分にカットできることを意味します(例:二分探索)
O(n log n)は、O(log n)操作を実行していることを意味します各データセット内のアイテム
'O(n log log n)'は意味をなさないと確信しています。または、そうであれば、O(n log n)まで単純化されます。
マージソートがnlog nである理由を直感的に分析してみます。また、n log log nアルゴリズムの例を教えていただければ、それを処理することもできます。
マージソートは、要素のみが存在するまで要素のリストを繰り返し分割し、これらのリストをマージすることで機能する並べ替えの例です。これらの各マージの主な操作は比較であり、各マージには最大でn回の比較が必要です。ここで、nは結合された2つのリストの長さです。これから、漸化式を導き出し、簡単に解決できますが、その方法は避けます。
代わりに、マージソートがどのように動作するかを検討します。リストを取得して分割し、次にそれらの半分を取得して、長さ1のパーティションがn個になるまで再度分割します。この再帰が次のようになることを簡単に確認できます。リストをn個のパーティションに分割するまで、ログ(n)を深くするだけです。
これらのn個のパーティションのそれぞれをマージする必要があるので、それらがマージされたら、長さnのリストが再び得られるまで、次のレベルをマージする必要があります。このプロセスの簡単な例については、ウィキペディアの図を参照してください http://en.wikipedia.org/wiki/File:Merge_sort_algorithm_diagram.svg 。
ここで、このプロセスにかかる時間を検討します。ログ(n)レベルがあり、各レベルですべてのリストをマージする必要があります。毎回合計n個の要素をマージするため、各レベルのマージにはn時間かかります。次に、比較操作を最も重要な操作とすると、マージソートを使用して配列を並べ替えるのにn log(n)時間かかることがかなり簡単にわかります。
不明な点がある場合、またはどこかをスキップした場合は、お知らせください。より詳細に説明することができます。
2番目の説明を編集:
これをもっとよく説明できるかどうか考えさせてください。
問題は小さなリストの束に分割され、次に、現在ソートされている元のリストに戻るまで、小さいリストがソートおよびマージされます。
問題を分割すると、最初にサイズのいくつかの異なるレベルがあります:n/2、n/2の2つのサイズのリストがあり、次のレベルではサイズの4つのリストがあります:n/4、n/4、n/4、n/4次のレベルでは、n/8、n/8、n/8、n/8、n/8、n/8、n/8、n/8が続きます。 n/2 ^ kが1に等しくなるまで(各細分は長さを2の累乗で割ったものです)、すべての長さが4で割り切れるわけではないので、それほどきれいではありません)。これは2で除算を繰り返し、2 ^(log_2(n))= nであるため、最大でlog_2(n)回継続できます。したがって、2で除算すると、サイズがゼロのリストが生成されます。
ここで重要なのは、すべてのレベルにn個の要素があるため、マージは線形操作であるため、各レベルでマージにn時間かかることです。再帰のlog(n)レベルがある場合、この線形演算をlog(n)回実行するため、実行時間はn log(n)になります。
それでも役に立たない場合は申し訳ありません。
分割統治アルゴリズムを適用して、問題を簡単になるまでサブ問題に分割する場合、分割がうまくいくと、各サブ問題のサイズはn/2程度になります。これは多くの場合、big-Oの複雑さで発生するlog(n)
の起源です。O(log(n))
は、パーティショニングがうまくいくときに必要な再帰呼び出しの数です。