4

-----------

When I first opened this public account two years ago, I wrote a learning data structure and algorithm . Now I have read more than 5w, which is very awesome data for a purely technical article.

In the past two years, in the process of constantly writing questions, thinking and writing public accounts, my understanding of algorithms has gradually deepened, so today I will write another article to condense my experience and thinking of the past two years into 4000 words. Share with everyone.

This article has two main parts, one is to talk about my understanding of the nature of the algorithm, and the other is to summarize various commonly used algorithms. There is no hard-core code in the full text. It is all my experience. It may not be too tall, but it will definitely help you avoid detours and understand and master the algorithm more thoroughly.

In addition, this article contains a large number of links to historical articles. Reading historical articles in conjunction with this article may be able to develop the framework thinking and knowledge system of learning algorithms more quickly.

The essence of the algorithm

If you want me to summarize in one sentence, I want to say that the essence of the algorithm is "exhaustive" .

That said, someone must refute it. Are all algorithmic problems really exhaustive in nature? Is there no exception?

An exception is definitely yes, like a few days ago I made a line of code algorithm can solve problems , these questions are through observation, found that the law, and then find the optimal solution.

Another example is mathematics-related algorithms, many of which are mathematical inferences and then expressed in the form of programming, so their essence is mathematics, not computer algorithms.

From the point of view of computer algorithms, combined with the needs of most of us, this kind of pure skill for showing IQ is absolutely a minority. Although it is easy for people to shout for exquisiteness, it can't extract the general thinking of thinking about algorithmic problems. On the contrary, the real general thinking From the avenue to the simplicity, it is exhaustive.

I remember that when I first started learning algorithms, I also thought that algorithms were very big things. Every time I saw a question, I wondered if I could derive a mathematical formula, and then I could figure out the answer with a snap.

For example, you told a person who has never studied (computer) algorithms that you wrote an algorithm to calculate permutations and combinations. He probably thought you invented a formula that can directly calculate all permutations and combinations. But actually? There is no permutation and combination subset problem previous article, but the backtracking algorithm must be used to brute force.

The misunderstanding of computer algorithms may be a "sequelae" of previous mathematics studies. Generally, you can observe carefully, find geometric relations, formulate equations, and then calculate the answers to math problems. If you need to perform a large-scale exhaustion to find the answer, then there is a high probability that your problem-solving idea is wrong.

The computer’s way of solving problems is just the opposite. If there are any mathematical formulas, leave it to you humans to derive, but if you can’t derive them, then exhaust them. Anyway, as long as the complexity allows, there are no answers that cannot be exhaustive. .

How do you find the maximum and minimum algorithm questions for the technical post written examination? You have to enumerate all feasible solutions to find the best value.

"Exhaustion" can be specifically divided into two points. Seeing an algorithm problem, you can think about from these two dimensions:

1. How to exhaust ?

2. How to wisely enumerate ?

Different types of questions have different difficulties. Some questions are difficult to "how to exhaust", and some are difficult to "how to wisely exhaust".

What algorithm is difficult to "how to exhaust"? Generally recursive problems, the most typical is the dynamic programming series of problems .

The previous article dynamic programming core routine explained the core principles of the dynamic programming series of problems, nothing more than to write a brute force exhaustive solution (state transition equation), add a memo to become a top-down dynamic programming solution, and then modify it. Change it to a bottom-up iterative solution, dynamic programming dimensionality reduction strike also talked about how to analyze and optimize the space complexity of the dynamic programming algorithm.

The above process is constantly optimizing the time and space complexity of the algorithm, which is the so-called "how to exhaustively intelligently". These skills will be easy to listen to. However, many readers left messages to clarify these principles, and they still would not do it when encountering dynamic programming problems, because the first violent solution could not be written.

This is normal, because dynamic programming types of topics can be weird, and finding the state transition equation is difficult, so there is the dynamic programming design method: the longest increasing subsequence This article tells you that the core of recursive exhaustion is mathematics Inductive method, clear the definition of the function, and then use this definition to write a recursive function, you can exhaust all feasible solutions.

What is the difficulty of the algorithm "how to do it wisely"? Some familiar non-recursive algorithm techniques can be classified in this category .

