web-dev-qa-db-ja.com

マージソートの再帰を理解する

私が見るマージソートの実装のほとんどはこれに似ています。アルゴリズムの本の紹介と、私が検索するオンラインの機能。私の再帰チョップは、フィボナッチ世代をいじり回すこと(これは十分に単純だった)よりもはるかに先に進まないので、多分それは私の心を吹き飛ばす複数の再帰かもしれませんマージ機能。

Howこれはステップスルーですか?ここでプロセスをよりよく理解するために受けるべき戦略や読み物はありますか?

void mergesort(int *a, int*b, int low, int high)
{
    int pivot;
    if(low<high)
    {
        pivot=(low+high)/2;
        mergesort(a,b,low,pivot);
        mergesort(a,b,pivot+1,high);
        merge(a,b,low,pivot,high);
    }
}

そして、マージ(率直に言って、私はこの部分に到達する前に精神的に立ち往生していますが)

void merge(int *a, int *b, int low, int pivot, int high)
{
    int h,i,j,k;
    h=low;
    i=low;
    j=pivot+1;

    while((h<=pivot)&&(j<=high))
    {
        if(a[h]<=a[j])
        {
            b[i]=a[h];
            h++;
        }
        else
        {
            b[i]=a[j];
            j++;
        }
        i++;
    }
    if(h>pivot)
    {
        for(k=j; k<=high; k++)
        {
            b[i]=a[k];
            i++;
        }
    }
    else
    {
        for(k=h; k<=pivot; k++)
        {
            b[i]=a[k];
            i++;
        }
    }
    for(k=low; k<=high; k++) a[k]=b[k];
}
19
2c2c

MergeSortの「並べ替え」関数名はちょっと間違った名前だと思います。実際には「分割」と呼ばれるべきです。

進行中のアルゴリズムの視覚化です。

enter image description here

関数が再帰するたびに、入力配列の左半分から開始して、入力配列のますます細分化されていきます。関数が再帰から戻るたびに、関数は続行し、右半分で作業を開始するか、再び再帰してより大きな半分で作業します。

このような

[************************]mergesort
[************]mergesort(lo,mid)
[******]mergesort(lo,mid)
[***]mergesort(lo,mid)
[**]mergesort(lo,mid)
 [**]mergesort(mid+1,hi)
[***]merge
   [***]mergesort(mid+1,hi)
   [**]mergesort*(lo,mid)
    [**]mergesort(mid+1,hi)
   [***]merge
[******]merge
      [******]mergesort(mid+1,hi)
      [***]mergesort(lo,mid)
      [**]mergesort(lo,mid)
       [**]mergesort(mid+1,hi)
      [***]merge
         [***]mergesort(mid+1,hi)
         [**]mergesort(lo,mid)
           [**]mergesort(mid+1,hi)
         [***]merge
      [******]merge
[************]merge
            [************]mergesort(mid+1,hi)
            [******]mergesort(lo,mid)
            [***]mergesort(lo,mid)
            [**]mergesort(lo,mid)
             [**]mergesort(mid+1,hi)
            [***]merge
               [***]mergesort(mid+1,hi)
               [**]mergesort(lo,mid)
                 [**]mergesort(mid+1,hi)
               [***]merge
            [******]merge
                  [******]mergesort(mid+1,hi)
                  [***]mergesort(lo,mid)
                  [**]mergesort*(lo,mid)
                    [**]mergesort(mid+1,hi)
                  [***]merge
                     [***]mergesort(mid+1,hi)    
                     [**]mergesort(lo,mid)
                      [**]mergesort(mid+1,hi)
                     [***]merge
                  [******]merge
            [************]merge
[************************]merge
14
slashdottir

MERGE SORT:

1)アレイを半分に分割します
2)左半分を並べ替える
3)右半分を並べ替え
4)2つの半分を結合します

enter image description here

enter image description here

19
abe312

明らかなことは、サイズ8(ここでは2のべき乗が便利です)などの小さな配列で、紙の上でこのマージソートを試すことです。あなたがコードを実行しているコンピュータのふりをして、それが少し明確になり始めるかどうかを確認してください。

紛らわしいと思うものを説明しないので、あなたの質問は少し曖昧ですが、頭の中で再帰呼び出しを展開しようとしているように聞こえます。それは良いことかもしれないし、そうでないかもしれませんが、それはすぐにあなたの頭の中にあまりにも多くをもたらすことにつながると思います。コードを最初から最後までトレースしようとする代わりに、概念を抽象的に理解できるかどうかを確認してください。ソートのマージ:

  1. 配列を半分に分割します
  2. 左半分をソートします
  3. 右半分をソートします
  4. 2つの半分をマージします

