6
头图

Since the SF Markdown editor cannot support label images, the article images cannot be displayed. For a better reading experience, you can go to the recommended reading address: click to jump

In intensive reading "DOM diff principle" article, we mentioned that Vue uses a greedy + bisection algorithm to find the longest ascending subsequence, but did not delve into the principle of this algorithm, so a special chapter is opened up to explain it in detail.

In addition, as an algorithmic problem, the longest ascending subsequence is very classic. At the same time, it is practical and difficult in the industry, so I hope everyone must master it.

intensive reading

What is the longest ascending subsequence? It is to find the longest continuously rising part in an array, as shown in the following figure:

<img width=400 src="https://img.alicdn.com/imgextra/i4/O1CN01VAEEcg25wjCsjsQAj_!!6000000007591-2-tps-900-310.png">

If the sequence itself is ascending, then directly return itself; if no segment of the sequence is ascending, any number can be returned. As you can see in the figure, although 3, 7, 22 is also rising, because 22 can't be connected, its length is 3, 3, 7, 8, 9, 11, 12 , so it is not easy to find.

In a specific DOM diff scenario, in order to ensure that as little DOM is moved as possible, we need keep the longest ascending subsequence unchanged, and only move other elements. why? Because the longest ascending subsequence itself is relatively orderly, as long as the other elements are moved, the answer will come out. Or this example, assuming that the original DOM is in such an increasing order (of course, it should be a continuous subscript of 1 2 3 4, but it does not affect the algorithm whether the interval is continuous, as long as it is incremented):

<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN01QH5F8j23hxCVcnYgx_!!6000000007288-2-tps-910-140.png">

If you keep the longest ascending subsequence unchanged, you only need to move three times to restore:

<img width=400 src="https://img.alicdn.com/imgextra/i2/O1CN01m9E97v1Fv1iUJ2ZQm_!!6000000000548-2-tps-934-146.png">

Any other moving method will not be less than three steps, because we have kept the orderly part as .

Then the question is, how to find this longest ascending subsequence? Solutions that are easier to think of are: violence and dynamic programming.

Violent solution

Time complexity: O(2ⁿ)

We finally want to generate the longest subsequence length, so let's simulate the process of generating this subsequence, but this process is violent.

How to generate a sub-sequence by violent simulation? It is to try to select or not select the current number every time from the range of [0,n], provided that the number selected later is larger than the previous one. Since the length of the array is n, each number can be selected or not, that is, there are two choices for each number, so at most 2ⁿ results will be generated, and the longest length is found from the answer:

<img width=500 src="https://img.alicdn.com/imgextra/i2/O1CN01xdPLqX1uNxMiu1Kzn_!!6000000006026-2-tps-1166-1132.png">

If you keep trying so stupidly, you will definitely be able to try the longest section, and record the longest section in the traversal process.

Because this method is too inefficient, it is not recommended, but this kind of violent thinking still needs to be mastered.

Dynamic programming

Time complexity: O(n²)

If this problem is considered with the idea of dynamic programming, the definition of DP(i) according to experience is: the length of the longest subsequence at the end of the i-th string.

There is an experience here, that is, the general DP return value is the answer. String questions often end with the i-th string, so you can scan it once. Moreover, the longest sub-sequence has repeated sub-questions, that is, the i-th answer calculation includes some of the previous calculations. In order to avoid repeated calculations, dynamic programming is used.

Then it depends on which results of the i-th item are related to the previous results, as shown in the figure for the convenience of understanding:

<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01qqNHXb1VnjG4iMhwQ_!!6000000002698-2-tps-900-164.png">

Suppose we look at the number 8, which is what DP(4) is. Since the previous DP(0), DP(1) ... DP(3) have been calculated at this time, let's see what is the relationship between DP(4) and the previous calculation results.

A simple observation can be found that if nums[i] > nums[i-1] , then DP(i) is equal to DP(i-1) + 1 , this is obvious, that is, if 8 is greater than 4, then the answer at position 8 is the answer length at position 4 + 1, if at position 8 The value is 3, less than 4, then the answer is 1, because the previous one does not satisfy the upward relationship, so I can only use the number 3 to fight alone.

