[H] 297. 序列化與反序列化二元樹 (Serialize and Deserialize Binary Tree)
題目
- LeetCode 連結
- 主題:Binary Tree
- 難度:Hard
題目描述
序列化是將資料結構或物件轉換為位元序列的過程,以便將其儲存於檔案或記憶體緩衝區中,或透過網路連線進行傳輸,稍後在相同或其他環境中重組。
請設計一種演算法來序列化和反序列化二元樹。沒有任何限制此演算法如何運作,只要你的序列化/反序列化演算法確保能將二元樹序列化為字串,並且能從該字串正確地反序列化為原始的樹結構。
說明:輸入/輸出的格式可以與 LeetCode 的二元樹序列化格式相同。你不需要完全遵循該格式,可以發揮創意並提出不同的解法。
範例 1:
範例 2:
限制條件:
- 樹中結點的數量範圍為
[0, 10⁴]
。 - 每個結點的值範圍為
-1000 <= Node.val <= 1000
。
問題釐清
- 簡單理解題意的話,會希望去完成兩個函式,讓一個二元樹在經過
serialize
後成字串後,再把該字串傳入deserialize
可以得到原本的二元樹,這樣理解沒錯吧? - 如果用 TypeScript 去實作的話,把這個可以做序列化、反序列化的功能做成一個像是 TreeCodec 的 class 可能會更好維護?
- 確認
serialize
函式:- 輸入的型別部份限制為 TreeNode 或 null 即可,或需要處理其他不符合需求的格式做錯誤處理?
- 輸入的 TreeNode 中的值可以限制為 number,不需對其他非預期格式做處理?
- 當輸入中含有負值,則輸出可能會像是
-1,2,-3,n,n,-4,5
這樣? - 輸出的部份一律為字串?
- 輸出的字串格式參考範例一的話,是否一定要像是
[1,2,3,null,null,4,5]
的格式,或也可以簡化像是1,2,3,n,n,4,5
即可? - 當輸入的樹在最後一層中只有一個最左邊的節點,則輸出字串中是否需移除連續的空值,像是
1,2,3,4,n,n,n
其實只需要1,2,3,4
即可 - 當輸入為 null 時,則輸出空字串?
- 確認
deserialize
函式:- 輸入的型別部份限制為只有字串,會需要處理其他不符合需求的格式做錯誤處理?
- 輸出的部份為 TreeNode 或 null?
- 當輸入為空字串時,則輸出 null?
提出測試案例
- 基本測資能將範例一的
[1,2,3,null,null,4,5]
做序列化、反序列化後能得到同樣的 tree - 確認空樹在序列化後得到空字串、將空字串反序列化後得到空樹 (null)
- 測試最後一層只有最左邊一個節點時,序列化後不會有連續的 n 字串
- 檢查含有負值的樹也能正確運作
- 檢查 root 節點值為 0 的狀況
- 檢查節點數為 10000 的只有右節點的樹能正確運作
- 檢查節點數接近 10000 的 complete binary tree 能正確運作
提出思路
定義好上述序列化與反序列化的規則後,可以視為逐層遍歷與字串處理問題:
- 定義
TreeCodec
的 class,裡面包含serialize
與deerialize
兩個函式 - 序列化
- 若輸入為 null,回傳空字串
- 遍歷的演算法可以像做 Level Order Traversal 一樣用 BFS 來解
- 宣告一個 queue 來儲存每一層樹節點,跑迴圈來遍歷直到 queue 為空
- 依照類似
-1,2,-3,n,n,-4,5
這樣的規則來輸出序列化字串資料,n 代表空節點,且可能有負數值 - 移除多餘的連續空值
- 組合字串陣列並以
,
做分隔 - 返回結果字串
- 反序列化
- 若輸入為空字串,回傳 null
- 對輸入字串用
,
符號做 split 得到節點值的陣列 - 對該字串陣列值做處理,遇到
n
則視為空節點 null,否則轉為數字 - 用 queue 儲存當前待處理的節點,並跑迴圈用 top down 的方式來逐層組合輸出的二元樹,直到 queue 為空則停止
- 返回結果二元樹
以註解表示:
實作
將上述的思路實作後會像以下的程式:
試著將這段程式中的兩個函式放到 leetcode 上去 submit 可以通過,以下試著來根據上面的測試案例實作單元測試。
撰寫單元測試
這裡實際去執行測試後,只有倒數第二個這個左側一條深層的樹會遇到 RangeError: Maximum call stack size exceeded
的 stack overflow 問題而失敗。
先試著把反序列化的部份註解掉就能正常通過了,所以效能瓶頸是發生在 deserialized
的地方,試著做一些特製的邊界條件去調整後問題還是存在,後來在想有可能是原本 Vitest 設定可能有一些效能相關的問題。
找到一個 pool 的設定,試著調整成 pool: ‘threads'
就通過了,研究了下文件看起來預設是使用 pool: 'forks'
的方式,也就是將每個測試跑在不同的 node:child_process
,而若有效能問題,文件建議可以改成用 node:worker_threads
的方式改善。
又好奇去看這是不是只有 Vitest 特製的設定,找到 jest 中也有一個 workerThreads 的設定,關於這兩者的差別還沒太深入研究,先筆記在這。
程式碼
完整程式碼可以參考此 GitHub 連結。