For example, the previous Union Find and the detailed explanation of the collection algorithm tell you a technique for efficiently calculating connected components. Theoretically, I want to judge whether two nodes are connected. I use DFS/BFS brute force search (exhaustive). However, the Union Find algorithm just uses an array to simulate the tree structure, giving you the complexity of operations related to connectivity to O(1) .

This is a clever exhaustive list, and you will use it after you have learned it. I am afraid it is difficult to come up with this kind of thinking if you have not learned it.

Another example is the greedy algorithm technique. In the previous article when the old driver learns the greedy algorithm will tell you that the so-called greedy algorithm is to find some rules in the problem (professional point is called the greedy selection property), so that you can get it without exhaustively exhausting all the solutions. Answer.

Someone else’s dynamic programming is to exhaust all the solutions without redundancy, and then find the best value. Your greedy algorithm is good, you can find the answer without exhausting all the solutions, so the previous greedy algorithm solves the greedy algorithm in the jumping game Efficiency is higher than dynamic programming.

Another example is the famous KMP algorithm. It is easy for you to write a string brute force matching algorithm, but you invent a KMP algorithm to try? The essence of the KMP algorithm is to cache and reuse some information intelligently, reducing redundant calculations. The preceding KMP character matching algorithm is the KMP algorithm implemented using the idea of a state machine.

Below I outline some common algorithm techniques for your reference.

Array/singly linked list series algorithm

single-linked list frequently tested skills is the double pointer , the previous single-linked list six skills all summed up for you, these skills are not difficult for those who know, and those who are difficult will not.

For example, to determine whether a singly linked list is in a ring, what is the violent solution to slap your head? It is to use a HashSet to cache the passed nodes. If you encounter duplicates, it means there is a ring, right. But we can avoid using extra space by using fast and slow pointers, which is a clever exhaustion.

Of course, for the problem of finding the midpoint of a linked list, using the double pointer technique just shows that you have learned this technique. It is similar to the conventional solution of traversing the linked list twice from the perspective of time and space complexity.

array of commonly used skills, a large part of it is related to double pointer skills, to put it is to teach you how to cleverly exhaust 16177c3a3771f4.

first talk about the binary search technique , which can be classified as a double pointer with both ends to the center. If you are asked to search for elements in an array, a for loop will definitely do it, right, but if the array is ordered, isn't binary search a smarter way to search.

The previous binary search framework detailed explanation summarizes the binary search code template for you to ensure that there will be no search boundary problems. The previous binary search algorithm uses to summarize the commonalities of binary search related topics and how to apply the binary search idea to the actual algorithm.

Similar to the two-pointer technique with both ends to the center, there is also a series of problems with the sum of N numbers. The previous one function kills all nSum problems. talks about the commonality of these problems. Regardless of the sum of the numbers, the solution must be exhaustive. All combinations of numbers, and then see if the sum of that number combination is equal to the target sum. The smarter way is to sort first and use the double pointer technique to quickly calculate the result.

Let me talk about sliding window algorithm techniques , a typical fast and slow double pointer, in the middle of the fast and slow pointer is a sliding "window", mainly used to solve the substring problem.

The question of the smallest covering substring in the article allows you to find the shortest substring that contains a specific character. What is the conventional solution? It must be similar to a string brute force matching algorithm, using nested for loops to exhaustively, with square-level complexity.

The sliding window technique tells you that you don't have to be so troublesome. You can find the answer by traversing it once with the speed pointer. This is an exhaustive technique to teach you cleverness.

However, just like binary search can only be applied to ordered arrays, sliding windows also have their limitations, that is, you must clearly know when to expand the window and when to shrink the window.

For example, the maximum sub-array problem faces the problem that there is no way to use the sliding window, because the elements in the array have negative numbers. Expanding or shrinking the window does not guarantee that the sum of the elements in the window will increase and decrease, so The sliding window technique cannot be used, but the dynamic programming technique can only be exhausted.

also has a palindrome related technique . If you judge whether a string is a palindrome, use a double pointer to check from both ends to the center. If you find a palindrome substring, spread from the center to both ends. In the previous article, longest palindrome substring uses a technique to deal with the case where the palindrome is odd or even in length at the same time.