But if you think about it, you will find that this subsequence does not have to be continuous. In case the i-th item is combined with the i-2, i-3 item, it may be longer than the i-1 item combined? We can give a counter example:

<img width=250 src="https://img.alicdn.com/imgextra/i4/O1CN01W1uPCR1sWXYUvgQgz_!!6000000005774-2-tps-510-164.png">

Obviously, 1, 2, 3, 4 combined is the longest ascending subsequence. If you only look at 5, 4 , then the answer can only be 4 .

It is precisely because of this discontinuity that we need to compare the i-th item with the j-th item in turn. Among them, j=[0,i-1] . Only when it is compared with all the previous items, we can rest assured that the result found for the i-th item is indeed the longest:

<img width=400 src="https://img.alicdn.com/imgextra/i4/O1CN01wQ4iDy1BvFRejScwt_!!6000000000007-2-tps-918-676.png">

So how is the time complexity calculated? Dynamic programming solution, we first circulating from 0 to n, and wherein for each i, do it again [0,i-1] additional cycle, the number of computations is 1 + 2 + ... + n = n * (n + 1) / 2 , after excluding a constant magnitude is O (n²).

Greedy + Dichotomy

Time complexity: O(nlogn)

To be honest, it is generally good to think of a dynamic programming solution, and it is very difficult to further optimize the time complexity. If you haven't done this problem before and want to challenge it, you can stop here.

Okay, the answer is announced. To be honest, this method is not like normal human thinking. It has a big thinking jump, so I can't give the thinking derivation process. Let's just say the conclusion: greedy + dichotomy.

If we have to say what we think, we can discuss it with hindsight from the time complexity. Generally, the time complexity of n² will become nlogn after optimization, and the time complexity of a binary search is logn, so we desperately think of ways to combine Right.

The specific plan is just one sentence: uses a stack structure. If the value is larger than all the values in the stack, it is pushed into the stack, otherwise the smallest number larger than it is replaced. The final stack length is the answer :

<img width=500 src="https://img.alicdn.com/imgextra/i4/O1CN01f1Ovif1vabvAE1yU0_!!6000000006189-2-tps-1058-1060.png">

First explain the time complexity. Because of the operation reasons, the numbers stored in the stack are in ascending order, so the dichotomy can be used to compare and insert. The complexity is logn, and the outer layer is n cycles, so the overall time complexity is O(nlogn) . Another problem with this solution is that the length of the answer is accurate, but the array in the stack may be wrong. If you want to fully understand this sentence, you have to fully understand the principle of this algorithm, and only after understanding the principle can you know how to improve to get the correct subsequence.

Next, we need to explain the principle. is not complicated. You can watch while drinking tea. First of all, we must have an intuitive understanding, that is, in order to make the longest ascending subsequence as long as possible, we must ensure that the growth rate of the selected number is as slow as possible, and vice versa. For example, if the number we choose is 0, 1, 2, 3, 4 then this kind of greed is relatively stable, because it has grown as slowly as possible, and the high probability encountered later can be included. But if we choose a 0, 1, 100 that pick 100 time in relation to panic, because it increases 100 , behind 100 numbers within it are not giving up yet? At this time, 100 not necessarily a wise choice. If it is lost, the future space may be larger. This is actually greedy thinking. The so-called local optimal solution is the global optimal solution.

But the above idea is obviously incomplete, we continue to think, if there is no number after 0, 1, 100 100 can still be put in, although 100 very large, it is the last one, and it is still useful. So when traversing from left to right, if you encounter a larger number, you should put . The point is, if you continue to read later and read a number smaller than 100 , what should I do?