(1)かなり明白で直感的であるべきです。ステップ(2)の重要な洞察はこれです。配列の左半分は配列です。 マージソートが機能すると仮定、配列の左半分をソートできるはずです。正しい?ステップ(4)は、実際にはアルゴリズムのかなり直感的な部分です。例はそれを簡単にする必要があります:

_at the start
left: [1, 3, 5], right: [2, 4, 6, 7], out: []

after step 1
left: [3, 5], right: [2, 4, 6, 7], out: [1]

after step 2
left: [3, 5], right: [4, 6, 7], out: [1, 2]

after step 3
left: [5], right: [4, 6, 7], out: [1, 2, 3]

after step 4
left: [5], right: [6, 7], out: [1, 2, 3, 4]

after step 5
left: [], right: [6, 7], out: [1, 2, 3, 4, 5]

after step 6
left: [], right: [7], out: [1, 2, 3, 4, 5, 6]

at the end
left: [], right: [], out: [1, 2, 3, 4, 5, 6, 7]
_

したがって、(1)と(4)を理解していると仮定すると、マージソートを考える別の方法はこれです。他の誰かがmergesort()を書いて、それが機能すると確信していると想像してください。次に、mergesort()の実装を使用して次のように記述できます。

_sort(myArray)
{
    leftHalf = myArray.subArray(0, myArray.Length/2);
    rightHalf = myArray.subArray(myArray.Length/2 + 1, myArray.Length - 1);

    sortedLeftHalf = mergesort(leftHalf);
    sortedRightHalf = mergesort(rightHalf);

    sortedArray = merge(sortedLeftHalf, sortedRightHalf);
}
_

sortは再帰を使用しないことに注意してください。 「両方の半分をソートしてからマージする」というだけです。上記のマージの例を理解していれば、このsort関数が言っていることを実行しているように思われることを願っています...並べ替えます。

さて、もっと注意深く見てみると... sort()mergesort()とほとんど同じです!それはmergesort()であるためです(ただし、再帰的ではないため、ベースケースがありません!)。

しかし、それは私が再帰関数を考えるのが好きです-あなたがそれを呼び出すときに関数が機能すると仮定します。必要なことを行うブラックボックスとして扱います。この仮定を立てると、多くの場合、そのブラックボックスを埋める方法を見つけるのは簡単です。特定の入力について、それを小さな入力に分解してブラックボックスにフィードできますか?それを解決した後、残っているのは、関数の開始時に基本ケースを処理することだけです(再帰呼び出しを行う必要がないケースです。たとえば、mergesort([])は単に戻ります空の配列; mergesort())の再帰呼び出しを行いません。

最後に、これは少し抽象的ですが、再帰を理解する良い方法は、実際に帰納法を使用して数学的証明を書くことです。帰納法による証明を書くために使われるのと同じ戦略が、再帰関数を書くために使われます:

数学の証明:

  • 主張が基本ケースに当てはまることを示す
  • いくつかのnよりも小さい入力に対してtrueであると仮定します
  • その仮定を使用して、サイズnの入力に対してまだ正しいことを示します。

再帰関数:

  • ベースケースを処理する
  • 再帰関数がいくつかのnより小さい入力で機能すると仮定します。
  • この仮定を使用して、サイズnの入力を処理します
8
rliu

マージソートの再帰部分に関しては、この page が非常に役立つことがわかりました。実行中のコードを追跡できます。最初に実行されるものと、次に続くものが表示されます。

トム

6

mergesort()は、if条件が_low < high_になるまで、配列を2つの半分に単純に分割します。 mergesort()を2回呼び出すと、1つはlowからpivotに、2つ目は_pivot+1_からhighになり、サブ配列が分割されますさらにもっと。

例を見てみましょう:

_a[] = {9,7,2,5,6,3,4}
pivot = 0+6/2 (which will be 3)
=> first mergesort will recurse with array {9,7,2} : Left Array
=> second will pass the array {5,6,3,4} : Right Array
_

left配列とright配列のそれぞれに1つの要素ができるまで繰り返します。最終的に、これに似たものができます:

_L : {9} {7} {2} R : {5} {6} {3} {4} (each L and R will have further sub L and R)
=> which on call to merge will become

L(L{7,9} R{2}) : R(L{5,6} R{3,4})
As you can see that each sub array are getting sorted in the merge function.

=> on next call to merge the next L and R sub arrays will get in order
L{2,7,9} : R{3,4,5,6}

Now both L and R sub array are sorted within
On last call to merge they'll be merged in order

Final Array would be sorted => {2,3,4,5,6,7,9}
_

@roliuによる回答のマージ手順を参照してください

4
Dangling Cruze

これがこのように回答された場合、私の謝罪。これは詳細な説明ではなく、単なるスケッチであることを認めます。

