After reading this article, you not only learned the algorithm routines, but you can also go to LeetCode to win the following topics:

912. Sorted Array (Medium)

315. Count the number of elements on the right that are smaller than the current element (difficult)

-----------

Many readers have always said that they want me to use the framework thinking talk about the basic sorting algorithm. I think it is really necessary to talk about it. After all, learning anything requires a mastery. Only a deeper understanding of its essence can ease-of-use.

This article will first talk about merge sort, give a set of code templates, and then talk about its application in algorithmic problems. Before reading this article, I hope you have read the previous article Brushing the Binary Tree by Hand (Programme) .

When I was talking about binary trees in (first issue) , I mentioned merge sort, saying that merge sort is the post-order traversal of binary trees. At that time, many readers left messages saying that they were enlightened.

Do you know why many readers feel brain-burning when they encounter recursive-related algorithms? Because it is still in the stage of "seeing mountains as mountains, and seeing water as water".

Let’s talk about merge sort. If I show you the code and let you think about the process of merge sort, what scenario will appear in your mind?

It's an array sorting algorithm, so you make up a GIF of an array, swapping elements one by one? If so, the pattern is low.

But if what comes to your mind is a binary tree, or even a scene of post-order traversal of the binary tree, then the pattern is high, and you have a high probability of mastering the framework thinking that I often emphasize, and learning algorithms with this abstract ability Save a lot.

So, merge sort is obviously an array algorithm, what does it have to do with binary trees? Next I will talk about it in detail.

Algorithm ideas

just put it this way, all recursive algorithms, you don't care what they do, are essentially traversing a (recursive) tree, and then executing the code on the nodes (the pre-in-post-order position), you have to write recursion The algorithm essentially tells each node what to .

You look at the code frame of merge sort:

// 定义:排序 nums[lo..hi]
void sort(int[] nums, int lo, int hi) {
    if (lo == hi) {
        return;
    }
    int mid = (lo + hi) / 2;
    // 利用定义,排序 nums[lo..mid]
    sort(nums, lo, mid);
    // 利用定义,排序 nums[mid+1..hi]
    sort(nums, mid + 1, hi);

    /****** 后序位置 ******/
    // 此时两部分子数组已经被排好序
    // 合并两个有序数组,使 nums[lo..hi] 有序
    merge(nums, lo, mid, hi);
    /*********************/
}

// 将有序数组 nums[lo..mid] 和有序数组 nums[mid+1..hi]
// 合并为有序数组 nums[lo..hi]
void merge(int[] nums, int lo, int mid, int hi);

Looking at this framework, you can also understand the classic summary: merge sort is to sort the left half of the array first, then the right half of the array, and then merge the two halves of the array.

The above code is very similar to the post-order traversal of a binary tree:

/* 二叉树遍历框架 */
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    traverse(root.left);
    traverse(root.right);
    /****** 后序位置 ******/
    print(root.val);
    /*********************/
}

Going a step further, think about the algorithm code for finding the maximum depth of a binary tree:

// 定义:输入根节点,返回这棵二叉树的最大深度
int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    // 利用定义,计算左右子树的最大深度
    int leftMax = maxDepth(root.left);
    int rightMax = maxDepth(root.right);
    // 整棵树的最大深度等于左右子树的最大深度取最大值,
    // 然后再加上根节点自己
    int res = Math.max(leftMax, rightMax) + 1;

    return res;
}

Is it more like?

The previous article Brushing the binary tree by hand (Programme) said that the binary tree problem can be divided into two types of ideas, one is the idea of traversing the binary tree once, and the other is the idea of decomposing the problem. According to the above analogy, it is obvious that the merge sort uses the decomposition problem idea (divide and conquer algorithm).

The process of merge sort can be logically abstracted into a binary tree, the value of each node on the tree can be considered as nums[lo..hi] , and the value of the leaf node is a single element in the array :

Then, execute the merge function at the post-order position of each node (the left and right child nodes have been sorted) to merge the subarrays on the two child nodes:

This merge operation will be executed once on each node of the binary tree, and the execution order is the order of post-order traversal of the binary tree.

Post-order traversal of binary trees should be familiar to everyone, which is the traversal order in the following figure:

