二叉树
是树结构中具有艳明特点的子类。
二叉树
要求树的每一个结点(除叶结点)的子结点最多只能有 2
个。在二叉树
的基础上,继续对其进行有序限制则变成二叉排序树
。
二叉排序树特点:
基于二叉树
结构,从根结点开始,从上向下,每一个父结点的值大于左子结点(如果存在左子结点)的值,而小于右子结点(如果存在右子结点)的值。则把符合这种特征要求的树称为二叉排序树
。
构建二叉排序树
:
如有数列 nums=[5,12,4,45,32,8,10,50,32,3]
。通过下面流程,可以把数列中的数字映射到二叉排序树
的结点上。
5
作为根结点。4
插入到左边,数字 12
插入到右边。原始数列中的数字是无序的,根据二叉排序树
的插入算法,最终可得到一棵有排序性质的树结构。对此棵树进行中序遍历
就可得到从小到大的一个递增有序数列。
综观二叉排序树
,进行关键字查找时,也应该是接近于二分查找算法的时间复杂度。
Tips:原始数列中的数字顺序不一样时,生成的二叉排序树的结构也会有差异性。对于查找算法的性能会产生影响。
使用OOP
设计方案描述二叉排序树的数据结构。
template<typename T>
struct TreeNode{
//结点上的值
T value;
//左子结点
TreeNode *lelfChild;
//右子结点
TreeNode * rightChild;
//无参构造
TreeNode(){
this->lelfChild=NULL;
this->rightChild=NULL;
}
//有参构造
TreeNode(T val){
this->value=val;
this->lelfChild=NULL;
this->rightChild=NULL;
}
//有参构造
TreeNode(T val,TreeNode *leftChild,TreeNode *rightChild){
this->value=val;
this->lelfChild=leftChild;
this->rightChild=rightChild;
}
};
template<typename T>
class BinarySortTree {
private:
//根结点
TreeNode<T> * root;
//树的大小
int size;
public:
//无参构造函数
BinarySortTree() {
//初始根结点为NULL
this->root=NULL;
this->size=0;
}
//有参构造函数,初始化根结点
BinarySortTree(T value) {
this->root=new TreeNode<T>(value);
this->size++;
}
//返回根结点
TreeNode<T> *getRoot() {
return this->root;
}
//析构函数
~BinarySortTree();
//查询是否存在给定的关键字
TreeNode<T> * findByVal(T value);
//基于递归查询
TreeNode<T> * findByVal(TreeNode<T> * node,T value) ;
//插入新结点
TreeNode<T> * insert(T value) ;
//基于递归的前序遍历
void preOrder(TreeNode<T> * node) ;
//基于递归的中序遍历
void inOrder(TreeNode<T> * node) ;
//基于递归的后序遍历
void postOrder(TreeNode<T> * node) ;
//非递归的前序遍历
void preOrderByStack();
//非递归的中序遍历
void inOrderByStack();
//非递归的后序遍历
void postOrderByStack();
//删除
bool deleteNode(T val);
};
插入新结点之前,需要为新结点找到一个合适的位置。所以,先要讨论在树中如何查找指定的结点。
查找的基本思路:
查找函数的实现:
下面提供递归和非递归 2
种方案,如果存在要查找的结点,返回此结点,如果没有查找,则返回最后访问过的结点。
//查询是否存在给定的关键字
template<typename T>
TreeNode<T> * BinarySortTree<T>::findByVal(T value) {
//从根结点开始比较
TreeNode<T> *move=BinarySortTree<T>::root;
//保留最后访问过的结点
TreeNode<T> *last=NULL;
while(move!=NULL) {
last=move;
if(move->value==value) {
//找到
return move;
} else if(move->value>value) {
//在左子树上查找
move=move->leftChild;
} else {
//在右子树上查找
move=move->rightChild;
}
}
return last;
}
//基于递归查询
template<typename T>
TreeNode<T> * BinarySortTree<T>::findByVal(TreeNode<T> * node,T value) {
if (node->value==value)
return node;
else if (node->value>value) {
if(node->leftChild) {
//存在左子树
return findByVal(node->leftChild,value);
} else {
//不存在左子树,返回最后访问的结点
return node;
}
} else {
if(node->rightChild) {
//存在左子树
return findByVal(node->rightChild,value);
} else {
//不存在左子树,返回最后访问的结点
return node;
}
}
}
现在讨论在二叉排序树
中插入新结点的实现思路:
Tips: 如果插入的值在树中已经存在,本文采用简单的替换方案。
插入函数:
//插入新结点
template<typename T>
TreeNode<T> * BinarySortTree<T>::insert(T value) {
//查找
TreeNode<T> *node= BinarySortTree<T>::findByVal(value);
//创建新结点
TreeNode<T> *newNode=new TreeNode<T>(value);
if(node->value==value) {
//已经存在
return NULL;
} else if(node->value>value) {
//放在左子树
node->leftChild=newNode;
} else {
//放在右子树
node->rightChild=newNode;
}
this->size++;
}
可以通过中序遍历
测试插入函数的正确性,这个在后文讲解中序遍历时再测试。
对任何一种树类型操作时,都需要提供对整棵树的遍历操作。遍历有 2
种搜索模式:
根据对根结点及其子结点的访问顺序的不同,常规的深度遍历操作有 3
种,可以使用递归或非递归方案实现。
编码实现:
//基于递归的前序遍历
template<typename T>
void BinarySortTree<T>::preOrder(TreeNode<T> * node) {
if(node!=NULL) {
cout<<node->value<<"\t";
preOrder(node->leftChild);
preOrder(node->rightChild);
}
}
编码实现:
//基于递归的中序遍历
template<typename T>
void BinarySortTree< T>::inOrder(TreeNode<T> * node) {
if(node!=NULL) {
inOrder(node->leftChild);
cout<<node->value<<"\t";
inOrder(node->rightChild);
}
}
从上图遍历路线可知,中序遍历是由小到大输出二叉排序树的结点,从而可以实现有序输出结点。可以使用中序遍历测试前面的插入算法的正确性。
int main() {
//实例化二叉排序树
BinarySortTree<int> * bt=new BinarySortTree<int>(5);
//插入新结点
int nums[9]= {12,4,45,32,8,10,50,32,3};
for(int i=1; i<9; i++) {
bt->insert(nums[i]);
}
//得到根结点
TreeNode<int> *root= bt->getRoot();
//中序遍历二叉树
bt->inOrder(root);
return 0;
}
从下面的输出结果可知,插入算法是正确的。
编码实现:
//基于递归的后序遍历
template<typename T>
void BinarySortTree<T>::postOrder(TreeNode<T> * node) {
if(node!=NULL) {
postOrder(node->leftChild);
postOrder(node->rightChild);
cout<<node->value<<"\t";
}
}
除了可以使用递归方案实现树的遍历,也可以使用非递归方案实现遍历,下面再讨论如何使用非递归实现遍历。
//非递归的前序遍历
template<typename T>
void BinarySortTree<T>::preOrder() {
//实例化栈
stack<TreeNode<T>*> stack;
if (BinarySortTree<T>::root==NULL)
return ;
//把根结点压入栈中
stack.push( BinarySortTree<T>::root);
while (!stack.empty()) {
//得到栈顶数据
TreeNode<T> *top=stack.top();
stack.pop();
//输出栈顶结点数据
cout<<top->value<<"\t";
//检查是否存在右子结点
if (top->rightChild!=NULL)
stack.push(top->rightChild);
//先压右边的,再压左边的
if (top->leftChild!=NULL)
stack.push(top->leftChild);
}
}
测试前序遍历:
int main() {
//实例化二叉排序树
BinarySortTree<int> * bt=new BinarySortTree<int>(5);
//插入新结点
int nums[9]= {12,4,45,32,8,10,50,32,3};
for(int i=1; i<9; i++) {
bt->insert(nums[i]);
}
//得到根结点
TreeNode<int> *root= bt->getRoot();
//递归前序遍历二叉树
bt->preOrder(root);
cout<<endl;
//非递归前序遍历二叉树
bt->preOrder();
return 0;
}
输出结果:
//非递归的中序遍历
template<typename T>
void BinarySortTree<T>::inOrder() {
//实例化栈
stack<TreeNode<T>*> stack;
if (BinarySortTree<T>::root==NULL)
return ;
//把根结点压入栈中
TreeNode<T>* top= BinarySortTree<T>::root;
while ( top!=NULL || !stack.empty()) {
if (top!=NULL) {
//如果根结点存在,压入栈中
stack.push(top);
//获取到根结点的左子结点
top=top->leftChild;
} else {
//如果结点不存在,弹出
top=stack.top();
stack.pop();
cout<<top->value<<"\t";
//获取右子结点
top=top->rightChild;
}
}
}
测试中序遍历:
int main() {
//实例化二叉排序树
BinarySortTree<int> * bt=new BinarySortTree<int>(5);
//插入新结点
int nums[9]= {12,4,45,32,8,10,50,32,3};
for(int i=1; i<9; i++) {
bt->insert(nums[i]);
}
//得到根结点
TreeNode<int> *root= bt->getRoot();
//递归中序遍历二叉树
bt->inOrder(root);
cout<<endl;
//非递归中序遍历二叉树
bt->inOrder();
return 0;
}
输出结果:
//基于非递归的后序遍历
template<typename T>
void BinarySortTree<T>::postOrder() {
TreeNode<T>* top= BinarySortTree<T>::root;
//存储结点
stack<TreeNode<T>*> stackNode;
//存储标志
stack<int> stackFlag;
int flag;
while(top || !stackNode.empty()) {
if(top) {
flag=0;
//结点第一次入栈
stackNode.push(top);
stackFlag.push(flag);
top=top->leftChild;
} else {
top= stackNode.top();
stackNode.pop();
flag= stackFlag.top();
stackFlag.pop();
if(flag==0) {
flag=1;
//第二次入栈
stackNode.push(top);
stackFlag.push(flag);
top=top->rightChild;
} else {
cout<<top->value<<"\t";
top=NULL;
}
}
}
}
测试后序遍历:
int main() {
//实例化二叉排序树
BinarySortTree<int> * bt=new BinarySortTree<int>(5);
//插入新结点
int nums[9]= {12,4,45,32,8,10,50,31,3};
for(int i=0; i<9; i++) {
bt->insert(nums[i]);
}
//得到根结点
TreeNode<int> *root= bt->getRoot();
bt->postOrder(root);
cout<<endl;
bt->postOrder();
return 0;
}
从二叉树中删除结点,需要保证整棵二叉排序树的有序性依然存在。删除操作比插入操作要复杂,下面分别讨论。
1. 如果要删除的结点是叶子结点。只需要把删除结点的父结点的左指针或右指针的引用值设置为空便可。
2. 删除的结点只有一个右子结点。如下图删除结点 `8`。
因为结点8
没有左子树,只需要把它的右子结点替换删除结点就可以了。
25
的结点。此种情形有 2
种可选方案。复制方案:找到结点 25
的左子树中的最大值,即结点 20
(该结点的特点:可能存在左子结点,一定没有右子结点)。用此结点值替换结点25
便可。
为什么要这么做? 道理很简单,既然是左子树中的最大值,替换删除结点后,整个二叉排序树的特性可以继续保持。
如果结点 20
存在左子结点,则把它的左子结点作为结点18
的右子结点。
Tips:如果删除值为
45
的结点,虽然存在左子结点,但左树中不存在最大右结点,可以直接把32
替换过去.
移子树方案:同样找到结点25
中左子树中的最大值结点 20
,然后把结点 25
的右子树作为结点 20
的右子树。
再把结点 25
的左子树移到 25
位置。
这种方案会让树增加树的深度。所以,建议使用第一种方案。
删除方法的实现:
template<typename T>
bool BinarySortTree<T>::deleteNode(T val) {
//查找结点是否存在,从根结点开始比较
TreeNode<T> *move=BinarySortTree<T>::root;
//删除时,需要结点的前驱结点
TreeNode<T> *parentNode=NULL;
while(move!=NULL) {
if(move->value==val)
//找到
break;
else if(move->value>val) {
parentNode=move;
//在左子树上查找
move=move->leftChild;
} else {
parentNode=move;
//在右子树上查找
move=move->rightChild;
}
}
if (move==NULL)
//不存在
return false;
if (move->leftChild==NULL && move->rightChild==NULL) {
//如果是叶结点
if (parentNode->leftChild==move)
parentNode->leftChild=NULL;
else
parentNode->rightChild=NULL;
delete move;
} else if(move->leftChild==NULL && move->rightChild!=NULL) {
//如果不存在左子结点,只有右子树
if (parentNode->leftChild==move)
parentNode->leftChild=move->rightChild;
else
parentNode->rightChild=move->rightChild;
delete move;
} else if(move->leftChild!=NULL) {
//如果存在左子结点,找到左子结点中的最大值结点
TreeNode<T> *pre=move;
TreeNode<T> *p=move->leftChild;
//如果左子结点不存在右子结点
if(p->rightChild==NULL) {
move->value=p->value;
if(p->leftChild!=NULL) {
move->leftChild= p->leftChild;
} else {
move->leftChild=NULL;
}
delete p;
} else {
while(p->rightChild!=NULL) {
pre=p;
p=p->rightChild;
}
//将最大值结点的值赋值绘删除结点
move->value=p->value;
if(p->leftChild!=NULL) {
pre->rightChild=p->leftChild;
}
pre->rightChild=NULL;
delete p;
}
}
return true;
}
使用中序遍历测试删除,理论上,删除任一结点后,中序遍历的输出结果应该还是有序的。
int main() {
//实例化二叉排序树
BinarySortTree<int> * bt=new BinarySortTree<int>(5);
//插入新结点
int nums[9]= {12,4,45,32,8,10,50,31,3};
for(int i=0; i<9; i++) {
bt->insert(nums[i]);
}
//得到根结点
TreeNode<int> *root= bt->getRoot();
cout<<"删除之前"<<"\t";
bt->inOrder(root);
cout<<endl;
for(int i=0; i<9; i++) {
//删除任一结点
cout<<"删除结点"<<nums[i]<<"之后:\t";
bt->deleteNode(nums[i]);
bt->inOrder(root);
cout<<endl;
}
return 0;
}
输出结果:
本文讲解了什么是二叉排序树,并深入地探讨了在二叉排序树如何进行数据的插入、遍历、删除……