日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > vue >内容正文

vue

Vue3 Diff算法之最长递增子序列,学不会来砍我!

發(fā)布時(shí)間:2024/1/21 vue 56 coder
生活随笔 收集整理的這篇文章主要介紹了 Vue3 Diff算法之最长递增子序列,学不会来砍我! 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

專欄分享:vue2源碼專欄,vue3源碼專欄,vue router源碼專欄,玩具項(xiàng)目專欄,硬核??推薦??
歡迎各位ITer關(guān)注點(diǎn)贊收藏??????

Vue2 Diff算法可以參考【Vue2.x源碼系列08】Diff算法原理

Vue3 Diff算法可以參考【Vue3.x源碼系列06】Diff算法原理

在 上一章結(jié)尾亂序比對(duì) 算法中,可以看到,我們倒序遍歷了新的亂序節(jié)點(diǎn),對(duì)每一個(gè)節(jié)點(diǎn)都進(jìn)行了插入操作(移動(dòng)節(jié)點(diǎn)位置),這就有點(diǎn)浪費(fèi)性能。

我們能不能盡可能少的移動(dòng)節(jié)點(diǎn)位置,又能保證節(jié)點(diǎn)順序是正確的呢?

例如舊節(jié)點(diǎn) 1, 3, 4, 2,新節(jié)點(diǎn) 1, 2, 3, 4。那我們完全可以只將 2 移動(dòng)到 3 前面,只需移動(dòng)一次!就能保證順序是正確的!??!

ok!我們可以針對(duì)于亂序比對(duì)中生成的數(shù)組 newIndexToOldIndexMap 獲取最長(zhǎng)遞增子序列

注意!vue3 中的最長(zhǎng)遞增子序列算法求的是 最長(zhǎng)遞增子序列的對(duì)應(yīng)索引,下面示例我們用的是最長(zhǎng)遞增子序列,便于直觀理解,可讀性+++

舉個(gè)實(shí)際例子捋一下思路

請(qǐng)聽題,有一個(gè)數(shù)組,[3, 2, 8, 9, 5, 6, 7, 11, 15] ,最終的最長(zhǎng)遞增子序列是什么?

// [2, 8, 9, 11, 15]     No!這一個(gè)不是最長(zhǎng)遞增子序列
// [2, 5, 6, 7, 11, 15]  這一個(gè)才是最長(zhǎng)的!

這個(gè)時(shí)候我們只需移動(dòng) 9,8,3 三個(gè)節(jié)點(diǎn)即可,而不是全部節(jié)點(diǎn)!增子序列越長(zhǎng),所需要移動(dòng)的節(jié)點(diǎn)次數(shù)就越少

我們可以利用 貪心算法 + 二分查找 獲取原始的最長(zhǎng)遞增子序列,時(shí)間復(fù)雜度:O(NlogN)

// 3
// 2(2替換3)
// 2, 8
// 2, 8, 9
// 2, 5, 9(5替換掉8,二分查找,找到第一個(gè)比5大的進(jìn)行替換,即所有大于當(dāng)前值的結(jié)果中的最小值)
// 2, 5, 6(6替換掉9,二分查找,找到第一個(gè)比6大的進(jìn)行替換)
// ...
// 2, 5, 6, 7, 11, 15(最長(zhǎng)遞增子序列)

由于貪心算法都是取當(dāng)前局部最優(yōu)解,有可能會(huì)導(dǎo)致最長(zhǎng)遞增子序列在原始數(shù)組中不是正確的順序

例如數(shù)組:[3, 2, 8, 9, 5, 6, 7, 11, 15, 4],此算法求得結(jié)果如下。雖然序列不對(duì),但序列長(zhǎng)度是沒問題的,在vue3 中我們會(huì)用 前驅(qū)節(jié)點(diǎn)追溯 來解決此問題

// 2, 4, 6, 7, 11, 15(最長(zhǎng)遞增子序列)

讓我們整理一下思路,用代碼實(shí)現(xiàn)此算法

  1. 遍歷數(shù)組,如果當(dāng)前這一項(xiàng)比我們最后一項(xiàng)大則直接放到末尾

  2. 如果當(dāng)前這一項(xiàng)比最后一項(xiàng)小,需要在序列中通過二分查找找到比當(dāng)前大的這一項(xiàng),用他來替換掉

  3. 前驅(qū)節(jié)點(diǎn)追溯,替換掉錯(cuò)誤的節(jié)點(diǎn)

最優(yōu)情況

function getSequence(arr) {
  const len = arr.length
  const result = [0] // 默認(rèn)以數(shù)組中第0個(gè)為基準(zhǔn)來做序列,注意??!存放的是數(shù)組索引
  let resultLastIndex // 結(jié)果集中最后的索引

  for (let i = 0; i < len; i++) {
    let arrI = arr[i]
    // 因?yàn)樵趘ue newIndexToOldIndexMap 中,0代表需要?jiǎng)?chuàng)建新元素,無需進(jìn)行位置移動(dòng)操作
    if (arrI !== 0) {
      resultLastIndex = result[result.length - 1]
      if (arrI > arr[resultLastIndex]) { // 比較當(dāng)前項(xiàng)和最后一項(xiàng)的值,如果大于最后一項(xiàng),則將當(dāng)前索引添加到結(jié)果集中
        result.push(i) // 記錄索引
        continue
      }
    }
  }
  return result
}

