A microscopic view of recursion in merge sort

Preface

This time, we are not discussing the merge sort algorithm in detail, but using the merge sort algorithm to discuss recursion. The characteristic of merge sort is that it uses two recursive calls in a row. This time we will observe the entire recursive process from a micro perspective and understand recursion in essence. If you can read it, you will definitely become stronger!

code

Let’s go straight to the code first!

using System.CodeDom.Compiler;

int _1 = 0;
int _2 = 0;

void __merge(int[] arr, int left, int mid, int right, string flag)
{
    
     
    Console.WriteLine($"__merge_{
      
      flag}: left={
      
      left+1}, mid={
      
      mid + 1}, right={
      
      right + 1}");
    int[] copy = new int[right - left + 1];
    //copy arr[left,right] to copy[]
    for (int ii = left; ii <= right; ii++)
    {
    
    
        copy[ii - left] = arr[ii];
    }
    int i = left;
    int j = mid + 1;
    for (int k = left; k <= right; k++)
    {
    
    
        if (i > mid)
        {
    
    
            arr[k] = copy[j-left];
            j++;
        }
        else if (j > right)
        {
    
    
            arr[k] = copy[i - left];
            i++;
        }
        else if (copy[i - left] < copy[j - left])
        {
    
    
            arr[k] = copy[i - left];
            i++;
        }
        else
        {
    
    
            arr[k] = copy[j - left];
            j++;
        }
    }
}

void __merge_sort(int[] arr, int left, int right, string flag)
{
    
    
    
    if (left >= right)
        return;

    if (flag.Contains("1"))
    {
    
    
        _1 += 1;
    }
    if (flag.Contains("2"))
    {
    
    
        _2 += 1;
    }

    int mid = (left + right) / 2;
    Console.WriteLine($"{
      
      flag}, left={
      
      left+1}, mid={
      
      mid+1}, right={
      
      right + 1}");
    __merge_sort(arr, left, mid, "第1个merge_sort");
    __merge_sort(arr, mid + 1, right, "第2个merge_sort");
    __merge(arr, left, mid, right, flag);
}


void merge_sort(int[] arr)
{
    
    
    __merge_sort(arr, 0, arr.Length - 1, "第0个merge_sort");
}

int[] arr = {
    
     1, 3, 5, 7, 8, 2, 4, 6};
merge_sort(arr);


Console.WriteLine($"_1:{
      
      _1}||_2:{
      
      _2}");
foreach (var item in arr)
{
    
    
    Console.Write(item + " ");
}

Console.ReadLine();

recursive analysis

The special thing about this code is that it uses two recursions:

_1 and _2 record the number of calls of the first and second recursion (irrelevant to the algorithm logic). The flag parameter added here is mainly to analyze the recursive process.

The first __merge_sort recursive function is mainly to continuously divide the array on the left into two parts.
The main function of the second __merge_sort recursion is to continuously divide the array on the right into two parts.

Merge combines the two divided arrays into one in order of size!

The difficulty of implementing this algorithm lies in the recursive construction and grasp of the array boundaries.

Macroscopically

void __merge_sort(int[] arr, int left, int right)
{
    
    
    int mid = (left + right) / 2;
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag);
}

The process is to divide the array into two parts through the recursion of __merge_sort, and then merge the two divided arrays.
The premise for __merge to merge is that the two arrays to be merged are already sorted arrays!
However, if we divide it into a single number, a number is an array, and this number can also be regarded as an
ordered array.
Insert image description here
Therefore, when the two points reach the "extreme" level, the premise of __merge is met.

After the bisection is completed, the following is the work of Merge:
Merge process
Seeing this picture, it is easy to think of a recursive algorithm, but how to construct a recursive function? It's a bit like:
How many steps does it take to install an elephant in a refrigerator? This is what we see from a macro perspective:
1 The first step is divided into the left side: __merge_sort(arr, left, mid);
2 The second step is divided into the right side: __merge_sort(arr, mid + 1, right);
3 The third step is integrated together : __merge(arr, left, mid, right, flag);

From a micro perspective

