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 和我聯系~