web-dev-qa-db-ja.com

マージソートで、再帰的に半分に分割するのではなく、すぐに個々のアイテムに分割しないのはなぜですか?

マージソートを学習する際の例では、アイテムのリストが何度も何度も分割され、その後再びマージされていることを示しています。

最初の分割アクションが常にリストを単一の個別のアイテムに分割する場合、リスト内の個別のアイテムペアを繰り返し処理することから始めませんか?

私はそれが反復的な方法対再帰的であることを理解していますが、この特定の例では、「分割」ステップは常にそれを単一の項目に分割するように思われるので、可能な場合に再帰的に分割する理由さえ少しわかりません1つのステップで1つのアイテムに分割するだけです。

マージソートの説明:

MergeSort関数は、部分配列array [p..r]を再帰的にソートする必要があります。つまり、mergeSort(array、p、r)を呼び出した後、配列のインデックスpからインデックスrまでの要素を昇順でソートする必要があります。

-サブ配列のサイズが0または1の場合、すでに並べ替えられているため、何もする必要はありません。 -それ以外の場合、マージソートは分割統治法を使用してサブ配列をソートします。

Merge(array、p、q、r)を使用して、ソートされたサブ配列array [p..q]とarray [q + 1..r]をマージします。

jsの例

var mergeSort = function(array, p, r) {
    if (r === p)
    {
        return array;
    }

    var q = p + floor((r - p)/2);

    mergeSort(array, p, q);
    mergeSort(array, q+1, r);
    merge(array, p, q, r);
};
3
MonkeyBonkey

再帰関数はすべて反復関数に変換できます。ただし、通常はマージソートに再帰を使用します。これは、半分に何度も分割すると、自然に再帰に向くためです。

再帰関数を反復関数に変換するには、通常、再帰によって無料で提供されるスタックを置き換えるための補助スタック実装が必要です。ただし、マージソートは、そのようなスタックがなくても反復的に実行できます。

マージソート、再帰バージョン:

void mergeSort(int arr[], int l, int r)
{
   if (l < r)
   {
      int m = l+(r-l)/2; //Same as (l+r)/2 but avoids overflow for large l & h
      mergeSort(arr, l, m);
      mergeSort(arr, m+1, r);
      merge(arr, l, m, r);
   }
}

マージソート、反復バージョン:

void mergeSort(int arr[], int n)
{
   int curr_size;  // For current size of subarrays to be merged
                   // curr_size varies from 1 to n/2
   int left_start; // For picking starting index of left subarray
                   // to be merged

   // Merge subarrays in bottom up manner.  First merge subarrays of
   // size 1 to create sorted subarrays of size 2, then merge subarrays
   // of size 2 to create sorted subarrays of size 4, and so on.
   for (curr_size=1; curr_size<=n-1; curr_size = 2*curr_size)
   {
       // Pick starting point of different subarrays of current size
       for (left_start=0; left_start<n-1; left_start += 2*curr_size)
       {
           // Find ending point of left subarray. mid+1 is starting 
           // point of right
           int mid = left_start + curr_size - 1;

           int right_end = min(left_start + 2*curr_size - 1, n-1);

           // Merge Subarrays arr[left_start...mid] & arr[mid+1...right_end]
           merge(arr, left_start, mid, right_end);
       }
   }
}

反復バージョンにはネストされたループが必要であることに注意してください。 merge関数は、どちらの場合もまったく同じです。

さらに読む
反復マージソート

7
Robert Harvey

重要なのは、マージ手順の数を最小限にすることです。

この「リスト内の個々のアイテムのペアを順番に繰り返し処理することから始める」というのが実際にどうなるかはわかりません。最初のペアを取り、それを簡単にソートしてから、2番目にソートされたペアとマージし、その結果を3番目のペアとマージするとします。その場合は、n/2のマージ手順を実行する必要があります。これには、平均でn/2の項目を反復処理する必要があるため、O(n ^ 2)になります。しかし、「実際の」マージソートを使用すると、O(log(n)* n)の全体的な複雑さのために、log2(n)マージステップしかありません。

2

トラバースするにはツリー構造が必要なので、(そして、コールスタックがトラバースforします。)

本質的に示唆しているように、ツリーを下から上に構築すると、実行する作業が減ることはありません。中間結果を異なる方法で追跡しているだけで、アルゴリズムがより複雑で直感的でないと私は考えています。

ロバート・ハーヴェイの答え が意味するように(ただし、直接述べることはありません)、ボトムアップでの構築はより反復的なアプローチです。ただし、繰り返しのアプローチでは、中間の戻り値に対して呼び出しスタック/ツリーが行うのと同じすべてのコンテナーとトラッカーを作成する必要があります。

違いは、youが反復解で行う作業が増えることです。


注目に値するもの:メモリに収まらない配列の処理を開始する場合は、言うまでもありませんが、ボトムアップからはるかに手動で構築すると、よりインテリジェントに作業できる可能性があります。ディスクから。

1
svidgen