1. 二叉搜索树的概念
二叉搜索树,又称二叉排序数或二叉查找树。
它要么一棵空树,要么具有如下性质:
(1)若它的左子树不为空,则左子树上所有节点的值都小于根节点的值;
(2)若它的右子树不为空,则右子树上所有节点的值都大于根节点的值;
(3)它的左右子树也分别为二叉排序树;
下图就是一棵二叉搜索树
二叉排序树具有如下优点:
- 排序、查找、插入和删除方便;
- 二叉排序树的中序遍历序列为所有节点的排序结果。
- 查找的时间复杂度为,仅次于通过哈希表建立数据索引来查找的方式(查找时间复杂度为);
2. 二叉搜索树的存储形式
二叉搜索树存储方式和普通二叉树一样,有两种存储方式,一种是顺序存储,一种是链式存储。
(1)顺序存储
二叉树的顺序存储,就是用一组连续的存储单元存放二叉树中的结点。因此,必须把二叉树的所有结点安排成为一个恰当的序列,结点在这个序列中的相互位置能反映出结点之间的逻辑关系,用编号的方法从树根起,自上层至下层,每层自左至右地给所有结点编号。
缺点是有可能对存储空间造成极大的浪费,在最坏的情况下,一个深度为且只有个结点的右单支树需要个结点存储空间。
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
如图所示:
图1 完全二叉树使用顺序存储结构存储。
图2 非完全二叉树改造成完全二叉树后的存储示意图。
(2)链式存储结构
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。其结点结构为:
其中,data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。利用这样的结点结构表示的二叉树的链式存储结构被称为二叉链表,如图3所示。
图3 二叉树的二叉链表表示示意图
3. 二叉搜索树中的常用操作
- 查找
查找操作的思路:
(1)从根节点开始查找,如果根节点为空,则返回NULL;
(2)如果根节点非空,则将查找数据和根节点比较,
(3)如果查找数据大于根节点,则在右子树继续查找;
(4)如果查找数据小于根节点,则在左子树继续查找;
(5)如果查找数据等于根节点的值,则表示查找完成,返回该节点;
实现代码:
struct BinaryTreeNode{
int value;
BinaryTreeNode* left;
BinaryTreeNode* right;
BinaryTreeNode(int val) :value(val), left(nullptr), right(nullptr){}
};
//二叉搜索树中的递归查找
bool search_recursion(BinaryTreeNode* root, int val){
if (root == nullptr){
return false;
}
if (root->value == val){
return true;
}
else if (root->value > val){
return search_recursion(root->left, val);
}
else{
return search_recursion(root->right, val);
}
}
//二叉搜索树中的非递归查找
bool search(BinaryTreeNode* root, int val){
BinaryTreeNode* p = root;
while (p!=nullptr){
if (p->value == val){
return true;
}
else if (p->value > val){
p = p->left;
}
else{
p = p->right;
}
}
return false;
}
- 插入
插入操作基本思路是:
(1)如果指针指向的当前节点为空,则找到待插入节点的位置,为待插入节点分配空间,并令指针指向待插入节点;
(2)如果指向指向的当前节点不为空,则将当前节点的值与待插入节点比较;
(3)如果当前节点的值小于待插入节点,则在右子树中插入节点;
(4)如果当前节点大于待插入节点,则在左子树插入节点;
实现代码:
//二叉搜索树的递归插入操作
BinaryTreeNode* insert_recursion(BinaryTreeNode* root, int node_value){
//当走到空指针,就找到了node_value的插入位置。
if (root == nullptr){
root = new BinaryTreeNode(node_value);
}
/*if (root->value == node_value){
return;
}*/
if (root->value > node_value){
root->left = insert_recursion(root->left, node_value);
}
else if(root->value < node_value){
root->right = insert_recursion(root->right, node_value);
}
return root;
}
BinaryTreeNode* insert(BinaryTreeNode* root, int node_value){
BinaryTreeNode* iter = root;
//记录待插入节点的父节点;
BinaryTreeNode* iter_parent = nullptr;
//记录待插入节点是左孩子还是右孩子。
int enter = -1;
while (iter != nullptr){
iter_parent = iter;
/*if (iter->value == node_value){
return;
}*/
if (iter->value > node_value){
iter = iter->left;
enter = 0;
}
else if(iter->value < node_value){
iter = iter->right;
enter = 1;
}
}
BinaryTreeNode* node = new BinaryTreeNode(node_value);
if (enter == -1){
root = node;
}
else if (enter == 0){
iter_parent->left = node;
}
else{
iter_parent->right = node;
}
return root;
}
- 删除
删除操作根据待删除的节点分为三种情况:
第一种情况,如果待删除的节点为叶子节点,则直令删除该节点即可;
第二种情况,如果待删除节点只有一个子树(左子树或右子树),则直接用子树覆盖该节点;
第三种情况,如果待删除节点既有左子树,又有右子树,则用左子树的最大节点或右子树的最小节点覆盖待删除节点,并删除左子树中的最大节点或删除右子树中的最小节点。
以下面这个例子来说明:
假如我们要删除35这个结点,它有左右两个子树,这时我们可以选择第一,从右子树中找最小值,35的右子树只有41,可以把41的值拷贝去替换掉35,然后删除41这个节点。,这棵树仍是二叉搜索树,满足相应的关系。第二,从左子树中找最大值,35的左子树中最大值是28,就把28拷贝替换掉35的位置,然后删除28这个结点,此时25顺应就接到了22的右子树的位置上,可以发现替换后树仍然是一棵二叉搜索树。这样做的好处是,把第三种情况要删除的结点有两个子树变成要删除的结点只有一个子树。为什么是这样?因为我们可以想一下,左子树中的最大值,肯定是在左子树的最右边,而这个结点肯定不会有两个结点,要么没有要么就只有一个结点。右子树中的最小值同样是这样。
实现代码:
//在二叉搜索树中找到最大节点
BinaryTreeNode* findMax(BinaryTreeNode* root){
BinaryTreeNode* iter = root;
while (iter->right != nullptr){
iter = iter->right;
}
return iter;
}
//在二叉搜索树中找到最小节点
BinaryTreeNode* findMin(BinaryTreeNode* root){
BinaryTreeNode* iter = root;
while (iter->left != nullptr){
iter = iter->left;
}
return iter;
}
BinaryTreeNode* remove(BinaryTreeNode* root,int remove_value){
if (root == nullptr){
cout << "找不到要删除的节点" << endl;
return nullptr;
}
//找到要删除的节点
if (root->value == remove_value){
//若待删除节点的左右子树都不为空
if (root->left != nullptr && root->right != nullptr){
//在待删除节点的左子树中找到最大节点或在右子树中找到最小节点
BinaryTreeNode* leftmaxnode = findMax(root->left);
//用左子树最大节点的值覆盖待删除节点的值
root->value = leftmaxnode->value;
//删除待左子树中的最大节点
root->left = remove(root->left, leftmaxnode->value);
}
else{
BinaryTreeNode* temp = root;
//如果待删除节点的左孩子为空,则用右子树覆盖待删除节点,否则用左子树覆盖待删除节点
if (root->left == nullptr){
root = root->right;
}
else if (root->right == nullptr){
root = root->left;
}
//释放待删除节点所占的空间
delete temp;
}
}
else if (root->value < remove_value){
root->right = remove(root->right, remove_value);
}
else{
root->left = remove(root->left, remove_value);
}
return root;
}
- 中序遍历
- 前序遍历
- 后序后序