Vue3 Diff算法之最长递增子序列,学不会来砍我!
專欄分享:vue2源碼專欄,vue3源碼專欄,vue router源碼專欄,玩具項目專欄,硬核??推薦??
歡迎各位ITer關注點贊收藏??????
Vue2 Diff算法可以參考【Vue2.x源碼系列08】Diff算法原理
Vue3 Diff算法可以參考【Vue3.x源碼系列06】Diff算法原理
在 上一章結尾亂序比對 算法中,可以看到,我們倒序遍歷了新的亂序節點,對每一個節點都進行了插入操作(移動節點位置),這就有點浪費性能。
我們能不能盡可能少的移動節點位置,又能保證節點順序是正確的呢?
例如舊節點 1, 3, 4, 2,新節點 1, 2, 3, 4。那我們完全可以只將 2 移動到 3 前面,只需移動一次!就能保證順序是正確的!!!
ok!我們可以針對于亂序比對中生成的數組 newIndexToOldIndexMap 獲取最長遞增子序列
注意!vue3 中的最長遞增子序列算法求的是 最長遞增子序列的對應索引,下面示例我們用的是最長遞增子序列,便于直觀理解,可讀性+++
舉個實際例子捋一下思路
請聽題,有一個數組,[3, 2, 8, 9, 5, 6, 7, 11, 15] ,最終的最長遞增子序列是什么?
// [2, 8, 9, 11, 15] No!這一個不是最長遞增子序列
// [2, 5, 6, 7, 11, 15] 這一個才是最長的!
這個時候我們只需移動 9,8,3 三個節點即可,而不是全部節點!增子序列越長,所需要移動的節點次數就越少
我們可以利用 貪心算法 + 二分查找 獲取原始的最長遞增子序列,時間復雜度:O(NlogN)
// 3
// 2(2替換3)
// 2, 8
// 2, 8, 9
// 2, 5, 9(5替換掉8,二分查找,找到第一個比5大的進行替換,即所有大于當前值的結果中的最小值)
// 2, 5, 6(6替換掉9,二分查找,找到第一個比6大的進行替換)
// ...
// 2, 5, 6, 7, 11, 15(最長遞增子序列)
由于貪心算法都是取當前局部最優解,有可能會導致最長遞增子序列在原始數組中不是正確的順序
例如數組:[3, 2, 8, 9, 5, 6, 7, 11, 15, 4],此算法求得結果如下。雖然序列不對,但序列長度是沒問題的,在vue3 中我們會用 前驅節點追溯 來解決此問題
// 2, 4, 6, 7, 11, 15(最長遞增子序列)
讓我們整理一下思路,用代碼實現此算法
-
遍歷數組,如果當前這一項比我們最后一項大則直接放到末尾
-
如果當前這一項比最后一項小,需要在序列中通過二分查找找到比當前大的這一項,用他來替換掉
-
前驅節點追溯,替換掉錯誤的節點
最優情況
function getSequence(arr) {
const len = arr.length
const result = [0] // 默認以數組中第0個為基準來做序列,注意!!存放的是數組索引
let resultLastIndex // 結果集中最后的索引
for (let i = 0; i < len; i++) {
let arrI = arr[i]
// 因為在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作
if (arrI !== 0) {
resultLastIndex = result[result.length - 1]
if (arrI > arr[resultLastIndex]) { // 比較當前項和最后一項的值,如果大于最后一項,則將當前索引添加到結果集中
result.push(i) // 記錄索引
continue
}
}
}
return result
}
// 最長遞增子序列:[10, 11, 12, 13, 14, 15, 16]
// 最長遞增子序列索引:[0, 1, 2, 3, 4, 5, 6]
const result = getSequence([10, 11, 12, 13, 14, 15, 16, 0])
console.log(result) // [0, 1, 2, 3, 4, 5, 6]
貪心+二分查找
-
遍歷數組,如果當前這一項比我們最后一項大則直接放到末尾
-
如果當前這一項比最后一項小,需要在序列中通過二分查找找到比當前大的這一項,用他來替換掉
function getSequence(arr) {
const len = arr.length
const result = [0] // 默認以數組中第0個為基準來做序列,注意!!存放的是數組 索引
let resultLastIndex // 結果集中最后的索引
let start
let end
let middle
for (let i = 0; i < len; i++) {
let arrI = arr[i]
// 因為在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作
if (arrI !== 0) {
resultLastIndex = result[result.length - 1]
if (arrI > arr[resultLastIndex]) {
// 比較當前項和最后一項的值,如果大于最后一項,則將當前索引添加到結果集中
result.push(i) // 記錄索引
continue
}
// 這里我們需要通過二分查找,在結果集中找到僅大于當前值的(所有大于當前值的結果中的最小值),用當前值的索引將其替換掉
// 遞增序列 采用二分查找 是最快的
start = 0
end = result.length - 1
while (start < end) {
// start === end的時候就停止了 .. 這個二分查找在找索引
middle = ((start + end) / 2) | 0 // 向下取整
// 1 2 3 4 middle 6 7 8 9 6
if (arrI > arr[result[middle]]) {
start = middle + 1
} else {
end = middle
}
}
// 找到中間值后,我們需要做替換操作 start / end
if (arrI < arr[result[end]]) {
// 這里用當前這一項 替換掉以有的比當前大的那一項。 更有潛力的我需要他
result[end] = i
// p[i] = result[end - 1] // 記住他的前一個人是誰
}
}
}
return result
}
const result = getSequence([3, 2, 8, 9, 5, 6, 7, 11, 15])
console.log(result) // [1, 4, 5, 6, 7, 8] (結果是最長遞增子序列的索引)
// 3
// 2(2替換3)
// 2, 8
// 2, 8, 9
// 2, 5, 9(5替換掉8,二分查找,找到第一個比5大的進行替換,即所有大于當前值的結果中的最小值)
// 2, 5, 6(6替換掉9,二分查找,找到第一個比6大的進行替換)
// ...
// 2, 5, 6, 7, 11, 15(最長遞增子序列)
如果 newIndexToOldIndexMap 數組為 [102, 103, 101, 105, 106, 108, 107, 109, 104]
const result = getSequence([102, 103, 101, 105, 106, 108, 107, 109, 104])
console.log(result) // [2, 1, 8, 4, 6, 7](結果是最長遞增子序列的索引)
// 102
// 102, 103
// 101, 103(102替換掉101,二分查找,找到第一個比101大的進行替換)
// 101, 103, 105
// 101, 103, 105, 106
// 101, 103, 105, 106, 108
// 101, 103, 105, 106, 107(107替換掉108,二分查找,找到第一個比107大的進行替換)
// 101, 103, 105, 106, 107, 109
// 101, 103, 104, 106, 107, 109(104替換掉105,二分查找,找到第一個比104大的進行替換)
得到的最長遞增子序列為 101, 103, 104, 106, 107, 109,我們發現其在原始數組中并不是正確的順序,雖然序列不對,但序列長度是沒問題的。
下一章我們就以此為栗子,用 前驅節點追溯 糾正其錯誤的 101 和 104 節點
前驅節點追溯
再次提醒!最長遞增子序列是 [101, 103, 104, 106, 107, 109], 最長遞增子序列的索引是[2, 1, 8, 4, 6, 7],我們的 result 是最長遞增子序列的索引 !!!
我們發現,只要把 101 替換為 102, 104 替換為 105 ,則序列就被糾正了,思路如下
-
創建一個 回溯列表 p
**[0, 0, 0, 0, 0, 0, 0, 0, 0]**,初始值均為0,長度和數組一樣長,即傳入getSequence 的數組 -
記錄每個節點的前驅節點。無論是 追加到序列末尾 還是 替換序列中的某一項,都要記錄一下他前面的節點,最終生成一個回溯列表 p
[0, 0, 0, 1, 3, 4, 4, 6, 1] -
然后通過 序列的最后一項 109 對應的索引 7 往前回溯,p[7] 是 6,p[6] 是 4,p[4] 是 3 ......,最終得到
7 -> 6 -> 4 -> 3 -> 1 -> 0。 -
因為是從后往前追溯的,result 則被糾正為
[0, 1, 3, 4, 6, 7],替換掉了順序錯誤的節點
文字表達起來可能有點繞,可以看下這張圖輔助理解
export function getSequence(arr) {
const len = arr.length
const result = [0] // 默認以數組中第0個為基準來做序列,注意!!存放的是數組 索引
let resultLastIndex // 結果集中最后的索引
let start
let end
let middle
const p = new Array(len).fill(0) // 最后要標記索引 放的東西不用關心,但是要和數組一樣長
for (let i = 0; i < len; i++) {
let arrI = arr[i]
/** 當前這一項比我們最后一項大則直接放到末尾 */
if (arrI !== 0) {
// 因為在vue newIndexToOldIndexMap 中,0代表需要創建新元素,無需進行位置移動操作
resultLastIndex = result[result.length - 1]
if (arrI > arr[resultLastIndex]) {
// 比較當前項和最后一項的值,如果大于最后一項,則將當前索引添加到結果集中
result.push(i) // 記錄索引
p[i] = resultLastIndex // 當前放到末尾的要記錄他前面的索引,用于追溯
continue
}
/**這里我們需要通過二分查找,在結果集中找到僅大于當前值的(所有大于當前值的結果中的最小值),用當前值的索引將其替換掉 */
// 遞增序列 采用二分查找 是最快的
start = 0
end = result.length - 1
while (start < end) {
// start === end的時候就停止了 .. 這個二分查找在找索引
middle = ((start + end) / 2) | 0 // 向下取整
// 1 2 3 4 middle 6 7 8 9 6
if (arrI > arr[result[middle]]) {
start = middle + 1
} else {
end = middle
}
}
// 找到中間值后,我們需要做替換操作 start / end
if (arrI < arr[result[end]]) {
// 這里用當前這一項 替換掉以有的比當前大的那一項。 更有潛力的我需要他
result[end] = i
p[i] = result[end - 1] // 記住他的前一個人是誰
}
}
}
// 1) 默認追加記錄前驅索引 p[i] = resultLastIndex
// 2) 替換之后記錄前驅索引 p[i] = result[end - 1]
// 3) 記錄每個人的前驅節點
// 通過最后一項進行回溯
let i = result.length
let last = result[i - 1] // 找到最后一項
while (i > 0) {
i--
// 倒敘追溯
result[i] = last // 最后一項是確定的
last = p[last]
}
return result
}
優化Diff算法
我們求得是最長遞增子序列的索引,若亂序節點的索引存在于最長遞增子序列索引中,則跳過他,不移動。這樣就最大限度減少了節點移動操作
利用最長遞增子序列,優化Diff算法,代碼如下
// 獲取最長遞增子序列索引
let increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
let j = increasingNewIndexSequence.length - 1
// 需要移動位置
// 亂序節點需要移動位置,倒序遍歷亂序節點
for (let i = toBePatched - 1; i >= 0; i--) {
let index = i + s2 // i是亂序節點中的index,需要加上s2代表總節點中的index
let current = c2[index] // 找到當前節點
let anchor = index + 1 < c2.length ? c2[index + 1].el : null
if (newIndexToOldIndexMap[i] === 0) {
// 創建新元素
patch(null, current, container, anchor)
} else {
if (i != increasingNewIndexSequence[j]) {
// 不是0,說明已經執行過patch操作了
hostInsert(current.el, container, anchor)
} else {
// 跳過不需要移動的元素, 為了減少移動操作 需要這個最長遞增子序列算法
j--
}
}
總結
以上是生活随笔為你收集整理的Vue3 Diff算法之最长递增子序列,学不会来砍我!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SetFitABSA: 基于 SetFi
- 下一篇: 神经网络优化篇:详解Adam 优化算法(