// 最長(zhǎng)遞增子序列:[10, 11, 12, 13, 14, 15, 16]
// 最長(zhǎng)遞增子序列索引:[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]

貪心+二分查找

  1. 遍歷數(shù)組,如果當(dāng)前這一項(xiàng)比我們最后一項(xiàng)大則直接放到末尾

  2. 如果當(dāng)前這一項(xiàng)比最后一項(xiàng)小,需要在序列中通過二分查找找到比當(dāng)前大的這一項(xiàng),用他來替換掉

function getSequence(arr) {
  const len = arr.length
  const result = [0] // 默認(rèn)以數(shù)組中第0個(gè)為基準(zhǔn)來做序列,注意??!存放的是數(shù)組 索引
  let resultLastIndex // 結(jié)果集中最后的索引
  let start
  let end
  let middle 

  for (let i = 0; i < len; i++) {
    let arrI = arr[i]
    // 因?yàn)樵趘ue newIndexToOldIndexMap 中,0代表需要?jiǎng)?chuàng)建新元素,無需進(jìn)行位置移動(dòng)操作
    if (arrI !== 0) {
      resultLastIndex = result[result.length - 1]
      if (arrI > arr[resultLastIndex]) {
        // 比較當(dāng)前項(xiàng)和最后一項(xiàng)的值,如果大于最后一項(xiàng),則將當(dāng)前索引添加到結(jié)果集中
        result.push(i) // 記錄索引
        continue
      }
      
      // 這里我們需要通過二分查找,在結(jié)果集中找到僅大于當(dāng)前值的(所有大于當(dāng)前值的結(jié)果中的最小值),用當(dāng)前值的索引將其替換掉
      // 遞增序列 采用二分查找 是最快的
      start = 0
      end = result.length - 1
      while (start < end) {
        // start === end的時(shí)候就停止了  .. 這個(gè)二分查找在找索引
        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]]) {
        // 這里用當(dāng)前這一項(xiàng) 替換掉以有的比當(dāng)前大的那一項(xiàng)。 更有潛力的我需要他
        result[end] = i
        // p[i] = result[end - 1] // 記住他的前一個(gè)人是誰
      }
    }
  }
  return result
}

const result = getSequence([3, 2, 8, 9, 5, 6, 7, 11, 15])
console.log(result) // [1, 4, 5, 6, 7, 8] (結(jié)果是最長(zhǎng)遞增子序列的索引)
// 3
// 2(2替換3)
// 2, 8
// 2, 8, 9
// 2, 5, 9(5替換掉8,二分查找,找到第一個(gè)比5大的進(jìn)行替換,即所有大于當(dāng)前值的結(jié)果中的最小值)
// 2, 5, 6(6替換掉9,二分查找,找到第一個(gè)比6大的進(jìn)行替換)
// ...
// 2, 5, 6, 7, 11, 15(最長(zhǎng)遞增子序列)

如果 newIndexToOldIndexMap 數(shù)組為 [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](結(jié)果是最長(zhǎng)遞增子序列的索引)
// 102
// 102, 103
// 101, 103(102替換掉101,二分查找,找到第一個(gè)比101大的進(jìn)行替換)
// 101, 103, 105
// 101, 103, 105, 106
// 101, 103, 105, 106, 108
// 101, 103, 105, 106, 107(107替換掉108,二分查找,找到第一個(gè)比107大的進(jìn)行替換)
// 101, 103, 105, 106, 107, 109
// 101, 103, 104, 106, 107, 109(104替換掉105,二分查找,找到第一個(gè)比104大的進(jìn)行替換)

得到的最長(zhǎng)遞增子序列為 101, 103, 104, 106, 107, 109,我們發(fā)現(xiàn)其在原始數(shù)組中并不是正確的順序,雖然序列不對(duì),但序列長(zhǎng)度是沒問題的。

下一章我們就以此為栗子,用 前驅(qū)節(jié)點(diǎn)追溯 糾正其錯(cuò)誤的 101 和 104 節(jié)點(diǎn)

前驅(qū)節(jié)點(diǎn)追溯

再次提醒!最長(zhǎng)遞增子序列是 [101, 103, 104, 106, 107, 109], 最長(zhǎng)遞增子序列的索引是[2, 1, 8, 4, 6, 7],我們的 result 是最長(zhǎng)遞增子序列的索引 ?。。?/strong>