Combined with the above basic analysis, we understand nums[lo..hi] as the node of the binary tree, and the srot function as the traversal function of the binary tree. The execution process of the entire merge sort is described by the following GIF:

In this way, the core idea of merge sort has been analyzed, and then it is only necessary to translate the idea into code.

Code implementation and analysis

As long as has the correct way of thinking, it is not difficult to understand the algorithm ideas, but it is also a test of a person's programming ability to implement the ideas into .

After all, the time complexity of the algorithm is only a theoretical measure, and the actual operating efficiency of the algorithm needs to consider more factors, such as frequent allocation and release of memory should be avoided, and the code logic should be as concise as possible.

After comparison, the merge sort code given in "Algorithm 4" is both concise and efficient, so we can refer to the code given in the book as the merge algorithm template:

class Merge {

    // 用于辅助合并有序数组
    private static int[] temp;

    public static void sort(int[] nums) {
        // 先给辅助数组开辟内存空间
        temp = new int[nums.length];
        // 排序整个数组(原地修改)
        sort(nums, 0, nums.length - 1);
    }

    // 定义:将子数组 nums[lo..hi] 进行排序
    private static void sort(int[] nums, int lo, int hi) {
        if (lo == hi) {
            // 单个元素不用排序
            return;
        }
        // 这样写是为了防止溢出,效果等同于 (hi + lo) / 2
        int mid = lo + (hi - lo) / 2;
        // 先对左半部分数组 nums[lo..mid] 排序
        sort(nums, lo, mid);
        // 再对右半部分数组 nums[mid+1..hi] 排序
        sort(nums, mid + 1, hi);
        // 将两部分有序数组合并成一个有序数组
        merge(nums, lo, mid, hi);
    }

    // 将 nums[lo..mid] 和 nums[mid+1..hi] 这两个有序数组合并成一个有序数组
    private static void merge(int[] nums, int lo, int mid, int hi) {
        // 先把 nums[lo..hi] 复制到辅助数组中
        // 以便合并后的结果能够直接存入 nums
        for (int i = lo; i <= hi; i++) {
            temp[i] = nums[i];
        }

        // 数组双指针技巧,合并两个有序数组
        int i = lo, j = mid + 1;
        for (int p = lo; p <= hi; p++) {
            if (i == mid + 1) {
                // 左半边数组已全部被合并
                nums[p] = temp[j++];
            } else if (j == hi + 1) {
                // 右半边数组已全部被合并
                nums[p] = temp[i++];
            } else if (temp[i] > temp[j]) {
                nums[p] = temp[j++];
            } else {
                nums[p] = temp[i++];
            }
        }
    }
}

With the previous foreshadowing, here we only need to focus on this merge function.

After the sort function has completed the recursive sorting of nums[lo..mid] and nums[mid+1..hi] , we have no way to merge them in place, so we need to copy them into the temp array, and then merge the double pointers of the ordered linked list through the six techniques similar to the previous singly linked list The trick combines nums[lo..hi] into an ordered array:

Note that we do not create a new auxiliary array when the merge function is executed, but new out the temp auxiliary array in advance, so as to avoid performance problems that may arise from frequent allocation and release of memory in recursion.

Let's talk about the time complexity of merge sort. Although everyone should know that it is O(NlogN) , not everyone knows how to calculate this complexity.

The previous dynamic programming said that the complexity of the recursive algorithm is the number of subproblems x the complexity of solving a subproblem. For merge sort, the time complexity is obviously concentrated in the process of merge function traversing nums[lo..hi] , but each time lo and merge input by hi are different, so it is not easy to intuitively see the time complexity.

How many times did the merge function execute? What is the time complexity of each execution? What is the total time complexity? This is combined with the picture drawn earlier:

The number of executions of is the number of binary tree nodes, and the complexity of each execution is the length of the subarray represented by each node, so the total time complexity is the number of "array elements" in the entire tree .

So on the whole, the height of this binary tree is logN , and the number of elements in each layer is the length of the original array N , so the total time complexity is O(NlogN) .

Likou Question 912 "Sort array" is to allow you to sort the array, we can directly apply the merge sort code template:

class Solution {
    public int[] sortArray(int[] nums) {
        // 归并排序对数组进行原地排序
        Merge.sort(nums);
        return nums;
    }
}