Let’s first look at how the entire recursive process is executed from a micro perspective (please view the following two pictures):
Insert image description hereInsert image description here
This is the execution result of the program, and the 0th one indicates that the outermost __merge_sort is called.
At this time, the leftmost one is 1, the middle one is 4, and the rightmost one is 8.
Then a recursive call of __merge_sort is triggered, and the first __merge_sort is responsible for the left side.
So it is: the leftmost one is 1, the middle one is 2, and the rightmost one is 4. At this time, the conditions for recursive exit are not met,
so the first __merge_sort is continued to be called. At this time, continue to be responsible for the left side (note that it is the left side of 1 2 3 4).
So there is 1 1 2, then it is obvious that the next time the recursion occurs, the left side will be equal to the right side (left >= right), so the conditions for recursive exit will be met next time.

The following paragraph is the key point:

So next time, the second recursive call starts! He is responsible for the two points on the right side. Some people may find it strange here, isn't it responsible for the call on the right? Why does it print 3 3 4? This is the left!
This is what I understand. Recursion is divided into levels. Each level of recursion is like going down a flight of stairs. Each time the recursion returns, it is a step up. When we exited just now, we were actually in two points 1 2 3 4 This level of the ladder, so at this time, in the entire level, what needs to be divided into two is the right side of 1 2 3 4! So the dichotomy is 3 3 4.

At this time, the __merge_sort of this layer will also return to the previous layer.
What is printed at this time is 5 6 8, and the direct division is 5 6 7 8 on the right. This is because the 1 2 3 4 on the left of the previous layer has been divided in the last recursion! (Each level of recursion has its own memory. In fact, the parameters of each level are pushed onto the stack for storage.) At this time, we have reached the top level of the recursion, and the left and right sides of the first level have been divided.
The next step is to continue the recursion to the next level. The 1 2 3 4 on the left has been divided into two, so it is 5 6 7 8 on the right, and 5 6 7 8 has also been
divided into 56 | 78. Therefore, the first __merge_sort starts to bisect the 56 on the left.
So what is printed at this time is 5 5 6, and finally the second one on the right is divided into 7 7 8. The entire bisection process is over.

It should be noted that the two __merge_sorts are always at the same level. When the first __merge_sort goes down a few stairs, the second one will actually go down the same number of stairs. (This will be explained further below)

merged part

Next, let's take a look at the calling process of the __merge function after the two divisions:
Insert image description here
Merge process
This is completely as expected:
display the left one, first merge 12, then merge 14, then merge 1234
and then the right one, first merge 56, Well, okay, 78, the result
is 148 after merging 5678, which is the entire merge of 12345678!

Now, let's combine recursion and merging to see what the sequence is:
Insert image description here
Insert image description here