If you can't make a jump in thinking here, the analysis can only stop here. You may think you can continue to analyze. For example 5 , you obviously have to 100 out, because the 0, 1, 5 and 0, 1, 100 are 3, but 0, 1, 5 is obviously greater than that of 0, 1, 100 , so the length remains the same, and one potential Bigger, it must be replaced! 3, 7, 11, 15 , but in another scenario, if you encounter 0607d69c26137e, then you encounter 9 , how do you change it? If considering 3, 7, 9 the best potential, but the length is sacrificed from 4 to 3, and you don’t know 9 . If not, this length is not better than the original 4 3, 7, 11, 15 because of length considerations, 10, 12, 13, 14 will be dumbfounded in case of a few consecutive 0607d69c261387, and it will feel short-sighted.

So the question is, how to deal with the next number so as not to be short-sighted in the future, and to "grasp the stable happiness". Jumping thinking began to appear here. , the answer is "If the value is greater than all the values in the stack, then push it into the stack, otherwise replace the smallest number larger than it". This reflects the leap thinking, the core of realizing both the present and the future is: sacrifices the correctness of the stack content, ensuring that the total length is correct, every step can seize the best opportunity in the future. is correct, can we guarantee the longest sequence. As for sacrificing the correctness of the stack content, we did pay a lot of price, but in exchange for the possibility of the future, at least the length can get the correct result, if the content If it is correct, it can be solved by adding some auxiliary means, which will be discussed later. So in general, this sacrifice is very worthwhile. The following figure introduces why sacrificing the correctness of the stack content can bring the correct length and seize future opportunities.

Let's take an extreme example: 3, 7, 11, 15, 9, 11, 12 . If you stick to the 3, 7, 11, 15 found at the beginning, the length is only 4, but if you give up 11, 15 and connect 3, 7, 9, 11, 12 , the length is better. According to the greedy algorithm, we will first encounter 3 7 11 15 . Since each number is larger than the previous one, there is nothing to think about, so we just stuff it into the stack:

<img width=300 src="https://img.alicdn.com/imgextra/i3/O1CN01SSCBG51IOSOiCPOUI_!!6000000000883-2-tps-704-256.png">

encountered 9 when wonderful , this time 9 not the biggest, we are secure in order to seize happiness, we simply than 9 slightly larger 11 replaced, so what would be the result?

<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01ACsWDj27OUq1oFzOa_!!6000000007787-2-tps-704-360.png">

First of all, the length of the array has not changed because the replacement operation will not change the length of the array. At this time, if 9 is no value after 0607d69c2614ba, we are not at a loss. At this time, the output length of 4 is still the best answer. We continue, the next step we encounter 11 , we still replace the 15

<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN01ihQKvo1UxyVHA8rOe_!!6000000002585-2-tps-704-456.png">

At this point we replaced the last digit, find 3, 7, 9, 11 finally is a reasonable sequence of, and the length and 3, 7, 11, 15 the same, but more potential , next 12 naturally should put the final, got the final answer: 5.

Here we did not actually say clearly the essence of this algorithm, we return to 3, 7, 9, 15 this step, figuring out 9 Why can replace 11 .

Assuming that 9 followed by a large 99 , then the next step 99 will be directly appended to the back:

<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN01qYv5tB27FnJJreD16_!!6000000007768-2-tps-604-458.png">

At this time we got 3, 7, 9, 15, 99 , but if you look carefully, you will find that 9 in the 15 , because our insertion caused 9 to be placed in 15 , so this is obviously not the correct answer, but the length is correct. , Because this answer is equivalent to choosing 3, 7, 11, 15, 99 ! Why is it so understandable? Because not replaced to the last number, the queue in our mind is actually the original queue.

<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN011YrND21QyecxRUvu7_!!6000000002045-2-tps-604-610.png">

That is, as long as the stack is not replaced, the newly inserted value will always only serve as a placeholder. The purpose is to make the new value easy to insert, but if there is really no new value to insert, then although the stack The content is wrong, but at least the length is correct, because 9 9 when it is not replaced, it is just a placeholder, and the value behind it is still 11 . So no matter how to change, as long as the last one is not replaced, this replacement operation is invalid. Let's take another example:

<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01vcMrcW1aChJSLWlYW_!!6000000003294-2-tps-904-652.png">