class Merge {
    // 见上文
}

other apps

In addition to the most basic sorting problems, merge sort can also be used to solve Likou's 315th problem "counting the number of elements on the right that is less than the current element":

The violent solution of slapping the head will not be mentioned, the nested for loop, the complexity of the square level.

What is the relationship between this question and merge sort, mainly in the merge function, when we merge two ordered arrays, we can actually know how many numbers after a number x are smaller than x .

Specifically, such as this scene:

At this time we should put temp[i] on nums[p] , because temp[i] < temp[j] .

But in this scenario, we can also know a piece of information: the number of elements smaller than 5 after 5 is the number of elements between j and mid + 1 , that is, 2.

In other words, in the process of merging nuns[lo..hi] , whenever nums[p] = temp[i] is executed, it can be determined that the number of elements following the element temp[i] is j - mid - 1 .

Of course, nums[lo..hi] itself is just a sub-array, and this sub-array will be executed merge , and the position of the elements will still change. But this is a problem that other recursive nodes need to consider. We only need to do some tricks in the merge function and superimpose the results recorded every time merge .

After discovering this rule, we only need to add two lines of code to merge to solve this problem, see the solution code:

class Solution {
    private class Pair {
        int val, id;
        Pair(int val, int id) {
            // 记录数组的元素值
            this.val = val;
            // 记录元素在数组中的原始索引
            this.id = id;
        }
    }
    
    // 归并排序所用的辅助数组
    private Pair[] temp;
    // 记录每个元素后面比自己小的元素个数
    private int[] count;
    
    // 主函数
    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        count = new int[n];
        temp = new Pair[n];
        Pair[] arr = new Pair[n];
        // 记录元素原始的索引位置,以便在 count 数组中更新结果
        for (int i = 0; i < n; i++)
            arr[i] = new Pair(nums[i], i);
        
        // 执行归并排序,本题结果被记录在 count 数组中
        sort(arr, 0, n - 1);
        
        List<Integer> res = new LinkedList<>();
        for (int c : count) res.add(c);
        return res;
    }
    
    // 归并排序
    private void sort(Pair[] arr, int lo, int hi) {
        if (lo == hi) return;
        int mid = lo + (hi - lo) / 2;
        sort(arr, lo, mid);
        sort(arr, mid + 1, hi);
        merge(arr, lo, mid, hi);
    }
    
    // 合并两个有序数组
    private void merge(Pair[] arr, int lo, int mid, int hi) {
        for (int i = lo; i <= hi; i++) {
            temp[i] = arr[i];
        }
        
        int i = lo, j = mid + 1;
        for (int p = lo; p <= hi; p++) {
            if (i == mid + 1) {
                arr[p] = temp[j++];
            } else if (j == hi + 1) {
                arr[p] = temp[i++];
                // 更新 count 数组
                count[arr[p].id] += j - mid - 1;
            } else if (temp[i].val > temp[j].val) {
                arr[p] = temp[j++];
            } else {
                arr[p] = temp[i++];
                // 更新 count 数组
                count[arr[p].id] += j - mid - 1;
            }
        }
    }
}

Because the index position of each element changes continuously during the sorting process, we encapsulate each element and its index in the original array nums with a Pair class, so that the count array records the number of elements that are smaller than it after each element .

Now look back and realize what I said at the beginning of this article:

All recursive algorithms essentially traverse a (recursive) tree and execute code at nodes (pre-, post-, post-order positions). You're writing a recursive algorithm, essentially telling each node what to do .

Do you have any taste?

Finally, to sum up, this article talks about the core idea and code implementation of merge sort from the perspective of binary tree, and also talks about an algorithm problem related to merge sort. This algorithm problem is actually a bit of private goods mixed in the logic of the merge sort algorithm, but it is still relatively difficult, and you may need to do it yourself to understand it.

Then I will leave a question at the end. In the next article, I will talk about quick sorting. Can you try to understand quick sorting from the perspective of binary tree? If you were asked to summarize the logic of quicksort in one sentence, how would you describe it?

Well, the answer will be revealed in the next article.

Click on my avatar view more high-quality algorithm articles, and take you by the hand to make the algorithm clear! My algorithm tutorial has won 100k stars, welcome to like it!

labuladong
63 声望37 粉丝