code review

    int mid = (left + right) / 2;
    Console.WriteLine($"{
       
       flag}, left={
       
       left+1}, mid={
       
       mid+1}, right={
       
       right + 1}");
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag); } ```

First of all, after the first three consecutive recursions of __merge_sort, the first merge started directly!
Here, someone may ask: According to the calling order of the functions, the second __merge_sort should not be executed at this time? Is it directly transferred to
the __merge function? Will the second __merge_sort not be executed?

Here, I emphasize the level issue again. Now we have recursed to the last level. At this time, left mid right
corresponds to 1 1 2. In fact, 12 is divided into two parts. This corresponds to the second __merge_sort at this level. Say:
__merge_sort(arr, mid + 1, right);
left = mid+1 So at this time, the recursive exit condition left >= right is met (in fact, there are only 2 left, so you don’t have to divide it on the right side!)
So At this time, it is not that the second __merge_sort is not called, but it exits directly. (The exit condition of recursion is also one of the most important cores of recursion)
So __merge is executed to complete 12 merges (the merge process is actually sorting, you can refer to the top __merge code).

At this time, the recursion has bottomed out and started to return to the previous time. The left side of the previous level has been recursively completed (12 has been divided into two, which also satisfies the recursive exit condition). Therefore, the recursion on the right side of the ladder starts and 34 is divided into two ( Note: All the divisions around 124 are over here ), and it will return after the two divisions are completed,
so __merge will be executed to complete the merger of 34. This time, after __merge ends, there is another
__merge, completing the merger of 1 2 4. In other words, the first two __merge_sorts are skipped!
Why is this?

This is because after __merge is executed, the recursion will go up to another level. At this level, it is actually the two divisions of 1 2 4,
and the division of left and right of 1 2 4 has ended in the previous recursive process, so The merger started immediately.

At this time, the remaining part is:
Insert image description here
Insert image description here
after the merger is completed, the recursion returns this time, and it reaches the top level of recursion, but the left part has been executed, so it is the division of 5 6 8 on the right, After dividing and playing, from the second __merge_sort, recursion is entered again (the next level of stairs). At this time, the first __merge_sort of the next level is encountered . So there are 5 5 6, which has hit the bottom, so when we return and encounter the second __merge_sort of this level, we have 778. At this point, both recursions have bottomed out and are completed, and the next step is to merge!

Here are some thoughts. After reading this, you should have realized the characteristics of calling two recursions. When you encounter the first recursion, you will recurse to the bottom layer, and then return layer by layer, as follows: During the return Insert image description here
process The second __merge_sort on the penultimate level will be called, so the first __merge_sort is called when recursively going down the stairs, and the second recursion is called when going up the stairs, and when going up to the top level , just after calling the second __merge_sort, it will enter the next level of recursion, and encounter the first __merge_sort again, and enter the first level of recursion again! Bottoming out again!

problem of times

Let's look at another problem (not related to recursion). If the array is expanded to 10:
Insert image description here
Insert image description here
this time, the recursion responsible for the left side runs 5 times, while the recursion responsible for the right side only runs 3 times. Is it imbalanced this time?
Do you find it strange?
This is because of the issue of odd and even numbers. When the array is 8, 8 becomes 4+4 after being divided into two, and finally becomes 2+2+2+2.
It is an even number until it becomes single. If it is 10, the two points will become 5. The number 5 will cause the number of two points on the left to be more when the number is two points.
So only when the number is 2 to the Nth power, such as 8 16, when the array length is such, the number of calls to the two recursions will be the same!

Recursive summary

See, in the end, can you still recall how __merge_sort achieves bisection?
It doesn't matter if you can't remember it, because this process is very secretive, but it is also the key to recursive design.

void __merge_sort(int[] arr, int left, int right)
{
    
    
    int mid = (left + right) / 2;
    __merge_sort(arr, left, mid);
    __merge_sort(arr, mid + 1, right);
    __merge(arr, left, mid, right, flag);
}

First, we have to design a recursive function ourselves, such as passing in an array. Our purpose is to change the order of the elements inside the array, but each time we consider one part of it. So I need a border, left and right.
For the entire array, left is 0 and right is length-1;
after bisection, left and right will change after each bisection.
Each recursive call will go down a ladder and enter the next level, causing left and right to change again.
Being able to understand "entering the next level" is the key to understanding recursion. In each recursion, the process of dichotomy is completed!
We can first design ideas from a macro perspective, and then ensure that the ideas are correct from a micro perspective.

I have been writing this article for a long time and I feel good about myself. I don’t know what you think. Feedback in the comment area is welcome~~~

Attached, please provide the complete python code.

I originally used python for testing before, but I still find it convenient to debug C# with vs:

def __merge(arr, left, mid, right):
    arr_copy = arr[left:right + 1][:]
    i = left
    j = mid+1
    for k in range(left, right+1):
        if i > mid:
            arr[k] = arr_copy[j-left]
            j = j + 1
        elif j > right:
            arr[k] = arr_copy[i-left]
            i = i + 1
        elif arr_copy[i-left] < arr_copy[j-left]:
            arr[k] = arr_copy[i-left]
            i = i + 1
        else:
            arr[k] = arr_copy[j-left]
            j = j + 1


def __merge_sort(arr, left, right):
    if left >= right:
        return
    mid = (left + right) // 2
    print(left, mid, right)
    __merge_sort(arr, left, mid)
    __merge_sort(arr, mid + 1, right)
    __merge(arr, left, mid, right)


def merge_sort(arr):
    __merge_sort(arr, 0, len(arr) - 1)


if __name__ == '__main__':
    arr0 = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]
    merge_sort(arr0)
    print(arr0)

Guess you like

Origin blog.csdn.net/songhuangong123/article/details/132432459