実際のコードがどのように再帰にマッピングされるかを見るのは明らかではありませんが、このように一般的な意味で再帰を理解することができました。

ソートされていないセット{2,9,7,5}入力として。 merge_sortアルゴリズムは、簡潔にするために「ms」で示されています。次に、操作を次のようにスケッチします。

ステップ1:ms(ms(ms(2)、ms(9))、ms(ms(7)、ms(5)))

ステップ2:ms(ms({2}、{9})、ms({7}、{5}))

ステップ3:ms({2,9}、{5,7})

ステップ4:{2,5,7,9}

一重項のmerge_sort({2})は、一重項(ms(2)= {2})。したがって、最も深い再帰レベルで最初の答えが得られます。残りの答えは、内部の再帰が終了し、結合されるとドミノのように転がります。

アルゴリズムの天才の一部は、ステップ1の再帰式をその構築を通じて自動的に構築する方法です。私を助けたのは、上記のステップ1を静的な式から一般的な再帰に変える方法を考えることでした。

3
Alan G.

再帰メソッドを呼び出すと、実際の関数がスタックメモリにスタックされると同時に実行されません。条件が満たされない場合、次の行に進みます。

これがあなたの配列であると考えてください:

int a[] = {10,12,9,13,8,7,11,5};

したがって、メソッドのマージソートは次のように機能します。

mergeSort(arr a, arr empty, 0 , 7);
mergeSort(arr a, arr empty, 0, 3);
mergeSort(arr a, arr empty,2,3);
mergeSort(arr a, arr empty, 0, 1);

after this `(low + high) / 2 == 0` so it will come out of first calling and going to next:

    mergeSort(arr a, arr empty, 0+1,1);

for this also `(low + high) / 2 == 0` so it will come out of 2nd calling also and call:

    merger(arr a, arr empty,0,0,1);
    merger(arr a, arr empty,0,3,1);
    .
    .
    So on

したがって、すべてのソート値は空のarrに保存されます。再帰関数がどのように機能するかを理解するのに役立つかもしれません

0
Milind

私はこれが古い質問であることを知っていますが、マージソートを理解するのに何が助けになったのかを考えたいと思いました。

ソートをマージするには2つの大きな部分があります

  1. 配列を小さなチャンクに分割(分割)
  2. アレイを結合する(征服する)

再帰の役割は単に分割部分です。

ほとんどの人を混乱させるのは、分割と分割の決定に多くのロジックがあると思うことですが、実際のソートのロジックのほとんどはmergeで発生します。再帰は単に前半を分割して実行するためにあり、後半は本当にループして、コピーします。

ピボットに言及する回答がいくつかありますが、マージソートとクイックソートを混同する簡単な方法なので、Wordの「ピボット」をマージソートに関連付けないことをお勧めします(「ピボット」の選択に大きく依存しています) )。どちらも「分割統治」アルゴリズムです。マージソートの場合、分割は常に中央で行われますが、クイックソートの場合、最適なピボットを選択する際に分割をうまく使用できます。

0
aug

問題をサブ問題に分割するプロセス 与えられた例は、再帰を理解するのに役立ちます。 int A [] = {短絡する要素の数。}、int p = 0; (恋人のインデックス)。 int r = A.length-1;(より高いインデックス)。

class DivideConqure1 {
void devide(int A[], int p, int r) {
    if (p < r) {
        int q = (p + r) / 2; // divide problem into sub problems.
        devide(A, p, q);   //divide left problem into sub problems
        devide(A, q + 1, r); //divide right problem into sub problems
        merger(A, p, q, r);  //merger the sub problem
    }
}

void merger(int A[], int p, int q, int r) {
    int L[] = new int[q - p + 1];
    int R[] = new int[r - q + 0];

    int a1 = 0;
    int b1 = 0;
    for (int i = p; i <= q; i++) {  //store left sub problem in Left temp
        L[a1] = A[i];
        a1++;
    }
    for (int i = q + 1; i <= r; i++) { //store left sub problem in right temp
        R[b1] = A[i];
        b1++;
    }
    int a = 0;
    int b = 0; 
    int c = 0;
    for (int i = p; i < r; i++) {
        if (a < L.length && b < R.length) {
            c = i + 1;
            if (L[a] <= R[b]) { //compare left element<= right element
                A[i] = L[a];
                a++;
            } else {
                A[i] = R[b];
                b++;
            }
        }
    }
    if (a < L.length)
        for (int i = a; i < L.length; i++) {
            A[c] = L[i];  //store remaining element in Left temp into main problem 
            c++;
        }
    if (b < R.length)
        for (int i = b; i < R.length; i++) {
            A[c] = R[i];  //store remaining element in right temp into main problem 
            c++;
        }
}
0
Shravan Kumar