Of course, there is a more sophisticated horse-drawn cart algorithm (Manacher algorithm) for finding the longest palindrome substring. However, the cost-effectiveness of learning this algorithm is not high, and there is no need to master it.

finally talk about the prefix and techniques and differential array techniques .

preSum to use a for loop to traverse each time, but the prefix and tricks pre-calculate a 06177c3a377879 array to avoid the loop.

Similarly, if you frequently increase and decrease sub-arrays, you can also use a for loop to operate each time, but the difference array technique maintains a diff array, which can also avoid loops.

The skills of the array linked list are almost the same. They are relatively fixed. As long as you have seen them, the difficulty of using them is not too big. Let's talk about the slightly more difficult algorithms.

Binary Tree Series Algorithm

Old readers know that I have talked about the importance of binary trees countless times before, because the binary tree model is almost the basis of all advanced algorithms, especially when so many people say that they do not have a good understanding of recursion, and they should brush up on binary tree related topics.

I said before that the recursive solution to the binary tree problem can be divided into two types of ideas. The first type is to traverse the binary tree to get the answer, and the second type is to calculate the answer by decomposing the problem. These two types of ideas correspond to the backtracking algorithm. The framework and dynamic programming core framework .

by traversing the binary tree once?

For example, the problem of calculating the maximum depth of a binary tree allows you to implement maxDepth . You can write code like this:

// 记录最大深度
int res = 0;
int depth = 0;

// 主函数
int maxDepth(TreeNode root) {
    traverse(root);
    return res;
}

// 二叉树遍历框架
void traverse(TreeNode root) {
    if (root == null) {
        // 到达叶子节点
        res = Math.max(res, depth);
        return;
    }
    // 前序遍历位置
    depth++;
    traverse(root.left);
    traverse(root.right);
    // 后序遍历位置
    depth--;
}

This logic is to use the traverse traverse all the nodes of the binary tree, maintain the depth variable, and update the maximum depth when the leaf node is used.

Do you feel familiar with this code? Can it correspond to the code template of the backtracking algorithm?

If you don’t believe me, follow the code comparison of the full arrangement problem in backtracking algorithm core framework

// 记录所有全排列
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    backtrack(nums);
    return res;
}

// 回溯算法框架
void backtrack(int[] nums) {
    if (track.size() == nums.length) {
        // 穷举完一个全排列
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (track.contains(nums[i]))
            continue;
        // 前序遍历位置做选择
        track.add(nums[i]);
        backtrack(nums);
        // 后序遍历位置取消选择
        track.removeLast();
    }
}

When I talked about the backtracking algorithm in the previous article, I told you that the essence of the backtracking algorithm is to traverse a multi-branch tree. Is the code implementation the same?

And I have often said before that although the backtracking algorithm is simple and rude and inefficient, it is particularly useful, because if you have nothing to do with a problem, the backtracking algorithm can at least help you write a violent solution.

calculate the answer 16177c3a377b0b by decomposing the question?

For the same problem of calculating the maximum depth of a binary tree, you can also write the following solution:

// 定义:输入根节点,返回这棵二叉树的最大深度
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;
}

Do you feel familiar with this code? Do you feel a bit of a form of dynamic programming solution code?

If you don’t believe me, look at the violent and exhaustive solution to the change problem in the dynamic programming core framework

// 定义:输入金额 amount,返回凑出 amount 的最少硬币个数
int coinChange(int[] coins, int amount) {
    // base case
    if (amount == 0) return 0;
    if (amount < 0) return -1;

    int res = Integer.MAX_VALUE;
    for (int coin : coins) {
        // 递归计算凑出 amount - coin 的最少硬币个数
        int subProblem = coinChange(coins, amount - coin);
        if (subProblem == -1) continue;
        // 凑出 amount 的最少硬币个数
        res = Math.min(res, subProblem + 1);
    }

    return res == Integer.MAX_VALUE ? -1 : res;
}

This violent solution plus a memo is a top-down dynamic programming solution. Compared with the solution code of the maximum depth of the binary tree, do you find that it is similar?

If you feel the difference between the two solutions to the problem of maximum depth, then strike while the iron is hot. I ask you, how do you write preorder traversal of the binary tree?

I believe everyone will sneer at this question, and you can write the following code without hesitation:

List<Integer> res = new LinkedList<>();

