web-dev-qa-db-ja.com

マージソートが最悪の場合の実行時間O(n log n)なのはなぜですか?

誰かが簡単な英語で説明してもらえますか、それを簡単に説明できますか?

50
adit

「従来の」マージソートでは、データを通過するたびに、ソートされたサブセクションのサイズが2倍になります。最初のパスの後、ファイルは長さ2のセクションにソートされます。 2回目のパスの後、長さ4。次に8、16など、ファイルのサイズまで。

ファイル全体を構成するセクションが1つになるまで、並べ替えられたセクションのサイズを2倍にしておく必要があります。ファイルサイズに達するには、セクションサイズをlg(N)倍にする必要があり、データの各パスには、レコード数に比例した時間がかかります。

31
supercat

Merge Sortは、Divide-and-Conquerアプローチを使用して、ソートの問題を解決します。まず、再帰を使用して入力を半分に分割します。分割後、半分をソートし、ソートされた1つの出力にマージします。図をご覧ください

MergeSort recursion tree

最初に問題の半分をソートし、単純なマージサブルーチンを実行することをお勧めします。そのため、マージサブルーチンの複雑さと、再帰で呼び出される回数を知ることが重要です。

merge sortの擬似コードは本当に簡単です。

# C = output [length = N]
# A 1st sorted half [N/2]
# B 2nd sorted half [N/2]
i = j = 1
for k = 1 to n
    if A[i] < B[j]
        C[k] = A[i]
        i++
    else
        C[k] = B[j]
        j++

すべてのループで、4つの操作があることが簡単にわかります:k ++i ++または j ++ifステートメント、および属性C = A | B。そのため、4N + 2以下の操作でO(N)の複雑さが得られます。証明のために、N = 1(4N +2 <= 6N)がtrueであるため、4N + 2は6Nとして扱われます。

したがって、[〜#〜] n [〜#〜]要素を持つ入力があり、[〜#〜] n [〜#〜 ]は2の累乗です。すべてのレベルで、前の入力の半分の要素を持つ入力で2倍以上の副問題があります。これは、レベルj= 0、1、2、...、lgNでは2 ^ j長さN/2 ^ jの入力を持つ副問題。各レベルでの操作の数jは以下になります

2 ^ j * 6(N/2 ^ j)= 6N

常に6N以下の操作を行うレベルは重要ではないことに注意してください。

LgN + 1レベルがあるため、複雑さは

O(6N * (lgN + 1)) = O(6N*lgN + 6N) = O(n lgN)

参照:

65
Davi Sampaio

これは、マージソートが最悪の場合でも平均的な場合でも、各ステージで配列を2つの半分に分割するだけで、lg(n)コンポーネントと他のNコンポーネントが各ステージで行われる比較から得られるためです。したがって、それを組み合わせるとほぼO(nlg n)になります。平均ケースか最悪ケースかに関係なく、lg(n)係数は常に存在します。残りのN係数は、両方のケースで行われた比較から得られる比較に依存します。最悪のケースは、各ステージでN個の入力に対してN回の比較が行われるケースです。したがって、O(nlg n)になります。

20
Pankaj Anand

単一の要素があるステージに配列を分割した後、つまりサブリストと呼びます。

  • 各段階で、各サブリストの要素をその隣接するサブリストと比較します。たとえば、[@ Daviの画像の再利用]

    enter image description here

    1. ステージ1では、各要素がその隣接要素と比較されるため、n/2回の比較が行われます。
    2. ステージ2では、サブリストの各要素が隣接するサブリストと比較されます。これは、各サブリストがソートされるため、2つのサブリスト間で行われる比較の最大数はサブリストの長さ<= 2(ステージ2)およびサブリストの長さが2倍になるため、ステージ3で4回、ステージ4で8回比較。つまり、各段階での比較の最大数=(サブリストの長さ*(サブリストの数/ 2))==> n/2
    3. ご覧のように、ステージの合計数はlog(n) base 2になるため、合計の複雑さは==(各ステージでの最大比較数*ステージ数)= = O((n/2)* log(n))==> O(nlog(n))
18
blueskin

アルゴリズムmerge-sortは、Sの2つの要素をO(1) time . enter image description here

8
Andrii Glukhyi

再帰ツリーには深さlog(N)があり、そのツリーの各レベルで、結合されたN作業を実行して、2つのソートされた配列をマージします。

ソートされた配列のマージ

並べ替えられた2つの配列_A[1,5]_と_B[3,4]_をマージするには、両方を最初から繰り返し、2つの配列の中で最も低い要素を選択し、その配列のポインターをインクリメントします。両方のポインターがそれぞれの配列の最後に到達したら、完了です。

_[1,5] [3,4]  --> []
 ^     ^

[1,5] [3,4]  --> [1]
   ^   ^

[1,5] [3,4]  --> [1,3]
   ^     ^

[1,5] [3,4]  --> [1,3,4]
   ^      x

[1,5] [3,4]  --> [1,3,4,5]
    x     x

Runtime = O(A + B)
_

ソート図のマージ

再帰呼び出しスタックは次のようになります。作業は最下位のリーフノードから始まり、バブルアップします。

_beginning with [1,5,3,4], N = 4, depth k = log(4) = 2

  [1,5]    [3,4]     depth = k-1 (2^1 nodes) * (N/2^1 values to merge per node) == N
[1]  [5]  [3]  [4]   depth = k   (2^2 nodes) * (N/2^2 values to merge per node) == N
_

したがって、ツリーのNレベルのそれぞれでk workを実行します。ここで、k = log(N)

N * k = N * log(N)

4
geg

他の答えの多くは素晴らしいですが、heightおよびdepth「マージソートツリー」の例に関連しています。ここでは、ツリーに重点を置いて質問にアプローチする別の方法を示します。説明に役立つ別の画像を次に示します。 enter image description here

要約:他の回答が指摘したように、シーケンスの2つの並べ替えられたスライスをマージする作業は線形時間で実行されます(メインの並べ替え関数から呼び出すマージヘルパー関数)。
このツリーを見てみましょう。ここでは、ルートの各子孫(ルート以外)をソート関数の再帰呼び出しと考えることができます。各ノードにどれだけの時間を費やしているかを評価してみましょう。 。シーケンスのスライスとマージ(両方)は線形時間を要するため、ノードの実行時間は、そのノードでのシーケンスの長さに対して線形です。

ここでツリーの深さが入ります。nが元のシーケンスの合計サイズである場合、任意のノードでのシーケンスのサイズはn/2です、ここでiは深さです。これは上の画像に示されています。これを各スライスの線形作業量と合わせると、実行時間はO(n/2)ツリー内のすべてのノードに対して。ここで、n個のノードについて合計する必要があります。これを行う1つの方法は、2 ツリーの各深さレベルのノード。したがって、どのレベルでも、O(2 * n/2)、これはO(n)です。これは2s!各深さがO(n)である場合、lognであるこのバイナリツリーのheightを掛けるだけです。回答:O(nlogn)

参照: Pythonのデータ構造とアルゴリズム

3
trad

MergeSortアルゴリズムには3つのステップがあります。

  1. 除算ステップはサブアレイの中間位置を計算し、一定の時間O(1)がかかります。
  2. 征服ステップは、それぞれ約n/2要素の2つのサブ配列を再帰的にソートします。
  3. 結合ステップは、各パスで合計n個の要素をマージし、最大n個の比較を必要とするため、O(n)を使用します。

このアルゴリズムは、n個の要素の配列をソートするために約lognパスを必要とするため、総時間の複雑さはnlognです。

2
MisterJoyson