Rust 學習之基於 RefCell 的簡單二叉樹
Rust 學習之基於 RefCell 的簡單二叉樹
- 作者:suhanyujie
- 來源:https://github.com/suhanyujie/rust-cookbook-note
- tags:Rust,binary-tree,Rc,RefCell
- tips:如有不當之處,還請指正~
最近,在力扣平臺刷題時,無意中刷到了一個關於二叉樹的題目:二叉樹的最小深度,打算使用 Rust 實現它。
不得不承認,我的思路有些死板。當我將該題的 project 新建好後,把預備程式碼準備完成,我是準備先進行資料的組裝,因為求二叉樹的最小深度的前提是你得有一棵”樹“,於是乎,參照“力扣”給出的節點資料結構,我開始實現”樹“的載入。
// 力扣給出的節點結構 // Definition for a binary tree node. #[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, } } } use std::cell::RefCell; use std::rc::Rc; struct Solution {} impl Solution { pub fn min_depth(root: Option<Rc<RefCell<TreeNode>>>) -> i32 { } }
在實現 min_depth 之前,我打算先實現樹的生成。
可以看出,實際上儲存時的節點結構是 Option<Rc<RefCell<TreeNode>>>
。其中的 Rc 和 RefCell 是 Rust 中的智慧指標。
Rc 是引用計數指標,通過 clone 的方式可以被多個變數擁有對應的引用所有權,如此導致的是儲存於 Rc 指標中的值是不可變的。如果我們要將值儲存到其中,如何做到呢?答案就是使用內部可變的 RefCell 指標。
準備工作
在開始寫程式碼之前,我們先用 cargo 建立一個專案:
// 假設我們的專案目錄名稱是 _111_minimum-depth-of-binary-tree cargo new --lib _111_minimum-depth-of-binary-tree cd _111_minimum-depth-of-binary-tree
此時 cargo 為你的專案生成了如下的目錄結構:
├── Cargo.lock
├── Cargo.toml
└── src
└── lib.rs
由於只是個比較小的程式碼庫,因此具體的程式碼實現可以直接寫在 lib.rs 檔案中。
二叉樹的生成
上面提到的“樹”的載入,其實就是指生成二叉樹的過程。簡單起見,我們以力扣中給定的示例資料為例,使用數字作為二叉樹的值。給定一個數組作為數節點的值:[3, 9, 20, 15, 7]
,生成一個樹前,先明確以下 2 點:
- 1.確定一個根節點,如果為空,則例項化一個節點作為樹的根節點 root
- 2.後續所有節點的插入,都以根節點 root 作為起始入口
生成一棵樹,我們先假設只有一個節點,入參是 [3]
。我們可以通過 TreeNode 的 new 函式例項化一個節點:
let node = TreeNode::new(3);
let root_op: Option<Rc<RefCell<TreeNode>>> = Some(Rc::new(RefCell::new(node)));
這只是簡單的將一個值包裝成根節點,實際情況下,我們會將一批資料加入到樹中,從而生成“茂盛”的樹狀結構。為此,我們一步一步來,先宣告一個 TreeTrait
trait,其中我們會宣告一些抽象方法,用於樹的初始化、節點的新增、刪除等:
trait TreeTrait {
// 例項化一棵樹
fn new(value: i32) -> Self;
// 插入
fn insert(self: &mut Self, value: i32) -> Result<i32, String>;
// 搜尋
fn search(self: &mut Self, value: i32) -> Option<Rc<RefCell<TreeNode>>>;
// 刪除
fn delete(self: &mut Self, value: i32) -> Result<i32, String>;
}
然後,我們需要宣告一個樹的結構 Tree
,併為它實現 TreeTrait
trait:
#[derive(Debug)]
struct Tree {
root: TreeNode,
length: u32,
}
impl TreeTrait for Tree {
fn new(self: &mut Tree, value: i32) -> Option<Rc<RefCell<TreeNode>>> {
todo!()
}
fn insert(self: &mut Tree, value: i32) -> Option<Rc<RefCell<TreeNode>>> {
todo!()
}
fn search(self: &mut Self, value: i32) -> Option<Rc<RefCell<TreeNode>>> {
todo!()
}
fn delete(self: &mut Self, value: i32) -> Result<i32, String> {
todo!()
}
}
輔助方法
在開始執之前,需要做些準備一些東西 ——— 輔助方法。由於節點的型別是 Option<Rc<RefCell<TreeNode>>>
,再加上 Rust 語法的所有權、借用等問題,會導致取數值比較、引數傳遞時不是很方便,因此我們編寫一些方法,簡化開發過程中的呼叫。
獲取 Rc 引用
職能指標 Rc,可以認為是對某個資料的引用,我們可以通過 Rc::clone()
的方式複製多份引用,賦值給多個變數,這樣可以實現多個變數都指向同一個“樹節點”,因為獲取引用的呼叫會比較繁瑣,因此我們將其封裝為方法 get_rc()
,放在 impl Tree 塊中,其實現如下:
impl Tree {
fn get_rc(rc_rc: &Option<Rc<RefCell<TreeNode>>>) -> Option<Rc<RefCell<TreeNode>>> {
if let Some(ref new_node_rf) = *rc_rc {
let new_rc = Rc::clone(new_node_rf);
Some(new_rc)
} else {
None
}
}
}
通過節點獲取對應的值
還好,因為此次為了簡化實現過程,節點儲存的的資料是簡單的 i32 型別,它是可 Copy 的,我們通過一個函式用於獲取型別為 Option<Rc<RefCell<TreeNode>>>
的變數的值,並將其放在 impl Tree 塊中:
impl Tree {
fn get_val(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
let rc = Tree::get_rc(node);
return rc.unwrap().borrow().val;
}
}
編寫測試用例測試一下它的功能:
#[test]
fn test_get_val() {
let node = Some(Rc::new(RefCell::new(TreeNode::new(3))));
assert_eq!(3, node.unwrap().borrow().val);
}
樹的例項化
我們會為 Tree 型別實現構造方法 new:
// 返回的是包裝後的根節點
fn new(value: i32) -> Tree {
let node = TreeNode::new(value);
Tree {
root: Some(Rc::new(RefCell::new(node))),
length: 1,
}
}
我們可以寫個測試用例,通過整合測試來對功能程式碼檔案中的函式進行測試,主要是看例項化的樹的節點和數量是否和期望的一致:
#[test]
fn test_tree_new() {
let tree = Tree::new(3);
let v1 = tree.root.unwrap().borrow().val;
assert_eq!(3, v1);
assert_eq!(1, tree.length);
}
新增節點
例項化一個只帶有根節點的樹後,我們還需要將更多的資料加入到樹中,因此我們實現 Tree 的 insert 方法。需要注意的是,這裡我們還是遵循二叉樹的以下性質:二叉樹的左節點小於其父節點的值,右子節點值大於根其父節點。insert 實現如下:
// 節點的新增
fn insert(self: &mut Tree, value: i32) -> Result<i32, String> {
let root = Tree::get_rc(&self.root);
let mut current_node = root;
// 宣告一個臨時變數,用於賦值給 current_node
let mut current_node_tmp: Option<Rc<RefCell<TreeNode>>>;
// 使用新的值例項化新的節點
let new_node = Some(Rc::new(RefCell::new(TreeNode::new(value))));
loop {
match current_node {
Some(ref node_rf) => {
let mut node_tr = node_rf.borrow_mut();
let new_node_val = if let Some(ref new_node_rf) = new_node {
let new_node_tr = (&new_node_rf).borrow();
new_node_tr.val
} else {
return Err("the TreeNode's value is invalid...".to_string());
};
if new_node_val > node_tr.val {
if node_tr.right == None {
node_tr.right = new_node;
self.length += 1;
return Ok(1);
} else {
// 獲取 right 值的 rc 引用
current_node_tmp = Tree::get_rc(&(node_tr.right));
}
} else {
if node_tr.left == None {
node_tr.left = new_node;
self.length += 1;
return Ok(1);
} else {
// 獲取 right 值的 rc 引用
current_node_tmp = Tree::get_rc(&(node_tr.left));
}
}
}
_ => {
return Err("insert error".to_string());
},
}
current_node = current_node_tmp;
}
}
當插入成功時,返回正確的 code 程式碼 1,如果異常,則返回 String 型別的異常資訊。測試用例如下:
#[test]
fn test_insert() {
let mut tree = Tree::new(3);
if let Ok(code) = tree.insert(4) {
assert_eq!(1, code);
} else {
panic!("insert error")
}
let arr = vec![9,6,10,11,5];
for val in arr {
match tree.insert(val) {
Ok(code) => assert_eq!(1, code),
Err(msg) => {
println!("{:?}", msg);
assert!(false);
}
}
}
// 3,4,9,6,10,11,5
assert_eq!(7, tree.length);
}
搜尋節點
二叉樹的典型場景就是查詢,在這裡,就是給定一個 i32 型別的值,我們從已知的二叉樹中查詢該值是否存在。實現如下:
fn search(self: &mut Self, value: i32) -> Option<Rc<RefCell<TreeNode>>> {
let mut current_node = Tree::get_rc(&self.root);
let needle_node = Some(Rc::new(RefCell::new(TreeNode::new(value))));
let needle_val = Tree::get_val(&needle_node);
loop {
let current_val = Tree::get_val(¤t_node);
if current_val == needle_val {
return current_node;
} else {
// 比它小,則從左子樹查詢,否則從右子樹查詢
if needle_val > current_val {
current_node = Tree::get_rc(¤t_node.unwrap().borrow().right);
} else {
current_node = Tree::get_rc(¤t_node.unwrap().borrow().left);
}
}
if current_node == None {
break;
}
}
return None;
}
利用 Rust 標準庫中的 Option 列舉,我們可以將該方法設計為,當查詢到的時候,返回 Option 包裝的節點指標;未查詢到時,則返回 None。用測試用例測試它:
#[test]
fn test_search() {
let mut tree = Tree::new(3);
let arr = vec![9,6,10,11,5];
for val in arr {
match tree.insert(val) {
Ok(code) => assert_eq!(1, code),
Err(msg) => {
println!("{:?}", msg);
assert!(false);
}
}
}
let needle = tree.search(10);
assert_eq!(10, needle.unwrap().borrow().val);
}
刪除節點
emmmm,作為練習,刪除節點的實現,就交給讀者們自己去實現(我不會告訴你們,其實是我不會寫...)。
conclusion
至此,基於 RefCell 的二叉樹就基本實現了。作為 Rust 新手,我只是用一些簡單的方式來實踐已知的知識,無論是鞏固歷史知識,還是對練習 Rust 都是有很多幫助。
誠然,本文描述的是非常簡單的場景,實際使用是,我們的資料不可能只是簡單的 i32,而可能是字串、結構體或者一些其他型別資料。而在二叉樹儲存複雜資料的場景中,我們還需要手動實現資料的判等、複製等操作。在後續的筆記中,我們會慢慢講解到。
文中提到的所有程式碼都能在 GitHub 上找到。此外,如果文章有不當之處,或者想和我交流,歡迎提 issue 和我聯絡~