昨天已經成功把隨機產生的 Array 畫成長條圖,今天終於要開始挑戰這個專案最重要的部分:讓 Bubble Sort 動起來。
目前我把畫面分成三個區塊:左側顯示原始資料與目前步驟,中間顯示排序的長條圖,右側則放 Bubble Sort 程式碼,希望之後執行排序時,也可以跟著目前的步驟一起高亮。
下面是我 Day 6 已經寫過的 Bubble Sort:
for (let j = arr.length; j > 1; j--) {
for (let i = 0; i < j - 1; i++) {
if (arr[i] > arr[i + 1]) {
const temp = arr[i]
arr[i] = arr[i + 1]
arr[i + 1] = temp
}
}
}
如果想讓長條圖透過顏色區分目前的排序狀態,應該只要知道「目前比較到哪兩個數字」就可以了吧?
所以我一開始有了以下的想法。
因為 Bubble Sort 的內層迴圈本來就有 i,所以目前正在比較的位置其實就是:
[i, i + 1]
例如:
arr = [5, 3, 8, 1]
第一次比較的是 index 0、1,下一次則是 1、2。
所以我原本想建立幾個狀態:
const comparing = ref<number[]>([])
const sorted = ref<number[]>([])
每次進入內層迴圈,就記錄目前比較的位置:
comparing.value = [i, i + 1]
這樣 template 就能透過 index 判斷目前哪些長條需要改變樣式:
:class="{
comparing: comparing.includes(index),
sorted: sorted.includes(index)
}"
而每一個 Pass 結束後,j - 1 的位置已經確定,因此也可以把它加入 sorted。
到這裡看起來好像都很合理。
真正實作後,我才發現最大的問題不是「狀態要存什麼」,而是:
Bubble Sort 跑得太快了。
我原本想像的執行過程是:
comparing = [0, 1]
↓
畫面更新
↓
交換
↓
comparing = [1, 2]
↓
畫面再次更新
但 for 迴圈是同步執行的,它不會每改一次狀態,就停下來等畫面完成這一步再繼續。
所以實際執行更接近:
[0,1] → [1,2] → [2,3] → Pass 完成 → 下一個 Pass → 排序完成
↓
畫面更新
也就是說,即使我知道每個時間點應該設定什麼狀態,這些狀態還是會在很短的時間內被下一個狀態覆蓋掉。
這時我才發現,我真正要解決的問題不是:
怎麼讓 Bubble Sort 慢慢執行?
而是:
怎麼把 Bubble Sort 的執行過程保存下來,再慢慢播放?
最後我的做法是建立 SortStep,把每一個重要時間點需要的資料存成一份快照:
interface SortStep {
arr: number[]
barStates: BarState[]
highlightLines: number[]
phase: string
pass: number
summary: string
detail: string
}
其中 arr 記錄當下的陣列、barStates 記錄每根長條的狀態、highlightLines 則提供右側程式碼高亮使用。
Bubble Sort 本身還是會很快執行完,不同的是,我會在「比較、交換、Pass 完成」這些重要時間點,把當下的狀態 push 進 steps。
概念上可以先簡化成:
const steps = []
for (...) {
for (...) {
// 記錄「比較」的狀態
steps.push({
arr: [...arr],
phase: 'compare'
})
if (...) {
// 執行交換
// ...
// 記錄「交換完成」的狀態
steps.push({
arr: [...arr],
phase: 'swap'
})
}
}
// 記錄這個 Pass 完成
steps.push({
arr: [...arr],
phase: 'pass'
})
}
這裡很重要的一點是:
steps.push() 不是讓 Bubble Sort 停下來。
Bubble Sort 還是會一次執行完成,只是在執行的過程中,把每個重要時間點「拍照保存」。
因此最後會得到類似:
Step 0 → 初始狀態
Step 1 → 比較 index 0、1
Step 2 → 交換 index 0、1
Step 3 → 比較 index 1、2
Step 4 → 比較 index 2、3
Step 5 → 交換 index 2、3
Step 6 → Pass 完成
...
這樣前面的狀態就不會因為下一次迴圈執行而消失,而是全部保存在 steps 裡。
有了所有 Step 之後,畫面就不需要直接跟著 Bubble Sort 的 for 迴圈跑了。
我另外使用:
currentStep
記錄「現在播放到第幾步」,再取得目前要顯示的 Step:
const step = steps[currentStep]
播放時只要控制:
Step 0
↓
等待
↓
Step 1
↓
等待
↓
Step 2
↓
等待
↓
Step 3
畫面就有時間根據每個 Step 的 arr、barStates、highlightLines 更新長條圖與右側程式碼。
所以真正被「放慢」的其實不是 Bubble Sort,而是 Step 的播放速度。
這次實作最困難的地方,是我原本一直以為要讓 Bubble Sort 的 for 迴圈慢慢執行。
實際做完才發現,真正要做的是把「比較、交換、排序完成」等重要時間點記錄下來,再按照順序慢慢播放。
這個過程就像拍電影一樣,先把每個重要畫面記錄成 Step,最後再一張一張播放,就形成了排序動畫。
今天終於從一張靜態的長條圖,走到可以把 Bubble Sort 的排序過程真正「播放」出來。
最後放上今天完成的成果圖~