二叉查找树和前面所过的二叉堆在实现存在很大区别,在二叉搜索树(Binary Search Tree或者简称BST)中,任意节点的左子树中的所有节点的值要小于其右子树中的所有节点的值。
二叉搜索树的性质:
- 任一当前节点的左子树包含的节点值小于当前节点。
- 任一当前节点的右子树包含的节点值大于当前节点。
- 任意两个节点不能有重复的值。(这个性质非常重要,它使得BST可以构架另外一个数据结构Set,Set中每个元素都是唯一的。)
- 除了叶子节点外,任意节点左子树和右子树也必须满足BST性质。
二叉搜索树的上述属性提供了节点值之间的排序关系,从而可以快速完成搜索、最小和最大等操作。但事实上,BST构建的排序逻辑事实上属于分治排序思想的其中一种,为什么这么说:
- 也就是说从BST的根节点为轴(Pivot)。左半分区就是根节点左子树,右半分区是根节点的右子树,这个操作我们叫分区(Partition).
- 分治排序的整个算法需要依次对左子数或右子树逐层递归执行。
但BST的不强制任意一个路径的所有节点的值都严格依照降序或升序排列,这有异于分治排序算法的。例如下图的完整二叉树,同时也是一个BST,我们看看具体的例子{32、16、11、23}和{32,16,11,31},这两条路径上的节点都不完全排序。
但我们换个角度去思考一下,如果我们设定两个参照条件。
- 条件1:锁定第h层对应的根节点。
- 条件2:限定每层遍历的方向:要么是一路按左节点遍历,要么一路按右节点遍历。
那么有趣的事情发生了
例如我以值为32的节点为根,一路left遍历,依次是降序排列的:
![equation?tex=32++%5Crightarrow16%5Crightarrow8%5Crightarrow7%5Crightarrow+NULL](https://i-blog.csdnimg.cn/blog_migrate/5a04ffe4e1124b9158e724fa5137e191.png)
例如我以值为16的节点为根,一路right遍历,依次是升序排列的:
![equation?tex=16%5Crightarrow8%5Crightarrow17%5Crightarrow31%5Crightarrow+NULL](https://i-blog.csdnimg.cn/blog_migrate/37c6dea1c3676c892408a4e91e1eba28.png)
例如我以值为32的节点为根,一路right遍历,依次是升序排列的:
![equation?tex=32%5Crightarrow42%5Crightarrow53%5Crightarrow55%5Crightarrow+NULL](https://i-blog.csdnimg.cn/blog_migrate/4b66c61fc0b8b5c011496e2449937c9f.png)
![986cb6cb1c9b0cec7bc507b07d289de7.png](https://i-blog.csdnimg.cn/blog_migrate/31e43e2f5ee5ad33d03cb8f8de771dbd.jpeg)
这里可以总结出BST的另外一些特性。
当明确第h层的参照节点为根
![equation?tex=N](https://i-blog.csdnimg.cn/blog_migrate/390824601cf6d50933744af0e4ccf208.png)
- 性质1:往左节点方向逐层遍历到叶子节点
- 性质2:往右节点方向逐层遍历到叶子节点
BST的类接口设计
我们知道BST构成的基本要素是节点,因此请查看如下BSNode类定义的代码清单
#ifndef __BSTREE_HH__
#define __BSTREE_HH__
#include <iostream>
//BSTree类模板声明
template<class T>
class BSTree;
//BST迭代器类模板声明
template<class T>
class BSTreeIterator;
template<class T>
class BNode{
friend class BSTree<T>;
private:
BNode<T>* d_parent; //父节点
BNode<T>* d_left; //左子节点
BNode<T>* d_right; //右子节点
T d_data; //数据域
public:
BNode(T val){
d_data=val;
d_parent=nullptr;
d_left=nullptr;
d_right=nullptr;
}
~BNode(){
d_parent=nullptr;
d_left=nullptr;
d_right=nullptr;
}
};
BST容器的类接口
在前面的ArrayList、LinkedList、Queue篇章,笔者多次强调过“容器”这个概念,这也是C++的std代码库组织内置所有数据结构的通用的编程风格。BST容器封装了BST的根、以及增、删、改、查等API的核心算法。
template<class T>
class BSTree{
private:
BNode<T>* d_root; //整个树的根
int d_height; //当前树的高度
int d_size; //当前元素的高度
//内部递归插入算法
BNode<T>* insertRcu(BNode<T>*,T);
void clear(BNode<T>*);
bool search(BNode<T>*,const T&);
//最大高度
int maxHeight(BNode<T>*);
//级别遍历所有节点
void level_visit(BNode<T>*);
//遍历某层的节点
void level_visit(BNode<T>*,ArrayList<T>*,int);
//先序遍历
void pre_visit(BNode<T>*);
//按序遍历
void ord_visit(BNode<T>*);
//后序遍历
void post_visit(BNode<T>*);
public:
BSTree();
//构造函数
BSTree(const T* arr,int size);
//析构函数
~BSTree();
//返回整个二叉树的根
BNode<T>* getRoot();
//返回当前传入节点的上一级的根
BNode<T>* getRoot(const BNode<T>*);
//BST的高度
int height();
//BST的节点数
int size();
//插入算法
void insert(T);
//查找
bool find(const T&);
//删除
void remove(const T&);
//最小值
T min();
//最大值
T max();
//修改
void update(const T&,T);
//级别遍历
ArrayList<T>* level_traversal(int);
//后续遍历
ArrayList<T> post_traversal();
//前序遍历
ArrayList<T> pre_traversal();
//顺序遍历
ArrayList<T> order_traversal();
template<typename R>
friend std::ostream &operator<<(std::ostream &,const BSTree<R> &);
};
BST的节点实现
节点对象BNode除定义常规的左子节点、右子节点之外,我们还定义了一个d_parent属性,就是用于描述每个节点它所指向的父节点,这样做的目地是方便在查找或遍历操作向更低层的节点回溯。
需要注意的是,作为树的根节点,它是没有父节点。因此约定父节点的d_parent指定为nullptr。这样用于方便标识它是整个BST的根。
template<class T>
class BSTree;
template<class T>
class BNode{
friend class BSTree<T>;
private:
BNode<T>* d_parent; //父节点
BNode<T>* d_left; //左子节点
BNode<T>* d_right; //右子节点
T d_data; //数据域
public:
BNode(T val){
d_data=val;
d_parent=nullptr;
d_left=nullptr;
d_right=nullptr;
}
~BNode(){
d_parent=nullptr;
d_left=nullptr;
d_right=nullptr;
}
};
其他类接口
对BST对象的操作可能产生异常,因此这里定义了一个异常类,用于抛出一些文本信息。
class OperationException:public std::exception{
private:
std::string d_mesg;
public:
OperationException(const std::string &mesg):d_mesg(mesg){}
~OperationException(){}
std::string getMessage(){
return d_mesg;
}
};
#endif
BST的高度计算
当BST满足如下图右斜的二叉树形态,会有如下特征
- 若BST的节点树明确为
,那么其最大高度为
,这种形态满足上述BST的性质
- 若BST的高度明确为
,则最小节点树为
![688385375c9526d12ff3984b75477f84.png](https://i-blog.csdnimg.cn/blog_migrate/de1e03475ae0e590db272cc222f82b43.jpeg)
当BST满足完整二叉树的形态,会有如下特征
- 若BST的节点树明确为
,那么最小高度为
- 若BST的高度明确为h,并所有级别完全填满时,其最大节点数为
但是由于BST经常用于增、删、改、查的操作,经常偏向于完满二叉树的形态(概念还没搞清楚请自行查看前面的二叉树概念篇)。那么上面提到的特殊形态下的高度计数,我们不会用代码实现它们,因为缺乏通用性。取而代之的是。使用递归遍历方式去获取BST的最大高度(即BST的高度)和最小高度。
其思想是递归计算任意一个节点的左子树和右子树的高度,那么该节点的高度(用
![equation?tex=H_%7Bn%7D](https://i-blog.csdnimg.cn/blog_migrate/57853db284ab71571fe6ac526b79781b.png)
![equation?tex=H_%7Bl%7D](https://i-blog.csdnimg.cn/blog_migrate/fe46471669ed160c557f64d9e88f50ea.png)
![equation?tex=H_%7Br%7D](https://i-blog.csdnimg.cn/blog_migrate/3221e0310f95f5d52a5836ff8e6e4764.png)
![equation?tex=H_%7Bn%7D%3Dstd%3A%3Amax%28H_%7Bl%7D%2CH_%7Br%7D%29%2B1](https://i-blog.csdnimg.cn/blog_migrate/57853db284ab71571fe6ac526b79781b.png%3Dstd%3A%3Amax%28H_%7Bl%7D%2CH_%7Br%7D%29%2B1)
举个例子下图,以值32的节点为根,它的左子树的高度是1,右子树的高度为0。注意这里计算BST的高度,笔者始终坚持是以节点之间的边缘计数为准。要抬杠,请自行滚~!!,那么以32的节点为根的BST高度等于左子数的高度+1,也就是2.
![751abdaef4e052ede50aa6e9fa40537e.png](https://i-blog.csdnimg.cn/blog_migrate/ed909f5c741088eeeaf43d22c9ae7e27.jpeg)
当然,熟悉了二叉树的高度概念,明眼一看就能辨别出来。但新手去理解递归计算高度的算法往往是另外一回事。说白一点,站在计算机角度怎么计算左子树的高度?
![da44d5f409b2e51c508d3ae328c05eef.png](https://i-blog.csdnimg.cn/blog_migrate/fe1dad7fdab7c04cd15d142bdfcc2eb2.jpeg)
以值8的节点为根的二叉树高度是多少?其递归算法如何去理解的呢?首先有个概念必须搞懂的是叶子节点也是整棵BST中的子树,只不过它的子节点皆为空节点。我们明确约定过,空节点或叫空树,它的高度为-1。那么叶子节点的高度计算逻辑就自然是这样的
首先,左子节点的NULL节点的高度为-1,右子节点的高度同为-1。那么套用公式
![equation?tex=H_%7B8%7D%3Dstd%3A%3Amax%28-1%2C-1%29%2B1%3D-1%2B1%3D0](https://i-blog.csdnimg.cn/blog_migrate/65a48befecf9cd95c0c71b8db8d027dd.png)
同理,值为17的叶子节点的高度为
![equation?tex=H_%7B17%7D%3Dstd%3A%3Amax%28-1%2C-1%29%2B1%3D-1%2B1%3D0](https://i-blog.csdnimg.cn/blog_migrate/752dcdbc6b42531a9bfea2f442d767ee.png)
同理,值为42的叶子节点的高度为
![equation?tex=H_%7B42%7D%3Dstd%3A%3Amax%28-1%2C-1%29%2B1%3D-1%2B1%3D0](https://i-blog.csdnimg.cn/blog_migrate/267b60451df60e7eb35cc349fb4e98c7.png)
那现在以值16为根的子树的高度为何等于1,你应该知道怎么计算了吧!!没错,就是这样。
![equation?tex=H_%7B17%7D%3Dstd%3A%3Amax%28H_%7B8%7D%2CH_%7B17%7D%29%2B1%3D0%2B1%3D1](https://i-blog.csdnimg.cn/blog_migrate/913a5f8dfca0b4e14ba1db2874076743.png)
理所当然,以值32为根的BST高度的就是
![equation?tex=H_%7B32%7D%3Dstd%3A%3Amax%28H_%7B16%7D%2CH_%7B42%7D%29%2B1%3D1%2B1%3D2](https://i-blog.csdnimg.cn/blog_migrate/cbaa89a7b4062e36241c148c2a14b64b.png)
如果上面的BST递归计算高度公式,你理解了的话,那么其C++实现的代码如下
- 递归获取左子树的最大深度
- 递归获取右子树的最大深度
- 获取左右子树的最大深度的最大值,并将当前节点下属子树的最大高度+1作为结果返回。
//计算BST高度
template<class T>
int BSTree<T>::height(){
return maxHeight(d_root);
}
//内部递归最大高度
template<class T>
int BSTree<T>::maxHeight(BNode<T>* node){
if(node==nullptr) return -1;
else{
int leftDept=maxHeight(node->d_left);
int rightDept=maxHeight(node->d_right);
return (leftDept>rightDept)?leftDept+1:rightDept+1;
}
}
还有一个等效的算法,和上面的代码相比就精简许多,只不过可能多几次max函数调用
template<class T>
int BSTree<T>::maxHeight(BNode<T>* node){
return node==nullptr?0:std::max(maxHeight(node->d_left)+1,
maxHeight(node->d_right)+1);
}
//计算BST高度
template<class T>
int BSTree<T>::height(){
return maxHeight(d_root)-1;
}
根据前篇我们对二叉树有了详细的理解之后,我们自然能够得出一个关于BST实现的接口定义。
template<class T>
class BSTree{
private:
BNode<T>* d_root; //整个树的根
int d_height; //当前树的高度
int d_size; //当前元素的高度
//内部递归插入算法
BNode<T>* insertRcu(BNode<T>*,T);
void clear(BNode<T>*);
public:
BSTree();
BSTree(const T* arr,int size);
~BSTree();
//返回整个二叉树的根
BNode<T>* getRoot();
//返回当前传入节点的上一级的根
BNode<T>* getRoot(const BNode<T>*);
//
int height();
int size();
//先序遍历
void pre_visit(const BNode<T>*);
//按序遍历
void ino_visit(const BNode<T>*);
//后序遍历
void pst_visit(const BNode<T>*);
//插入算法
void insert(T);
bool find(const T&);
template<typename R>
friend std::ostream &operator<<(std::ostream &,const BSTree<R> &);
};
class OperationException:public std::exception{
private:
std::string d_mesg;
public:
OperationException(const std::string &mesg):d_mesg(mesg){}
~OperationException(){}
std::string getMessage(){
return d_mesg;
}
};
#endif
构造函数
这个构造函数主要调用了insert批量插入传入数组的所有元素。这个操作的总的时间为
![equation?tex=O%28n%5E%7B2%7D%29](https://i-blog.csdnimg.cn/blog_migrate/745be8c743daa10d28c8edf5dca060b9.png)
![equation?tex=O%28n%29](https://i-blog.csdnimg.cn/blog_migrate/b1e1fe9d316f093d33735a5259d8e5e5.png)
![equation?tex=O%28n%29](https://i-blog.csdnimg.cn/blog_migrate/b1e1fe9d316f093d33735a5259d8e5e5.png)
template<class T>
BSTree<T>::BSTree(const T* data,int size){
//创建根节点
if(size>0){
d_root=new BNode<T>(data[0]);
for (size_t i = 1; i < size; i++)
{
insert(data[i]);
}
}
}
析构函数
这个算法就是通过递归clear,从叶子节点开始向BST的根依次对各个BNode对象执行内存释放。
//析构函数
template<class T>
BSTree<T>::~BSTree(){
if(d_root!=nullptr){
clear(d_root);
}
}
//内存释放所有节点
template<class T>
void BSTree<T>::clear(BNode<T>* cur){
if(cur==nullptr) return;
clear(cur->d_left);
clear(cur->d_right);
delete cur;
}
插入算法
例如从数组{50,34,19,45,73,59,82}依次向BST对象插入新的节点,如下图所示,插入算法需要完成如下动作
- 插入一个新的节点,意味着我们需要实例化一个BNode对象
- 在插入算法实现中,需要从根节点一直递归查找合适的叶子节点。
- 当找到叶子节点,新节点会成为原来叶子节点的一个子节点。
![1bfd2e1b4a51368bc78aa3ef2e65bfce.gif](https://i-blog.csdnimg.cn/blog_migrate/9ad3a636b47a18112610ea76d9f3447d.gif)
insert的算法实现,如下代码所示。插入操作的最坏情况时间复杂度是O(n),这完全取决于是二叉搜索树的高度。在最坏的情况下,我们可能不得不从根到最深的叶节。
template<class T>
void BSTree<T>::insert(const T value){
d_root=insertRcu(d_root,value);
}
//内部递归操作
template<class T>
BNode<T>* BSTree<T>::insertRcu(BNode<T>* node,T value){
//递归到最高层的
if(node==nullptr){
node=new BNode<T>(value); //生成一个叶子节点
return node;
}
//如果新节点的值少于当前节点的值,
//会将新节点作为当前节点的左子节点
if(value<node->d_data){
node->d_left=insertRcu(node->d_left,value);
if(node->d_left->d_parent==nullptr){
node->d_left->d_parent=node;
}
}
//如果新节点的值大于当前节点的值,
//会将新节点作为当前节点的右子节点
else if(value>node->d_data){
node->d_right=insertRcu(node->d_right,value);
if(node->d_right->d_parent==nullptr){
node->d_right->d_parent=node;
}
}
//遇到重复的节点值直接忽略
else if(value==node->d_data){
return;
}
return node;
}
值得提醒:如果用C语言来实现BST的插入算法,其代码会令人非常恶心,因为涉及BST内部的指针操作完全暴露在用户代码,因此笔者使用C++,对这些指针操作封装为BST类内部的一个私有方法insertRcu,它接受一个指向BNode类型的指针,以及一个泛型T类型的数值。java、C#版本实现的插入算法也会非常优雅。
而insert方法是一个公开给用户代码调用的接口,它内部实质上调用insertRcu这个递归版本的插入算法。
查找操作
要在BST中查找一个给定值n,首先需要和根节点的值对比,如果给定的值在根就返回true;如果给定的值少于根节点值就递归遍历查找左子树内的节点,反之查找递归查找根节点的右子树内的节点。
这种查找算法叫二分查找法或折半查找法,每往深一层查找,规模量N减半。这是一种典型的分区查找算法中的一种典型例子。 这种算法思想在已排序的线性表中应用非常频繁。当笔者写到算法时再讨论其细节。
//内部递归查找算法
template<class T>
bool BSTree<T>::search(BNode<T>* node,const T& data){
if(node==nullptr){
return false;
}else if(node->d_data==data){
return true;
}else if(data<=node->d_data){
return search(node->d_left,data);
}else{
return search(node->d_right,data);
}
}
//公开的查找算法
template<class T>
bool BSTree<T>::find(const T& data){
return search(d_root,data);
}
同理,我这里的实现也是使用一个私有版本的search封装了可能暴露在用户代码的指针操作。公开的查找方法是find。
这里我们讨论一下BST的时间复杂度。假设我们从搜索空间中的“n”个节点的BST,我们可以做一些逻辑上的推算。
其时间复杂度就是我们熟知的
![equation?tex=O%28log%5C+n%29](https://i-blog.csdnimg.cn/blog_migrate/ef4a83703a048d70744baf8492a8e2ac.png)
- BST是倾斜树的形态。
- 当查找的元素位于叶子节点,这种情况我们不妨做一个逻辑演算。当一个具有n个节点的BST,我们依次从0层到第n-1层遍历
从第0层,我们将搜索空间缩小到
![equation?tex=%5Cfrac%7Bn%7D%7B2%7D](https://i-blog.csdnimg.cn/blog_migrate/aa170fe6526090c335b3eb0dca4eb2fc.png)
第1层,搜索规模缩小为
![equation?tex=%5Cfrac%7Bn%7D%7B4%7D%3D%5Cfrac%7Bn%7D%7B2%5E%7B2%7D%7D](https://i-blog.csdnimg.cn/blog_migrate/8124675e66cddde7b9d73d6c37c7a2ec.png)
第2层,搜索规模缩小为
![equation?tex=%5Cfrac%7Bn%7D%7B8%7D%3D%5Cfrac%7Bn%7D%7B2%5E%7B3%7D%7D](https://i-blog.csdnimg.cn/blog_migrate/0f338bbb8869a98df832dc59fad220f6.png)
第3层,搜索规模缩小为
![equation?tex=%5Cfrac%7Bn%7D%7B16%7D%3D%5Cfrac%7Bn%7D%7B2%5E%7B4%7D%7D](https://i-blog.csdnimg.cn/blog_migrate/0430cb81bade278dba08c8d743b62f56.png)
....
第n-1层,搜索规模缩小为
![equation?tex=%5Cfrac%7Bn%7D%7B2%5E%7Bn%7D%7D](https://i-blog.csdnimg.cn/blog_migrate/dd9c64f07099bb948c75dd0739da6df2.png)
![equation?tex=T%28n%29%3D%5Cfrac%7Bn%7D%7B2%5E%7B1%7D%7D%2B%5Cfrac%7Bn%7D%7B2%5E%7B2%7D%7D%2B%5Cfrac%7Bn%7D%7B2%5E%7B3%7D%7D%2B...%2B%5Cfrac%7Bn%7D%7B2%5E%7Bn%7D%7D%3Dn%5Ccdot%28%5Cfrac%7B1%7D%7B2%5E%7B1%7D%7D%2B%5Cfrac%7B1%7D%7B2%5E%7B2%7D%7D%2B%5Cfrac%7B1%7D%7B2%5E%7B3%7D%7D%2B...%2B%5Cfrac%7B1%7D%7B2%5E%7Bn%7D%7D%29](https://i-blog.csdnimg.cn/blog_migrate/2dc8b7ab558a414cec05da4602bae64b.png)
那么其时间复杂度的最终形式
![equation?tex=T%28n%29%3Dn%5Ccdot%281%2B%5Cfrac%7B1%7D%7B2%7D%2B%5Cfrac%7B1%7D%7B2%5E%7B2%7D%7D%2B%5Cfrac%7B1%7D%7B2%5E%7B3%7D%7D%2B...%2B%5Cfrac%7B1%7D%7B2%5E%7Bn-1%7D%7D%2B%5Cfrac%7B1%7D%7B2%5E%7Bn%7D%7D%29%3Dn%5Ccdot%281-%5Cfrac%7B1%7D%7B2%5E%7Bn%7D%7D%29](https://i-blog.csdnimg.cn/blog_migrate/e2ae2cee78c7aea35baff140474d7d4a.png)
那么当
![equation?tex=n%5Crightarrow+%2B%5Cinfty](https://i-blog.csdnimg.cn/blog_migrate/c90bc9e1231ebc5113015d5fb7998767.png)
![equation?tex=T%28n%29%3DO%28n%29](https://i-blog.csdnimg.cn/blog_migrate/1dd09004e36562514d628f81711251df.png)
看到这里,应该会有人跟我抬杠了,你这不是应该这么表示吗 ?
![equation?tex=%5Csum_%7Bn%3D1%7D%5E%7B%5Cinfty%7D%7B%5Cfrac%7Bn%7D%7B2%5E%7Bn%7D%7D%7D%3D%5Cfrac%7B1%7D%7B2%5E%7B1%7D%7D%2B%5Cfrac%7B2%7D%7B2%5E%7B2%7D%7D%2B%5Cfrac%7B3%7D%7B2%5E%7B3%7D%7D%2B...%2B%5Cfrac%7Bn%7D%7B2%5E%7Bn%7D%7D%3D2](https://i-blog.csdnimg.cn/blog_migrate/8cfc8b0261a7f49b8f2df0ee449baf9b.png)
如果存在这种疑问的话,烦请搞清楚笔者所设定的前提条件,这里n是一个某个时刻规模量非常大的定量。
![equation?tex=%5Csum_%7Bn%3D1%7D%5E%7B%5Cinfty%7D%7B%5Cfrac%7Bn%7D%7B2%5E%7Bn%7D%7D%7D](https://i-blog.csdnimg.cn/blog_migrate/88e8c1c144a339b3462ad3c3d7fca64b.png)
![equation?tex=T%28n%29](https://i-blog.csdnimg.cn/blog_migrate/4220d63a3c7828e72f2522e64cf126f9.png)
小结
我们已经实现了部门BST的内部算法,剩下的留到下一篇再讨论。
有空再更新....