Понимание рекурсии mergesort

большинство реализаций mergesort, которые я вижу, похожи на это. введение в алгоритмы книга вместе с онлайн implentations я ищу. Мои рекурсионные отбивные не идут намного дальше, чем возня с генерацией Фибоначчи (что было достаточно просто), поэтому, возможно, это множественные рекурсии, взрывающие мой разум, но я даже не могу пройти через код и понять, что происходит, даже прежде чем я даже нажму функцию слияния.

как это шагать через это? Быть там какую-то стратегию или чтение я должен пройти, чтобы лучше понять процесс здесь?

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];
}

9 ответов


Я думаю, что имя функции " сортировка "в MergeSort является немного неправильным, его действительно следует называть"divide".

вот визуализация алгоритма в процессе.

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

СОРТИРОВКА СЛИЯНИЕМ:

1) разделить массив пополам
2) сортировка левой половины
3) сортировка правой половины
4) объединить две половины вместе

enter image description here

enter image description here


очевидной вещью было бы попробовать эту сортировку слияния на небольшом массиве, скажем, размер 8 (мощность 2 удобна здесь), на бумаге. Представьте, что вы компьютер, выполняющий код, и посмотрите, станет ли он немного яснее.

ваш вопрос немного неоднозначен, потому что вы не объясняете, что вы находите запутанным, но похоже, что вы пытаетесь развернуть рекурсивные вызовы в своей голове. Что может быть или не быть хорошо, но я думаю, что это может легко привести к тому, что тоже много в голове сразу. Вместо того, чтобы пытаться проследить код от начала до конца, посмотрите, можете ли вы понять концепцию абстрактно. Объединить сортировку:

  1. разбивает массив пополам
  2. сортирует левую половину
  3. сортирует правую половину
  4. объединяет две половины вместе

(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
  • используйте это предположение, чтобы показать, что оно по-прежнему верно для ввода размера n

рекурсивная функция:

  • обрабатывать базовые случаи
  • предположим, что ваша рекурсивная функция работает на входах меньше, чем некоторые n
  • используйте это предположение для обработки ввода размера n

Что касается части рекурсии сортировки слияния, я нашел это страница чтобы быть очень полезным. Вы можете следовать коду, как он выполняется. Он показывает вам, что выполняется первым, и что следует за этим.

Тома


мои извинения, если на это был дан такой ответ. Я признаю, что это лишь набросок, а не глубокое объяснение.

хотя не очевидно, как фактический код сопоставляется с рекурсией, я смог понять рекурсию в общем смысле таким образом.

возьмите пример несортированного набора {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 выше из статической формулы в общую рекурсию.


the mergesort() просто делит массив на две половины, пока if условие не выполняется, что является low < high. Как вы зовете mergesort() в два раза : с low to pivot и второе pivot+1 to 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

он будет повторяться, пока у вас не будет 1 элемент в каждом left а также right массив. В конце концов, у вас будет что-то похожее на это :

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


процесс, чтобы разделить проблему на подзадачи Данный пример поможет вам понять рекурсию. int A[]={число элементов, подлежащих замыканию.}, int p=0; (индекс любовника). int r= A. Длина-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++;
        }
}

Я знаю, что это старый вопрос, но хотел бросить свои мысли о том, что помогло мне понять слияние.

есть две большие части для объединения сортировки

  1. разбиение массива на более мелкие куски (деление)
  2. объединение массива вместе (завоевание)

роль рекурсии-это просто разделительная часть.

Я думаю, что большинство людей смущает то, что они думают, что есть много логика в расщеплении и определении того, что разделить, но большая часть фактической логики сортировки происходит на слияние. Рекурсия просто существует, чтобы разделить и сделать первую половину, а затем вторая половина действительно просто зацикливается, копируя вещи.

Я вижу некоторые ответы, которые упоминают повороты, но Я бы рекомендовал не связывать слово "pivot" с сортировкой слияния, потому что это простой способ спутать сортировку слияния с quicksort (которая сильно зависит от выбора "оси"). Это алгоритмы "разделяй и властвуй". Для сортировки слияния разделение всегда происходит посередине, тогда как для quicksort вы можете быть умны с разделением при выборе оптимального поворота.


когда вы вызываете рекурсивный метод, он не выполняет реальную функцию одновременно с стеком в память стека. И когда условие не удовлетворено, тогда оно переходит к следующей строке.

считайте, что это Ваш массив:

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

таким образом, ваш метод merge sort будет работать, как показано ниже:

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. Это может помочь понять, как работает рекурсивная функция