A Tour of Rust - Binary Serach Tree
A Tour of Rust - Binary Search Tree
学习Rust已经有一段时间了,最近开始尝试用 Rust 进行一些简单的demo开发进行练手。今天尝试了一下,实现了第一个简单的 Binary Search Tree。必须承认,Rust 的入门曲线确实相当陡峭。本来我的预计是花费2~3小时实现,结果最后花了整整6个小时,比预计的时间超出一倍多。这里记录一下自己的学习心得以及体会。
代码实现
在实现代码的过程中,我使用了两种不同的开发思路:
- 基于传统的命令式编程语言,使用语言提供的各种控制流语句(如
if,while)来控制逻辑; - 基于Rust强大额模式匹配控制逻辑;
传统模式
在传统模式下,我们很常规的定义了一个二叉树,和大部分语言不同的是,Rust
并不提倡使用 NULL 指针,而提供了强大的 Option
来为我们实现流程控制。
pub struct Node<T>
where
T: Ord,
{
val: T,
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
在这种情况下,我们的 add 方法实现也并不复杂,代码如下:
pub fn new(root: T) -> Node<T> {
Node {
val: root,
left: None,
right: None,
}
}
// `self` must be `mut` rather than `&mut` or `&` due to :
// 1.1 the struct must be mutable because we are going to modify it;
// 1.2 when we modify a node, we take the ownership because it maybe changes.
pub fn add_self(mut self, val: T) -> Node<T> {
assert!(self.val != val);
if self.val < val {
self.right = Self::add_self_child(self.right, val);
} else {
self.left = Self::add_self_child(self.left, val);
}
self
}
fn add_self_child(child: Option<Box<Node<T>>>, val: T) -> Option<Box<Node<T>>> {
match child {
Some(node) => Some(Box::new(node.add_self(val))),
None => Some(Box::new(Node::new(val))),
}
}
Rust模式
Rust模式相当特别,他并不直接显式的使用结构体来表示我们的二叉树节点,而是用了
enum 来代替我们的节点。而在 enum 中,我们嵌套了我们所需要的
struct 以及一个用于替代 NULL 指针的枚举类型
Empty,这种方式也是在Rust中处理 Cons
list 的一种技巧:
pub enum RsNode<T: Ord>
{
Node {
val: T,
left: Box<RsNode<T>>,
right: Box<RsNode<T>>,
},
Empty,
}
实际的插入过程也相当简单:
pub fn new() -> RsNode<T> {
RsNode::Empty
}
pub fn create(val: T) -> RsNode<T> {
RsNode::Node {
val,
left: Box::new(RsNode::Empty),
right: Box::new(RsNode::Empty),
}
}
pub fn add(&mut self, new_value: T) {
match self {
RsNode::Node {
ref val,
ref mut left,
ref mut right,
} => match new_value.cmp(val) {
Ordering::Less => left.add(new_value),
Ordering::Greater => right.add(new_value),
Ordering::Equal => return,
},
RsNode::Empty => {
*self = RsNode::create(new_value);
}
}
}
为什么我会觉得使用Rust模式更加的简单
两种风格的代码实现,其实都相当简单,他们最大的区别也不仅仅在于后面的模式匹配,而是在于一个非常重要的思想:
使用
enum将空值作为对象的一部分。
例如,在第一个实现中,如果我们不使用一个工具函数,那么代码将会是这样:
// `self` must be `mut` rather than `&mut` or `&` due to :
// 1.1 the struct must be mutable because we are going to modify it;
// 1.2 when we modify a node, we take the ownership because it maybe changes.
pub fn add_self(&mut self, val: T) {
assert!(self.val != val);
if self.val < val {
match &mut self.right {
None => self.right = Some(Box::new(Node::new(val))),
Some(right) => right.add_self(val),
}
} else {
match &mut self.left {
None => self.left = Some(Box::new(Node::new(val))),
Some(left) => left.add_self(val),
}
}
}
我们内部持有的对象,在每次使用时都必须进行判断。简单来说就是:
如果
空值是对象的一部分,那么在对象内部的变量,我们就可以直接持有 对象本身 而不是持有Option<T>,这样我们可以把空值的判断合并到一个流,或者说合并到一个方法,而不是通过Option<T>对象去间接的访问:下图很好的说明了这点:
- 在传统模式下,
Option<Box<Node>>并不能直接调用方法add(),而是要通过Box<Node>;- 在Rust模式下,
Box<Node>可以直接调用add()方法。
---
title: Rust vs. Traditional
---
flowchart TD
subgraph Rust
direction LR
subgraph RsNode
direction LR
subgraph RustNode["Node"]
Box2["Box[Node]"]:::front
add2["add()"]:::front
end
Empty
end
end
subgraph Traditional
subgraph Node
Option1["Option[Box[Node]]"]:::op
add["add()"]:::front
end
Box1["Box[Node]"]:::front
end
Option1 --> Box1 --> Node
classDef front 1,fill:#FFCCCC,stroke:#333;
classDef back fill:#969,stroke:#333;
classDef op fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
classDef header fill: #696,color: #fff,font-weight: bold,padding: 10px;