// 前序遍历函数
List<Integer> preorder(TreeNode root) {
    traverse(root);
    return res;
}

// 二叉树遍历函数
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序遍历位置
    res.addLast(root.val);
    traverse(root.left);
    traverse(root.right);
}

However, if you combine the two different thinking modes mentioned above, can the traversal of the binary tree also be solved by the idea of decomposing the problem?

In the previous article hand-to-hand brushing the binary tree (second period) said the characteristics of the front, middle and post order traversal results:

You pay attention to the result of the preorder traversal, the value of the root node is in the first place, followed by the preorder traversal result of the left subtree, and finally the preorder traversal result of the right subtree .

Have you experienced anything? In fact, you can completely rewrite the pre-order traversal code and write it out in the form of a decomposition problem to avoid external variables and auxiliary functions:

// 定义:输入一棵二叉树的根节点,返回这棵树的前序遍历结果
List<Integer> preorder(TreeNode root) {
    List<Integer> res = new LinkedList<>();
    if (root == null) {
        return res;
    }
    // 前序遍历的结果,root.val 在第一个
    res.add(root.val);
    // 后面接着左子树的前序遍历结果
    res.addAll(preorder(root.left));
    // 最后接着右子树的前序遍历结果
    res.addAll(preorder(root.right));
}

You see, this is to write the pre-order traversal of the binary tree in the mode of thinking of the decomposition problem. If you write the middle-order and post-order traversal, it is similar.

Of course, the dynamic programming series of problems have two characteristics: "optimal substructure" and "overlapping subproblems", and most of them let you find the best value. Although many algorithms do not belong to dynamic programming, they also conform to the thinking model of decomposition problems.

For example, the divide-and-conquer algorithm detailed explanation said in the priority problem of arithmetic expressions, the core is still the decomposition of the big problem into sub-problems, but there is no overlapping sub-problems, you can not use memos to optimize efficiency.

Of course, in addition to dynamic return, backtracking (DFS), and divide and conquer, there is also a common algorithm called BFS. The previous BFS algorithm core framework is modified according to the following binary tree traversal code:

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);

    int depth = 1;
    // 从上到下遍历二叉树的每一层
    while (!q.isEmpty()) {
        int sz = q.size();
        // 从左到右遍历每一层的每个节点
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();

            if (cur.left != null) {
                q.offer(cur.left);
            }
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
        depth++;
    }
}

goes a step further, the algorithm related to graph theory is also the continuation of the binary tree algorithm .

For example, graph theory foundation and ring judgment and topological sorting use the DFS algorithm; another example is Dijkstra algorithm template , which is a modified version of the BFS algorithm plus an array similar to dp table.

Okay, I'm almost done. The essence of the above-mentioned algorithms is an exhaustive two (multiple) fork tree. If you have the opportunity, you can reduce redundant calculations and improve efficiency by pruning or memos. That's it.

Final summary

During the live broadcast on the video account last week, some readers asked me what method of answering questions is correct. I said that the correct method of answering questions should be one question to get the effect of 10 questions. Are you going to finish it?

So how to do it? Learn the framework thinking of data structures and algorithms. said that you must have a framework thinking and learn to refine the key points. An algorithm technique can pack a hundred questions. If you can see through its essence at a glance, then there is no need to waste time brushing it. Well.

At the same time, you must think and make associations when doing questions, and then cultivate the ability to draw inferences from one another.

The previous Dijkstra algorithm template not really for you to memorize the code template. Otherwise, you can just throw out that piece of code. I talked about the sequence traversal from BFS to Dijkstra. Why do you talk about so much nonsense?

After all, I still hope that thinking readers can develop systematic algorithmic thinking. It is best to fall in love with algorithms instead of just looking at the solution of the problem to solve the problem. It is better to teach people how to fish than to teach people how to fish.

That's it for this article. The algorithm is really not that difficult. Anyone can learn well as long as they are willing. Sharing is a virtue. If this article is enlightening to you, please share it with friends in need.

_____________

View more high-quality algorithm articles Click on my avatar , and I will take you through the buckle by hand, dedicated to making the algorithm clear! My algorithm tutorial has won 90k stars, welcome to like it!

labuladong
63 声望37 粉丝