It can be seen that 1, 2, 3, 4 cannot 7, 8, 9, 10, 11 , so the final result is 1, 2, 3, 4, 11 , but it does not matter, as long as the replacement is not completed, the answer is 7, 8, 9, 10, 11 , but we did not record it, but only looking at the length, there is no difference between the two. So there is no problem. What if 1, 2, 3, 4, 5, 6 ? Let's see what happens when we can replace it:

<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN013K3Ta51FrMXxKypfY_!!6000000000540-2-tps-1102-842.png">

It can be seen that when 5 is replaced, the sequence order is correct, because 1, 2, 3, 4, 5 can completely replace 7, 8, 9, 10, 11 , and the potential is greater than it, and we have found the optimal local solution. Therefore, 1, 2, 3, 4, 11 in 1, 2, 3, 4 like an undercover 11 still there, I still swallow 7, 8, 9, 10, 11 as the boss (in fact, 1 calls 7 as boss, 2 calls 8 as boss, but when 5 , 1, 2, 3, 4, 5 can be 7, 8, 9, 10, 11 , because its strength has exceeded the original boss strength.

The seemingly insignificant replacement in front of us is actually to constantly search for the best possible solution in the future, until the day when there is a bright day, if there is no bright day, it would be good to be a little brother, the length is still right; if there is The maximum length is updated in the early days, so this greed can take into account correctness and efficiency at the same time.

Finally, let’s see how to find the correct sequence while finding the answer?

In fact, after reading this, you should be able to guess it. As mentioned earlier, replaces the last one or inserts, the stack order is correct . So we can store a copy of the current stack when replacing the last one or inserting, so that the last remaining copy is the final correct order.

Why is it so? We finally use an example to strengthen our understanding, because we are already very proficient, so the first few steps are combined:

<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01Mi7fPY1FLlDhiuGSC_!!6000000000471-2-tps-1200-344.png">

So far, 7, 8, 9, 13 does not exist, but it actually refers to 10, 11, 12, 13 , which has been explained before, so I won’t repeat it. We have already stored the queue 10, 11, 12, 13 at this time, so when it is over, the output of this queue is correct. We look at the next step:

<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01ZtAMAR1V30rKrchB2_!!6000000002596-2-tps-1204-444.png">

In order to facilitate identification, I added background colors to different grouped numbers, so that it is easier to observe: we found that since each replacement is a slightly larger number, once we encounter a smaller start 1, 2, 3, 4, 5 , even in the previous round 7, 8, 9 has not completely replaced 10, 11, 12, 13 , and the smaller ones must be replaced from the leftmost side, because the numbers in the stack increase monotonically. Then after replacing them all, or starting from a certain number and replacing them to the right, the numbers in the queue must be in the correct relative order. From the example here, 2, 3 will replace 8, 9 . When 13 is replaced, the relative order of the stack must conform to the relative order of the original array.

Finally, look at a more complex example to deepen the impression:

<img width=400 src="https://img.alicdn.com/imgextra/i1/O1CN01GXWX6G1jaoiMJWC9h_!!6000000004565-2-tps-1102-768.png">

After reading this, congratulations, you’re done, and you fully understand this DOM diff algorithm.

to sum up

So how much did Vue pay for the greedy calculation of the longest ascending subsequence? In fact, it is the relationship between O(n) and O(nlogn), we look at the picture:

<img width=500 src="https://img.alicdn.com/imgextra/i3/O1CN01ztHvIs1azFIPVTzJY_!!6000000003400-2-tps-1200-824.png">

It can be seen that the increasing trend of O(nlogn) time complexity is barely acceptable, especially in engineering scenarios, when the number of child nodes of a parent node is unlikely to be too many, it will not take up too much analysis time. The benefit is the minimum number of DOM moves. It is a perfect combination of algorithm and engineering practice.

The discussion address is: " · Issue #310 · dt-fe/weekly

If you want to participate in the discussion, please click here , a new theme every week, weekend or Monday. Front-end intensive reading-to help you filter reliable content.

Follow front-end intensive reading WeChat public number

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Freely reprinted-non-commercial-non-derivative-keep the signature ( Creative Commons 3.0 License )

黄子毅
7k 声望9.6k 粉丝