iT邦幫忙

2023 iThome 鐵人賽

DAY 19
0
Vue.js

淺談vue3源碼,很淺的那種系列 第 20

[Day 19] runtime-core——最長遞增子序列 - 3

  • 分享至 

  • xImage
  •  

今天是我們學習最長遞增子序列算法的第三天,我們終於要開始寫代碼了。

我假設看到這裡的你有先認真看完前兩天的內容,或許你可能看了但沒全看懂,就像去年我剛學這個算法的時候一樣,感覺這東西他喵的是人想出來的!?所以今天寫代碼的過程中我也會逐步說明,但最好還是先把前兩天的進度看完,才好和今天的內容對照著學。

俗話說程序員三日不寫代碼便覺面目可憎,今天是時候該付諸實戰了。讓我們新建檔案/src/runtime-core/sequence.ts,並暴露一個方法。

export default (arr: number[]) => {
  const result = [0];
  const prevIdxs: number[] = [];
};

這個方法接收一個陣列,也就是我們要從中找出最長遞增子序列的父陣列。
並在其中宣告兩個陣列result及prevIdxs,分別記錄子序列以及父陣列每項在被記入子序列時,其在子序列的前項。

然後我們遍歷arr:

arr.forEach((num, i) => {
  const last = result[result.length - 1];
  if (num > arr[last]) {

  } else {

  }
});

宣告一個last代表子序列的最後一項,並比較arr當前的項num,是否大於result末項在arr中對應的項。

如果大於代表目前我們遍歷到的這個num確實是遞增的,我們可以直接把它的索引推到result中,並且記錄它在result的前項,這會是比較簡單好處理的情況。因此我們先從這兒下手:

if (num > arr[last]) {
  result.push(i);
  prevIdxs[i] = last;
}

else的情況就比較麻煩一些,我們必須替換在維持遞增的前提下最小的項,這會需要用到二分查找算法。

所謂二分查找算法,就有點像是猜數字遊戲(誒……我的年齡暴露了?)。

我在心裡想一個1~100的數字,你猜我想的是多少,我會告訴你你猜的太大或太小。在這種猜數字遊戲,你先猜50,如果太小你再猜75,如果還是太小你再猜87,這樣在較多的情況下會比從1開始一個一個慢慢猜來得快速。

也就是當你要從一個順序固定的陣列中找尋符合條件的某個數,先從這個陣列的正中間開始判斷,太小就再往起點和中點的正中間找,太大就往中點和終點的正中間找,然後不斷重複直到找到為止。

let start = 0
let end = result.length - 1
while (end > start) {
  const middleIdx = ((start + end) / 2) | 0
  if (num > arr[result[middleIdx]]) start = middleIdx + 1
  else end = middleIdx
}

| 0和Math.floor()相同效果,都是取整。

當跳脫迴圈,start跟end會相等,都是result中,在arr對應的項剛好比num大的最小項,也就是我們要取代掉的項。

所以接下來就是用比較小的num的索引取代掉result[start]了。

result[start] = i;
prevIdxs[i] = result[start - 1];

此時我們的sequence.ts應該長這樣:

export default (arr: number[]) => {
  const result = [0];
  const prevIdxs: number[] = [];

  arr.forEach((num, i) => {
    const last = result[result.length - 1];
    if (num > arr[last]) {
      result.push(i);
      prevIdxs[i] = last;
    } else {
      let start = 0;
      let end = result.length - 1;
      while (end > start) {
        const middleIdx = ((start + end) / 2) | 0;
        if (num > arr[result[middleIdx]]) start = middleIdx + 1;
        else end = middleIdx;
      }

      result[start] = i;
      prevIdxs[i] = result[start - 1];
    }
  });
};

我們完成了arr的遍歷,記錄每一項的索引被寫入result後它在result中的前一項。接下來我們要從result的末尾往前遍歷,把每項的前一項替換成它在prevIdxs對應的索引。

for (let i = result.length - 1; i > 0; i--) {
  result[i - 1] = prevIdxs[result[i]];
}

最後把result給return出去即可。此時我們的sequence.ts應該長這樣:

export default (arr: number[]) => {
  const result = [0];
  const prevIdxs: number[] = [];

  arr.forEach((num, i) => {
    const last = result[result.length - 1];
    if (num > arr[last]) {
      result.push(i);
      prevIdxs[i] = last;
    } else {
      let start = 0;
      let end = result.length - 1;
      while (end > start) {
        const middleIdx = ((start + end) / 2) | 0;
        if (num > arr[result[middleIdx]]) start = middleIdx + 1;
        else end = middleIdx;
      }

      result[start] = i;
      prevIdxs[i] = result[start - 1];
    }
  });

  for (let i = result.length - 1; i > 0; i--) {
    result[i - 1] = prevIdxs[result[i]];
  }

  return result;
};

我們再來給它從頭過一次。

先創建兩個陣列result及prevIdxs,分別代表子序列和父陣列各項索引在子序列中的前項。這個子序列result會在遍歷父陣列arr時,遇大則推,遇小則替,最終將會變成父陣列arr的最長遞增子序列。而prevIdxs就是在arr的每一項索引被放入result時,它在result中的前一項。也就是說這個prevIdxs會跟arr一樣長,arr的每一項在prevIdxs都能找到對應的項。

接著我們遍歷arr,遇大則推沒甚麼問題,遇小則替,要替的就是比它大的最小項索引,因此用二分查找。找到以後就是把result中的那一項換成遍歷到的索引,同時也記錄它在result中的前一項。

最後再從尾端往回,把result的每一項都換回它真正的前項,這個result就是我們要找的最長遞增子序列。

明天我們將利用這個封裝好的方法,來算出不需要重新渲染的子節點,我們將以此完成diff算法以及renderer.ts。

githubmain分支commit「[Day 19] runtime-core——最長遞增子序列 - 3」


上一篇
[Day 18] runtime-core——最長遞增子序列 - 2
下一篇
[Day 20] runtime-core——完成渲染dom
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言