二叉樹(Binary Tree) 是非常基礎的數據結構。平衡樹可以讓樹的查找,更新,插入,刪除都是O(logN)的復雜度。
二叉樹的基本實現是比較簡單的。然而對于Rust,因為所有權(ownership)和借用檢查(borrow check) 機制,會比普通的語言稍微"麻煩"一點。leetcode 上有很多二叉樹的題目,本文采用leetcode定義的 tree 節點類型。一方面是 leetcode 的定義可以方便刷題,另一方面會涉及到rust語言特性,可以練習。
Note: leetcode上定義的rust treenode節點,并不是特別match rust的特性,原因參考
本文的主要內容如下:
- 二叉樹節點定義,使用rust 如何定義二叉樹節點
- 二叉樹的遍歷,dfs(先序,中序,后序),bfs(層序) 的寫法,遞歸和迭代的異同
- 二叉樹的樹形打印,方便直觀的看到一顆二叉樹的拓撲形狀
- 根據一個數組構建一顆二叉樹,即Leetcode二叉樹的輸入,方便離線環境刷題
樹節點定義
Leetcode 幾乎給出了其支持語言的樹節點的定義。如 python 的節點
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
python節點很簡單,定義一個 class,屬性分別是節點值,左右子樹。
golang 的節點
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
golang節點也不復雜,定義一個結構體,也是三個字段,分表是數據值,左右子樹的節點指針。之所以左右子樹用指針。TreeNode是遞歸定義,TreeNode如果還沒聲明,是無法在其字段中定義聲明的。
然而 rust的節點定義,卻看起來稍微復雜:
#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
pub val: i32,
pub left: Option<Rc<RefCell<TreeNode>>>,
pub right: Option<Rc<RefCell<TreeNode>>>,
}
impl TreeNode {
#[inline]
pub fn new(val: i32) -> Self {
TreeNode {
val,
left: None,
right: None
}
}
}
對于 #[derive(Debug, PartialEq, Eq)]
是rust的一種注解,表示默認實現的trait。這里表面這個結構體可以打印(Debug)到控制臺以及可以比較大小(PartialEq, Eq)。
然后定義 TreeNode 節點的時候,有了三層。
- Option<T> 一種枚舉,有兩個變體 Some(T) 和 None。有值的時候使用 Some 包裝,無值的時候使用 None。類似 python 的 None 和 golang 的nil,但與它們有本質的區別。
- Rc 是一種智能指針,可以共享所有權。rust 的變量和其值是綁定關系,每個值都有管理其所有權的變量。Rc是 (Reference Count)引用計數,它可以有多個所有權引用。但是Rc包裝的內容不可變。
- 因為Rc包裝的內容不能改變,而 treenode 會變更其左右子樹,因此 RefCell 提供了內部可變性。
Option比較好理解,下面分別對剩下兩個原因進行代碼演示。為了方便演示,定義下面的簡化結構。
#[derive(Debug)]
struct TreeNode {
val: i32
}
let t1 = TreeNode{val:1};
println!("{:?}", t1); // 輸出: TreeNode { val: 1 }
定義一個 t1 變量,然后綁定一個 TreeNode 實例。通常 tree 的遍歷需要使用棧。
let t1 = TreeNode{val:1};
println!("{:?}", t1);
let mut stack = vec![];
stack.push(t1);
println!("{:?}", t1);
讓 t1 進棧,然后再打印 t1。很不幸,rust編譯器將拒絕。原因就是 stack.push(t1)
,將所有權轉移到了push函數內。t1 變量就等同于"失效"了, println!
再使用 t1 就會報錯。下面是 rust 編譯錯誤
17 | let t1 = TreeNode{val:1};
| -- move occurs because `t1` has type `TreeNode`, which does not implement the `Copy` trait
...
21 | stack.push(t1);
| -- value moved here
22 |
23 | println!("{:?}", t1);
| ^^ value borrowed here after move
使用 Rc 就能保持多個所有權引用。
let t1 = Rc::new(TreeNode{val:1}); // 使用 Rc 包裝 TreeNode
let mut stack = vec![];
stack.push(t1.clone()); // 增加 t1 的引用計數,其堆上的數據不變。
println!("{:?}", t1); // TreeNode { val: 1 }
println!("{:?}", Rc::strong_count(&t1)) // 2
通過 Rc 可以看到完美的解決了多所有權引用問題。t1.clone()
還可以寫成 Rc::clone(&t1)
。后者其實更符合rust習慣,因為可以和其他數據結構的 .clone
方法區分。通常其他結構的 .clone
方法是深復制。而 Rc 的 .clone
是淺復制,只增加 Rc 指針的引用計數,不會復制堆上的數據。
到目前為止,一切順利。實際上,我們對樹節點除了一般的查找,還會更新。也就是會更新 TreeNode內部字段的值。下面的代碼將不會編譯通過:
let mut t1 = Rc::new(TreeNode{val:1});
let mut stack = vec![];
stack.push(t1.clone());
println!("{:?}", t1); // TreeNode { val: 1 }
println!("{:?}", Rc::strong_count(&t1)); // 2
t1.val = 12;
// t1.deref().val = 12;
// t1.borrow() = 12;
// t1.borrow_mut() = 12;
t1.val 可以讀到值。因為 Rc 實現了 Deref trai,會自動解引用。但是不代表可以 t1.val 直接賦值修改。變量 mut t1 的修飾表示 t1 可以重新賦值別的值,但是還是不能修改其自生內部字段的值。同樣的,t1.deref 和 t1.borrow t1.borrow_mut 得到的 &TreeNode ,但是依然都無法修改 val 的值。
想要修改 TreeNode 內部字段的值,就需要 RefCell 結構。
let t1 = Rc::new(RefCell::new(TreeNode{val:1})); // 增加 RefCell 包裝
let mut stack = vec![];
stack.push(t1.clone());
println!("{:?}", t1); // TreeNode { val: 1 }
println!("{:?}", Rc::strong_count(&t1)); // 2
t1.borrow_mut().val = 12; // 通過 borrow_mut 方法獲取 RefMut<TreeNode> 類型,后者可以直接修改 val 的值。
println!("{:?}", t1.borrow().val); //
RefCell 結構通過 borrow/borrow_mut 方法可以得到 Ref/RefMut 類型。后者就等同于 Treenode,可以直接改變其內部字段。并且是 t1 的內部可變,其本身還是 不可變的。
綜上所述,Rc 用于多所有權的引用,RefCell 用于修改 struct 內部字段的值。所以 Leetcode 如此定義 TreeNode。
樹的遍歷
二叉樹的遍歷主要有兩大類,深度優先搜索(DFS)和廣度優先(BFS)。前者是指沿著樹的路徑先遍歷到葉子(Leaf)節點,然后再遍歷其他路徑。后者是指先遍歷每一層的節點,然后層層遞進。
二叉樹有兩個很有用的屬性,節點的高度和深度。高度就是節點到最深葉子節點的距離。其深度是節點到樹根的距離(Leetcode 的定義,實際上路徑數和節點數兩個量都可以)。
深度優先
如同樹節點的定義一樣,二叉樹是節點的集合,一個節點也是二叉樹。二叉樹可以看成:左子樹,根,右子樹的最基本單元。
根據 這三者的遍歷順序可以分為三者遍歷方法。
- 先序(前序)遍歷(preorder traversal): 根 -> 左子樹 -> 右子樹
- 中序遍歷(inorder traversal):左子樹 -> 根 -> 右子樹
- 后序遍歷(inorder traversal):左子樹 -> 右子樹 -> 根
遞歸遍歷
三者遍歷的遞歸寫法很簡單。
先序遍歷
例如下面的 Python3代碼。
def preorder(root: TreeNode):
# 遞歸基,節點不存在,直接返回
if root is None:
return
# 先訪問根節點的值
yield root.val
# 遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from preorder(root.left)
# 遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from preorder(root.left)
之所以這里使用python,是因為py的代碼和算法偽代碼很相似,后面再介紹 rust的實現。
中序遍歷
def inorder(root: TreeNode):
# 遞歸基,節點不存在,直接返回
if root is None:
return
# 先遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from inorder(root.left)
# 訪問根節點的值
yield root.val
# 遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from inorder(root.left)
后序遍歷
def postorder(root: TreeNode):
# 遞歸基,節點不存在,直接返回
if root is None:
return
# 先遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from postorder(root.left)
# 遞歸遍歷左子樹,即以左子樹為樹根進行二叉樹遍歷
yield from postorder(root.left)
# 最后訪問根節點的值
yield root.val
從上面的代碼可以看出,二叉樹的DFS遍歷的差別只是再訪問根的節點的順序,這本身也是其定義。先根,中根,后根。因此可以統一下面的偽代碼
def tree_order(node):
if node is None:
return
# preorder vist node
tree_order(node.left)
# inorder vist node
tree_order(node.right)
# postsorder vist node
rust 實現
leetcode 144. 二叉樹的前序遍歷 是二叉樹的先序遍歷。套用上面介紹的算法,rust代碼如下
use std::rc::Rc;
use std::cell::RefCell;
impl Solution {
pub fn preorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
fn preorder_dfs(node: &Option<Rc<RefCell<TreeNode>>>, ans: &mut Vec<i32>) {
// 遞歸基
if node.is_none(){
return
}
// 訪問根節點
ans.push(node.as_ref().unwrap().borrow().val);
// 訪問左子樹
preorder_dfs(&node.as_ref().unwrap().borrow().left, ans);
// 訪問右子樹
preorder_dfs(&node.as_ref().unwrap().borrow().right, ans);
}
let mut ans = vec![];
preorder_dfs(&root, &mut ans);
ans
}
}
因為 node 是 &Option<Rc<RefCell<TreeNode>>> 類型,因此需要先提取 option內的 rc,然后通過 borrow 活動 Ref類型的 TreeNode。上面代碼寫法和算法模板一致,但不是 rust 常用的寫法。下面中序遍歷用 rust 模式匹配進行重構
impl Solution {
pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
fn inorder_dfs(node: &Option<Rc<RefCell<TreeNode>>>, ans: &mut Vec<i32>){
match node {
None => {},
Some(x) => {
inorder_dfs(&x.borrow().left, ans);
ans.push(x.borrow().val);
inorder_dfs(&x.borrow().right, ans);
},
}
}
let mut ans = vec![];
inorder_dfs(&root, &mut ans);
ans
}
}
上面的代碼使用了 match 模式匹配。其寫法依然和算法模板很像。node是 &Option, 通過 match模式匹配,None 相當于 遞歸基,Some 變體解包可以得到 Rc ,進而得到TreeNode 進行訪問值和遞歸。
除了match,rust還提供了便捷的模式匹配語法。下面對145. 二叉樹的后序遍歷 進行說明
impl Solution {
pub fn postorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
fn postorder_dfs(node: &Option<Rc<RefCell<TreeNode>>>, ans: &mut Vec<i32>) {
if let Some(x) = node {
postorder_dfs(&x.borrow().left, ans);
postorder_dfs(&x.borrow().right, ans);
ans.push(x.borrow().val);
}
}
let mut ans = vec![];
postorder_dfs(&root, &mut ans);
ans
}
}
使用 if let
方式進行模式匹配,忽略掉匹配失敗的分支,這里是忽略了 None 變體的 &Option結構。推薦使用最后一種方式,簡單明了。
二叉樹的 dfs 的遞歸算法寫法很簡單。并且由于函數傳遞的是 &Option, 在遞歸調用過程中,也不需要引用多個所有權。對于dfs的非遞歸寫法,則需要借助棧。此時就涉及到二叉樹節點的多所有權。
迭代寫法
先序遍歷
python3 的算法模板:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
return list(self.preorder(root))
def preorder(self, node: TreeNode):
if node is None:
return
# 節點進棧
stack = [node]
while len(stack) > 0:
# 出棧
node = stack.pop()
# 訪問節點
yield node.val
# 右子樹進棧
if node.right is not None:
stack.append(node.right)
# 左子樹進棧
if node.left is not None:
stack.append(node.left)
用 Rust 直接翻譯算法代碼如下:
impl Solution {
pub fn preorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
Solution::preorder_dfs(root)
}
fn preorder_dfs(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
if root.is_none() {
return ans;
}
// 進棧
let mut stack = vec![root];
while !stack.is_empty() {
// stack pop的值會自動包裝 Option,需要調用 flatten 打平
let node = stack.pop().flatten().unwrap();
// 通過 Rc 的borrow 獲取 Ref<TreeNode> 節點
let node = node.borrow();
// 訪問節點值
ans.push(node.val);
// 右子樹進棧
if let Some(ref right) = node.right {
stack.push(Some(right.clone()));
}
// 左子樹進棧
if let Some(ref left) = node.left {
stack.push(Some(left.clone()));
}
}
ans
}
}
由于 Rust 有Option 類型,實際上進棧出棧也可以直接操作,即使遇到子樹不存在的 None,pop出來的時候也可以通過模式匹配處理。代碼會比較緊湊。
impl Solution {
pub fn preorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
Solution::preorder_dfs(root)
}
fn preorder_dfs(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
// 節點進棧,即使 root是None,也沒關系。
let mut stack = vec![root];
while !stack.is_empty() {
// 節點出棧則進行模式匹配,過濾不存在的節點 None
if let Some(node) = stack.pop().flatten() {
ans.push(node.borrow().val);
// 右子樹進棧,None也沒關系
stack.push(node.borrow().right.clone());
// 左子樹進棧,None也沒關系
stack.push(node.borrow().left.clone());
}
}
ans
}
}
向量 vec pop的結構被包裝了 option。在進行先序遍歷的時候,我們可以保證入棧出棧的節點都有值,就像第一種遍歷的方式,那么可以直接入棧 Rc 結構,而不是 Option結構。
fn preorder_dfs(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
if root.is_none() {
return ans;
}
// root 節點不是 None, unwrap 安全
let mut stack = vec![root.unwrap()];
while !stack.is_empty() {
if let Some(node) = stack.pop() {
ans.push(node.borrow().val);
if let Some(ref right) = node.borrow().right {
// 右子樹 Rc 結構進棧,不需要包裝一層 option
stack.push(right.clone());
}
if let Some(ref left) = node.borrow().left {
// 左子樹 Rc 結構進棧
stack.push(left.clone());
}
}
}
ans
}
上面是rust 實現先序遍歷的幾種寫法,實際問題可以具體考慮。還有一種先序遍歷的算法模板,即左子樹先進棧,進棧前訪問節點。如果是出棧的時候訪問節點,算法則變成中序遍歷。并且后序遍歷的算法結構也可以統一起來。
下面是 python的算法模板:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
return list(self.preorder(root))
def preorder(self, node: TreeNode):
stack = []
while True:
# 左子樹進棧
while node is not None:
# 訪問節點
yield node.val
# 節點進棧
stack.append(node)
# 依次遍歷左子樹
node = node.left
# 棧空,退出
if len(stack) <= 0:
break
# 出棧
# yield node.val 如果在此時訪問 節點,則是中序遍歷
node = stack.pop()
# 控制權轉移到右子樹
node = node.right
上面的算法模板,和遞歸的邏輯很相似。也就是先處理左子樹,然后處理右子樹。下面是 rust 代碼的實現:
impl Solution {
pub fn preorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
Solution::preorder(root)
}
fn preorder(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
let mut stack = vec![];
let mut node = root;
loop {
// 通過模式匹配活動節點的 Rc 引用
while let Some(x) = node {
ans.push(x.borrow().val);
// 節點進棧
stack.push(x.clone());
// 轉移左子樹
node = x.borrow().left.clone();
}
if stack.is_empty() {
break;
}
node = stack.pop().unwrap().borrow().right.clone();
}
ans
}
}
代碼和之前的先序遍歷不太一樣,node 會重新賦值,因此它不能借用。需要把所有權轉移給 x,x是 Rc 結構。可以clone增加引用計數。本質上是讓 node重新綁定了新的所有權。
掌握了上面幾種遍歷方式,套用最后一種遍歷方法。下面是中序和后序實現。
中序遍歷
impl Solution {
pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
Solution::inorder(root)
}
fn inorder(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
let mut stack = vec![];
let mut node = root;
loop {
while let Some(x) = node {
stack.push(x.clone());
node = x.borrow().left.clone();
}
if stack.is_empty() {
break;
}
if let Some(x) = stack.pop() {
ans.push(x.borrow().val);
node = x.borrow().right.clone();
}
}
ans
}
}
后序遍歷
impl Solution {
pub fn postorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
Solution::postorder(root)
}
fn postorder(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
let mut ans = vec![];
let mut stack = vec![];
let mut node = root;
let mut visited = None;
loop {
while let Some(x) = node {
stack.push(x.clone());
node = x.borrow().left.clone();
}
if stack.is_empty() {
break;
}
if stack.last().unwrap().borrow().right != visited {
node = stack.last().unwrap().borrow().right.clone();
visited = None
} else {
if let Some(x) = stack.pop(){
ans.push(x.borrow().val);
visited = Some(x.clone());
}
}
}
ans
}
}
至此,二叉樹的DFS遍歷方法介紹完畢,下面再看下廣度搜索層序遍歷
廣度優先
層序遍歷如其字面意思,先遍歷二叉樹的每一層所有節點,然后再向下推進一層。與先序遍歷類似,層序遍歷只需要借助一個 隊列 queue。
impl Solution {
pub fn level_order(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Vec<i32>> {
let mut ans = vec![];
let mut queue = VecDeque::new();
queue.push_back(root);
while !queue.is_empty() {
let size = queue.len();
let mut level = vec![];
for _ in 0..size {
if let Some(x) = queue.pop_front().flatten() {
let node = x.borrow();
level.push(node.val);
queue.push_back(node.left.clone());
queue.push_back(node.right.clone());
}
}
if !level.is_empty() {
ans.push(level);
}
}
ans
}
}
二叉樹反序列化
二叉樹的層序遍歷有很多用法。Leetcode 的test case 使用的是二叉樹的層序遍歷的數組,進行重建二叉樹。下面就介紹這個方法。
#[derive(Debug, PartialEq, Eq)]
pub struct TreeNode {
pub val: i32,
pub left: Option<Rc<RefCell<TreeNode>>>,
pub right: Option<Rc<RefCell<TreeNode>>>,
}
impl TreeNode {
#[inline]
pub fn new(val: i32) -> Self {
TreeNode {
val,
left: None,
right: None,
}
}
pub fn get_height(root: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
fn dfs(root: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
match root {
None => 0,
Some(node) => {
let node = node.borrow();
1 + max(dfs(&node.left), dfs(&node.right))
}
}
}
dfs(&root)
}
// 通過數組反序列化生成一棵樹
pub fn create(nums: Vec<Option<i32>>) -> Option<Rc<RefCell<Self>>> {
if nums.is_empty() {
return None;
}
let size = nums.len();
let mut index = 0;
let root = Some(Rc::new(RefCell::new(Self::new(nums[0].unwrap()))));
let mut queue = VecDeque::new();
queue.push_back(root.clone());
while !queue.is_empty() {
let q_size = queue.len();
for _i in 0..q_size {
if let Some(x) = queue.pop_front().flatten() {
let mut node = x.borrow_mut();
let lseq = 2 * index + 1;
let rseq = 2 * index + 2;
if lseq < size && nums[lseq].is_some() {
node.left = Some(Rc::new(RefCell::new(Self::new(nums[lseq].unwrap()))));
queue.push_back(node.left.clone());
}
if rseq < size && nums[rseq].is_some() {
node.right = Some(Rc::new(RefCell::new(Self::new(nums[rseq].unwrap()))));
queue.push_back(node.right.clone());
}
}
index += 1;
}
}
root
}
// 將一棵樹序列化成一個數組
pub fn literal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<Option<i32>> {
if root.is_none() {
return vec![];
}
let mut ans = vec![];
let mut queue = VecDeque::new();
queue.push_back(root);
while !queue.is_empty() {
let qsize = queue.len();
for _ in 0..qsize {
match queue.pop_front().flatten() {
Some(x) => {
ans.push(Some(x.borrow().val));
queue.push_back(x.borrow().left.clone());
queue.push_back(x.borrow().right.clone());
}
None => ans.push(None),
}
}
}
let size = ans.len();
for i in (0..size).rev() {
if ans[i].is_none() {
ans.pop();
} else {
break;
}
}
ans
}
}
二叉樹樹形打印
有了二叉樹的反序列化,可以方便的構造test case。通常為了直觀驗證一棵樹,可以把樹的拓撲樹形進行打印。打印二叉樹需要使用二叉樹樹高,因為滿二叉樹的寬度等于 width = (1<<height) - 1
。
leetcode 655. 輸出二叉樹
pub fn print_tree(root: Option<Rc<RefCell<TreeNode>>>) -> String {
// 二叉樹高度
let height = TreeNode::get_height(&root);
// 滿二叉樹的寬度
let width = (1 << height) - 1;
let mut ans = vec![vec![" ".to_string(); width as usize]; height as usize];
// dfs 搜索
fn dfs(ans: &mut Vec<Vec<String>>, node: &Option<Rc<RefCell<TreeNode>>>, deep: usize, lo: usize, hi: usize) {
if let Some(x) = node {
let node = x.borrow();
let mid = lo + (hi - lo) / 2;
ans[deep][mid] = x.borrow().val.to_string();
dfs(ans, &node.left, deep + 1, lo, mid);
dfs(ans, &node.right, deep + 1, mid + 1, hi);
}
}
dfs(&mut ans, &root, 0usize, 0usize, width as usize);
// 將所有字符連起來
ans.iter().map(|x| x.concat()).collect::<Vec<_>>().join("\n")
}
上述打印的樹形二叉樹并沒有 path 的邊。需要變可以自行添加。
總結
通過rust構造二叉樹,需要使用到 rust的特性,如所有權,借用檢查,內部可變性等概念。另外掌握二叉樹的基本性質和遍歷方法,可以解決很多 leetcode上的題目。最后,通過二叉樹的反序列化和樹形打印,對刷題和學習驗證相關算法都很有幫助。
更多 Rust 與Leetcode可以參考這個文檔