二叉树算得上是数据结构中的一种基础结构,在二叉树的基本操作中集中了大量数据结构设计中的常用操作。但是由于Rust语言特性的设计,在其他寓言中十分方便就可以实现的功能,在Rust中就需要绕一段路,二叉树的实现就是这样的一个典型例子。
这篇文章主要的目的就是记录使用Rust实现二叉树数据结构的方法,以及使用Rust实现一些经典数据结构的时候所可能使用的上的一些注意要点和技巧。
定义基本结构
要实现一个二叉树结构,首先要实现的就是二叉树的基本结构。对于二叉树基本结构的组成在本博客的文章《二叉树基础》中有很详细的叙述,所以再这篇文章中就不再赘述了。
一个二叉树的基本结构是由数据节点、左孩子节点和右孩子节点三部分组成的。其中左孩子节点和右孩子节点都是指向另一个数据节点的指针,并如此往复循环就形成了一棵二叉树。但是在许多语言中,二叉树基本结构中的左孩子节点和右孩子节点都是指针,或者说是可变的,这也是完成二叉树的一些必要操作的基础。
但是对于Rust来说,这里就是无法绕开的一个最关键的点。在Rust中,推崇的是数据不可变,尤其是结构体的字段。而获取可变引用的限制条件又非常多,所以在如何能够实现一个可自由操作的左孩子节点和右孩子节点就是一个比较关键的问题了。
例如下面这种简单粗暴的实现方法必然是不可以的。
|
|
这种定义方式再Java或者C++中是没有问题的,但是再Rust中就会收到一条编译错误,提示你使用了递归类型。这是因为递归类型在Rust的编译期是无法确定其占用内存的大小的,而且在理论上,这种类型是可以占用无穷的内存的。所以左孩子节点和右孩子节点还是要使用指针或者引用类型,一个比较简单的选择是使用Box<TreeNode<T>>
类型。Box<T>
类型会将其中存储的内容放置在堆上,然后返回一个指向这个堆空间的指针,所以这种实现方式是可以的。
使用Box<T>
的实现就是下面的样子。
|
|
但是这里还有一个问题,一个二叉树的节点上它的左孩子节点和右孩子节点并不是始终都存在的,所以这个实现里的左孩子节点和右孩子节点还得使用Option<T>
包装一下。另外一个问题就是我们在访问这个节点持有的数据的时候,是不能将其转移走的,所以还需要使用Rc<T>
或者Cow<'a, T>
把节点持有的值包装一下,在这个示例中,选择使用Cow<'a, T>
来实现。
Arc<T>
来代替Rc<T>
。
|
|
现在再来看看这个实现,这个实现其实已经可以实现一棵二叉树的基本操作了。但是它依旧有一个问题,就是这棵树会持有它所有孩子节点的所有权,那么在完成一些需要共享数据访问的时候就会受到一些操作上的限制而不是那么方便,而且在变更孩子节点的时候是必须获取树的可变实例的。所以,这个二叉树的基本结构在定义的时候,还得利用Rust提供的结构体的内部可变性并且最好考虑树的所有节点能够支持方便的共享访问,那么现在这个二叉树的基本结构实现就会变成下面的样子。
|
|
在这个定义里,TreeNodeRef<T>
的类型别名定义只是为了可以简化很长的类型书写,并没有其他额外的意图。RefCell<T>
可以提供基于引用的内部可变性访问,这也是允许二叉树的两个孩子节点可以自由变换的根本。而将RefCell<T>
包装在Rc<T>
中,则是为了更加方便的提供孩子节点的共享使用。Rc<T>
最终使二叉树的循环数据结构能够完成创建。而且使用Rc<RefCell<T>>
做为孩子节点类型的另一个好处是孩子节点的生命期可以超过根节点的生命期。
parent
节点是非常有用的,很多遍历和操作都需要用到访问其父级节点功能。一个节点的父级节点可以在插入节点的时候设置。
关于Rc<RefCell<T>>
的操作技巧
如果只是访问本层节点的内容,Rc<RefCell<T>>
的结构基本上不会造成任何困扰,因为Rc<T>
和RefCell<T>
都实现了Deref
特征,所以只需要使用*
解引用操作符就可以利用Rust的智能解引用操作直接获取到T
的内容。
Deref
特征的类型,都可以直接使用*
解引用操作符来处理和访问。或者可以换句话说,任何实现了Deref
特征的类型,都是指针,例如常见的Rc
、Arc
、Cow
、Box
、Ref
、RefCell
、MutexGuard
等。所有实现了Deref
特征的类型,其实例都可以直接访问、调用被包装类型的成员以及成员方法。
但如果需要使用可变访问的话,那么就不太一样了。返回一个可变引用需要实现BorrowMut
特征,但是很不巧的,Rc<T>
是没有实现这个特征的,所以就如果想要获取&mut T
,就去必须先对Rc<RefCell<T>>
做一次解引用,获取到RefCell<T>
。但是RefCell<T>
也没有实现BorrowMut
特征,只是实现了一个borrow_mut()
方法,所以可以直接使用这个方法来获取到一个可变的结构。RefCell<T>
的borrow_mut()
方法获取到的可变结构是RefMut<T>
,这个类型实现了DerefMut
,所以就可以使用它来获取&mut T
了,也可以用来直接调用T
中需要&mut self
的方法。
所以在后面的示例中,想要从Rc<RefCell<T>>
类型中获取到&mut T
时,就需要首先获取到&RefCell<T>
,这一步需要对Rc<T>
解引用,然后再将其转换为一个引用,所以操作是以下样子。
|
|
如果想利用RefCell<T>
的内部可变性来修改Rc<RefCell<T>>
内部实际指向的值,那么就可以使用上面的方法先解引用Rc<T>
,再获取&RefCell<T>
或者RefMut<T>
来操作。只是需要注意的是,不需要Rc<T>
是可变的,也不需要&RefCell<T>
是可变的。
二叉树基本操作
二叉树的基本操作主要就是二叉树节点的插入、删除、变换以及二叉树节点的旋转、遍历等。所有更高级的二叉树操作都是由这几种基本操作组合而来的。
但是在实现所有二叉树的基本操作之前,还是先需要完成二叉树节点的创建操作。
|
|
访问节点
对于节点的访问,可以编写几个getter方法返回节点的引用。
|
|
Cow<T>
来说,使用&
获取的引用是&Cow<T>
,而使用AsRef
特征的.as_ref()
方法,获取到的是&T
。大多数泛型类型中&
和.as_ref()
的操作都有这样的区别,使用时需要注意。
以上示例中,右孩子节点的获取也是一样的。孩子节点的获取原理是利用Rc<T>
的引用,将孩子节点的引用复制一份,但是在复制之前需要首先对孩子节点可能存在的None
值进行重新的包装。
插入节点
插入节点的操作主要是需要判断所插入的孩子节点位置是否是空的,如果孩子节点是空的,那么只需要直接挂载上去即可,如果已经存在了孩子节点,那么就需要替换这个节点。
以下以操作左孩子节点为例。
|
|
删除节点
删除节点就非常容易了,原则上就是将需要删除的孩子节点设置为None
,然后返回孩子节点的所有权。
|
|
节点遍历
二叉树的遍历有前序、中序和后序三种,这里只使用中序遍历做示例。中序遍历的节点访问顺序是先访问左孩子节点,再访问根节点,然后再访问右孩子节点。节点的遍历一般都会有多种实现的方式,常见的有递归、递推、队列等,这里拣选递归方式来实现。
递归遍历方法是所有方法中代码量比较少的,也是实现比较简单的,但是递归方法消耗的系统资源会稍微多一些。
|
|
在这个示例中由于self.left
是一个Option<Rc<RefCell<T>>>
类型,所以需要使用if let Some()
对其进行解构,解构赋值中的ref left
则是将解构出的Rc<RefCell<T>>
变换为&Rc<RefCell<T>>
,可以保证解构判断语句不会转移self.left
的所有权。
小结
其实从上面这些复杂的解引用操作和包装类型操作代码中可以看出来,这个二叉树的操作实际上并不适合构建成一个结构体的成员方法,反而使用函数式操作会更加高效一些,以下集中一套集合了上述功能的函数式实现。
|
|
使用这个实现操作二叉树就比较方便了,例如这个文件被封装为了模块bt.rs
,二叉树的使用就看起来很清爽了。
|
|