1. 程式人生 > 實用技巧 >Rust 學習之基於 RefCell 的簡單二叉樹

Rust 學習之基於 RefCell 的簡單二叉樹

Rust 學習之基於 RefCell 的簡單二叉樹

最近,在力扣平臺刷題時,無意中刷到了一個關於二叉樹的題目:二叉樹的最小深度,打算使用 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>>>。其中的 RcRefCell 是 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(&current_node);
        if current_val == needle_val {
            return current_node;
        } else {
            // 比它小,則從左子樹查詢,否則從右子樹查詢
            if needle_val > current_val {
                current_node = Tree::get_rc(&current_node.unwrap().borrow().right);
            } else {
                current_node = Tree::get_rc(&current_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 和我聯絡~

reference