我們發(fā)現(xiàn),只要把 101 替換為 102, 104 替換為 105 ,則序列就被糾正了,思路如下

  1. 創(chuàng)建一個(gè) 回溯列表 p **[0, 0, 0, 0, 0, 0, 0, 0, 0]**,初始值均為0,長(zhǎng)度和數(shù)組一樣長(zhǎng),即傳入getSequence 的數(shù)組

  2. 記錄每個(gè)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)。無論是 追加到序列末尾 還是 替換序列中的某一項(xiàng),都要記錄一下他前面的節(jié)點(diǎn),最終生成一個(gè)回溯列表 p [0, 0, 0, 1, 3, 4, 4, 6, 1]

  3. 然后通過 序列的最后一項(xiàng) 109 對(duì)應(yīng)的索引 7 往前回溯,p[7] 是 6,p[6] 是 4,p[4] 是 3 ......,最終得到7 -> 6 -> 4 -> 3 -> 1 -> 0。

  4. 因?yàn)槭菑暮笸白匪莸模瑀esult 則被糾正為 [0, 1, 3, 4, 6, 7],替換掉了順序錯(cuò)誤的節(jié)點(diǎn)

文字表達(dá)起來可能有點(diǎn)繞,可以看下這張圖輔助理解

export function getSequence(arr) {
  const len = arr.length
  const result = [0] // 默認(rèn)以數(shù)組中第0個(gè)為基準(zhǔn)來做序列,注意!!存放的是數(shù)組 索引
  let resultLastIndex // 結(jié)果集中最后的索引
  let start
  let end
  let middle
  const p = new Array(len).fill(0) // 最后要標(biāo)記索引 放的東西不用關(guān)心,但是要和數(shù)組一樣長(zhǎng)

  for (let i = 0; i < len; i++) {
    let arrI = arr[i]
    /** 當(dāng)前這一項(xiàng)比我們最后一項(xiàng)大則直接放到末尾 */
    if (arrI !== 0) {
      // 因?yàn)樵趘ue newIndexToOldIndexMap 中,0代表需要?jiǎng)?chuàng)建新元素,無需進(jìn)行位置移動(dòng)操作
      resultLastIndex = result[result.length - 1]
      if (arrI > arr[resultLastIndex]) {
        // 比較當(dāng)前項(xiàng)和最后一項(xiàng)的值,如果大于最后一項(xiàng),則將當(dāng)前索引添加到結(jié)果集中
        result.push(i) // 記錄索引
        p[i] = resultLastIndex // 當(dāng)前放到末尾的要記錄他前面的索引,用于追溯
        continue
      }

      /**這里我們需要通過二分查找,在結(jié)果集中找到僅大于當(dāng)前值的(所有大于當(dāng)前值的結(jié)果中的最小值),用當(dāng)前值的索引將其替換掉 */
      // 遞增序列 采用二分查找 是最快的
      start = 0
      end = result.length - 1
      while (start < end) {
        // start === end的時(shí)候就停止了  .. 這個(gè)二分查找在找索引
        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]]) {
        // 這里用當(dāng)前這一項(xiàng) 替換掉以有的比當(dāng)前大的那一項(xiàng)。 更有潛力的我需要他
        result[end] = i
        p[i] = result[end - 1] // 記住他的前一個(gè)人是誰
      }
    }
  }

  // 1) 默認(rèn)追加記錄前驅(qū)索引 p[i] = resultLastIndex
  // 2) 替換之后記錄前驅(qū)索引 p[i] = result[end - 1]
  // 3) 記錄每個(gè)人的前驅(qū)節(jié)點(diǎn)
  // 通過最后一項(xiàng)進(jìn)行回溯
  let i = result.length
  let last = result[i - 1] // 找到最后一項(xiàng)
  while (i > 0) {
    i--
    // 倒敘追溯
    result[i] = last // 最后一項(xiàng)是確定的
    last = p[last]
  }
  return result
}

優(yōu)化Diff算法

我們求得是最長(zhǎng)遞增子序列的索引,若亂序節(jié)點(diǎn)的索引存在于最長(zhǎng)遞增子序列索引中,則跳過他,不移動(dòng)。這樣就最大限度減少了節(jié)點(diǎn)移動(dòng)操作

利用最長(zhǎng)遞增子序列,優(yōu)化Diff算法,代碼如下

// 獲取最長(zhǎng)遞增子序列索引
let increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
let j = increasingNewIndexSequence.length - 1

// 需要移動(dòng)位置
// 亂序節(jié)點(diǎn)需要移動(dòng)位置,倒序遍歷亂序節(jié)點(diǎn)
for (let i = toBePatched - 1; i >= 0; i--) {
  let index = i + s2 // i是亂序節(jié)點(diǎn)中的index,需要加上s2代表總節(jié)點(diǎn)中的index
  let current = c2[index] // 找到當(dāng)前節(jié)點(diǎn)
  let anchor = index + 1 < c2.length ? c2[index + 1].el : null
  if (newIndexToOldIndexMap[i] === 0) {
    // 創(chuàng)建新元素
    patch(null, current, container, anchor)
  } else {
    if (i != increasingNewIndexSequence[j]) {
      // 不是0,說明已經(jīng)執(zhí)行過patch操作了
      hostInsert(current.el, container, anchor)
    } else {
      // 跳過不需要移動(dòng)的元素, 為了減少移動(dòng)操作 需要這個(gè)最長(zhǎng)遞增子序列算法
      j--
    }
  }

總結(jié)

以上是生活随笔為你收集整理的Vue3 Diff算法之最长递增子序列,学不会来砍我!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。