本文主要解析Vue3 Dom Diff中的核心算法:最長遞增子序列,不對diff細節作解析。git
維基百科:最長遞增子序列(longest increasing subsequence)是指,在一個給定的數值序列中,找到一個子序列,使得這個子序列元素的數值依次遞增,而且這個子序列的長度儘量地大。最長遞增子序列中的元素在原序列中不必定是連續的。
最長遞增子序列是動態規劃算法裏比較經典的一個例子,看完wiki的解釋,其實也是有點懵的,能夠看看LeetCode - 最長遞增子序列裏舉出的例子。github
arr = [10, 9, 2, 5, 3, 7, 101, 18] arr的最長遞增子序列是 [2, 5, 7, 101]、[2, 5, 7, 18]、[2, 3, 7, 101]、[2, 3, 7, 18]
在LeetCode中求解的是最長遞增子序列的長度。
在Vue3 diff算法中,求解出來的是序列的下標,好比上述例子,求得的結果是[2, 4, 5, 7]
。
Vue3的求解中,用的是動態規劃 + 貪心算法 + 二分查找 + 回溯結合。咱們能夠經過LeetCode的例子,使用兩種解法來了解一下動態規劃/貪心算法/二分查找。算法
維基百科:動態規劃經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法,經常適用於有重疊子問題和最優子結構性質的問題。
簡單來講就是將複雜的問題分解爲相同的子問題,求解子問題後將其存起來,根據子問題之間的關係逐步求解。
深刻了解: 五大基本算法之動態規劃算法
Flow數組
元素遍歷詳細過程ui
Codespa
var lengthOfLIS = function(nums) { if (nums.length === 0) return 0; // 生成對應的數組,存儲與之對應的最長子序列的長度 // dp = [1, 1, 1, 1, 1, 1, 1, 1] let dp = Array.from(Array(nums.length), () => 1); // 遍歷數組,判斷到了當前位置時,長度爲多少 for (let i = 0; i < nums.length; i++) { // 遍歷當前位置以前全部存儲過的長度 for (let j = 0; j < i; j++) { // 判斷當前位置num是否比前面的num大,大的話就在其長度上+1,並取最大值 nums[i] > nums[j] && (dp[i] = Math.max(dp[i], dp[j] + 1)) } } // 返回最長的長度 return Math.max(...dp) };
貪心算法:也叫作貪婪算法,在每一步作選擇時,老是選擇當前最優的方法。
舉個栗子:假若有一個揹包,最多裝50kg物品,有如下重量物品:28g、12g、8g、5g、4g。求解怎麼裝可以裝到最重的物品?按照貪心算法,則是每次選擇當前可裝的最重的物品,依次是:28g + 12g + 8g = 48g。以後就沒法再裝入揹包了,但實際咱們知道揹包最多可裝49g。貪心算法主要是追求局部最優解,而不必定是全局最優解。
Flowcode
元素遍歷詳細過程leetcode
Codeget
var lengthOfLIS = function(nums) { if (nums.length === 0) return 0; let result = [nums[0]]; for (let i = 1; i < nums.length; ++i) { // 若是當前數值大於已選結果的最後一位,則直接日後新增,若當前數值更小,則直接替換前面第一個大於它的數值 if (nums[i] > result[result.length - 1]) { result[result.length] = nums[i]; } else { // 二分查找:找到第一個大於當前數值的結果進行替換 let left = 0, right = result.length - 1; while (left < right) { let middle = ((left + right) / 2) | 0; if (result[middle] < nums[i]) { left = middle + 1; } else { right = middle; } } // 替換當前下標 result[left] = nums[i]; } } return result.length; };
前面說過,Vue3解出來的不是子序列長度,也不是最終的子序列數組,而是子序列對應的下標(爲何是下標能夠參考另外一篇Vue3 Dom Diff源碼解析)。好比[10, 9, 2, 5, 3, 7, 101, 18]
解出來的是[2, 4, 5, 7]
。經過前面兩個方法,感受這個求解已經很簡單了。使用貪心算法,將result存數值的下標,最後return result
拿到最終子序列數組。可是確定不是這樣簡單,來看看這個例子,直接看最後一步就好:源碼
經過最後結果的下標也能夠發現,最後一位最小,可是跑到前面去了,若是是求長度,這也是沒問題的,可是要拿到最後的結果,很明顯是不符合咱們想要的結果的。Vue3在計算時一樣適用了這個算法,並中使用回溯巧妙的解決了這個問題。
flow
元素遍歷詳細過程
目前獲得的是有誤的數據,因此須要經過p記錄的全部前一位的值去回溯。直接從最後一位開始,將前面的result所有覆蓋,若是不須要修正,則p中記錄的每一項都是對應的前一位,不會有任何影響。若是須要修正,則會將第一次記錄的正確的前一位值覆蓋。
code
function getSequence(arr) { const p = arr.slice() const result = [0] let i, j, u, v, c const len = arr.length // 遍歷數組 for (i = 0; i < len; i++) { const arrI = arr[i] // 此算法中排除了等於0的狀況,緣由是0成爲了diff算法中的佔位符,在上面的流程圖中已經忽略了,不影響對算法的瞭解 if (arrI !== 0) { j = result[result.length - 1] // 用當前num與result中的最後一項對比 if (arr[j] < arrI) { // 當前數值大於result子序列最後一項時,直接日後新增,並將當前數值的前一位result保存 p[i] = j result.push(i) continue } u = 0 v = result.length - 1 // 當前數值小於result子序列最後一項時,使用二分法找到第一個大於當前數值的下標 while (u < v) { c = ((u + v) / 2) | 0 if (arr[result[c]] < arrI) { u = c + 1 } else { v = c } } if (arrI < arr[result[u]]) { // 找到下標,將當前下標對應的前一位result保存(若是找到的是第一位,不須要操做,第一位前面沒有了) if (u > 0) { p[i] = result[u - 1] } // 找到下標,直接替換result中的數值 result[u] = i } } } u = result.length v = result[u - 1] // 回溯,直接從最後一位開始,將前面的result所有覆蓋,若是不須要修正,則p中記錄的每一項都是對應的前一位,不會有任何影響 while (u-- > 0) { result[u] = v v = p[v] } return result }
參考資料:
https://jishuin.proginn.com/p...
https://leetcode-cn.com/probl...
https://houbb.github.io/2020/...![]()