二叉查找树(BST)
二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。二叉查找树的递归定义如下:
- 要么二叉查找树是一棵空树
- 要么二叉查找树由根结点、左子树、右子树组成,其中左子树和右子树都是二叉查找树,且左子树上所有结点的数据域均小于或等于根结点的数据域,右子树所有结点的数据域均大于根结点的数据域。
二叉查找树实际上是一棵数据域有序的二叉树
二叉查找树的基本操作
二叉查找树的基本操作有查找、插入、建树、删除。
1. 查找操作
二叉查找树的性质决定了可以只选择其中一棵子树进行遍历,因此查找将会是从树根到查找结点的一条路径
基本思路:
- 如果当前根结点root为空,说明查找失败,返回。
- 如果需要查找的值x等于当前根结点的数据域root->data,说明查找成功,访问之
- 如果需要查找的值x小于当前根结点的数据域root->data,说明应该往左子树查找,因此向root->lchild递归
- 如果需要查找的值x大于当前根结点的数据域root->data,说明应该往右子树查找,因此向root->rchild递归
//search函数查找二叉查找树中数据域为x的结点
void search(node* root, int x) {
if(root == NULL) { //空树,查找失败
printf("search failed\n");
return ;
}
if(x == root->data) { //查找成功,访问之
printf("%d\n", root->data);
} else if(x < root->data, x) { //x在左子树
search(root->lchild);
} else { //x在右子树
search(root->rchild, x);
}
}
二叉查找树的数据域顺序总是左子树<根结点<右子树
2. 插入操作
//insert函数将在二叉树中插入一个数据域为x的新结点(注意参数root要引用&)
void insert(node* &root, int x) {
if(root == NULL) { //空树,说明查找失败,也即插入位置
root == newNode(x); //newNode操作在二叉树的建立部分
return ;
}
if(x == root->data) { //查找成功,说明结点已存在,直接返回
return ;
} else if(x < root->data) { //插左子树
insert(root->lchild, x);
} else { //插右子树
insert(root->rchild, x);
}
}
3. 建立操作
建立一棵二叉查找树,就是先后插入n个结点的过程
node* Create(int data[], int n) {
node* root = NULL; //新建根结点root
for(int i = 0l i < n; i++) {
insert(root, data[i]); //将data[0]~data[n-1]插入二叉查找树中
}
return root; //返回根结点
}
4. 删除操作
删除操作必须保证删除后仍然是一棵二叉查找树
把以二叉查找树中比结点权值小的最大结点称为该结点的前驱,而把比结点权值大的最小结点称为该结点的后继。
结点的前驱是该结点左子树的最右结点(也就是从左子树根结点开始不断沿着rchild往下直到rchild为NULL时的结点),结点的后继是该结点右子树的最左结点(也就是从右子树根结点开始不断沿着lchild往下直到lchild为NULL的结点)。
以下两个函数用来寻找以root为根的树中最大或最小权值的结点,用以辅助寻找结点的前驱和后继:
//寻找以root为根结点的树中的最大权值结点
node* findMax(node* root) {
while(root->rchild != NULL) {
root = root->rchild; //不断往右,直到没有右孩子
}
return root;
}
//寻找以root为根结点的树中的最小权值结点
node* findMax(node* root) {
while(root->lchild != NULL) {
root = root->rchild; //不断往右,直到没有右孩子
}
return root;
}
假设决定用结点N的前驱P来替换N,于是就把问题转换为在N的左子树中删除结点P,就可以递归下去了,直到递归到一个叶子结点,就可以直接把它删除了。
基本思路:
- 如果当前结点root为空,说明不存在权值为给定权值x的结点,直接返回
- 如果当前结点root权值恰为给定的权值x,说明找到了想要删除的结点,此时进入删除处理
a) 如果当前结点root不存在左右孩子,,说明叶子结点,直接删除
b) 如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然后让pre的数据覆盖root,接着在左子树中删除结点pre
c) 如果当前结点root存在右孩子,那么在右子树中寻找结点后继next,然后让next的数据覆盖root,接着在右子树中删除结点next - 如果当前结点root的权值大于给定的权值x,则在左子树中递归删除权值为x的结点。
- 如果当前结点root的权值小于给定的权值x,则在右子树中递归删除权值为x的结点。
//删除以root为根结点的树中权值为x的结点
void deleteNode(node* &root, int x) {
if(root == NULL) return ; //不存在权值为x的结点
if(root->data == x) { //找到欲删除结点
if(root->lchild == NULL && root->rchild == NULL) { //叶子结点直接删除
root = NULL; //把root地址设为NULL,父结点就不引用它了
} else if(root->lchild != NULL) { //左子树不为空时
node* pre = findMax(root->lchild); //找root前驱
root->data = pre->data; //用前驱覆盖root
deleteNode(root->lchild, pre->data); //在左子树中删除结点pre
} else { //右子树不为空时
node* next = findMin(root->rchild); //找root的后继
root->data = next->data; //用后继覆盖root
deleteNode(root->rchild, next->data); //在右子树中删除结点pre
}
} else if(root->data > x) {
deleteNode(root->lchild, x); //在左子树中删除x
} else {
deleteNode(root->rchild, x); //在右子树中删除x
}
}
总是优先删除前驱(或者后继)容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。解决这一问题的办法有两种:一种是每次交替删除前驱或后继;另一种是记录子树高度,总是优先在高度较高的一棵子树里删除结点。
#####二叉查找树的性质
二叉查找树一个实用的性质:对二叉查找树进行中序遍历,遍历的结果是有序的。
这是由于二叉查找树本身的定义中就包含了左子树<根结点<右子树的特点,而中序遍历的访问顺序也是左子树->根结点->右子树,因此,所得到的中序遍历序列是有序的。
另外,如果合理调整二叉查找树的形态,使得树上的每个结点都尽量有两个子结点,这样整个二叉查找树的高度就会很低,也即树的高度大概在log(N)的级别,其中N是结点个数。能实现这个要求的一种树是平衡二叉树(AVL)。
实例
题意
给出N个正整数来作为一棵二叉排序树的结点插入顺序,问:这串序列是否是该二叉排序树的先序序列或是二叉排序树的镜像树的先序序列。所谓镜像树是指交换二叉树的所有结点的左右子树而形成的树(也即左子树所有结点数据域大于或等于根结点,而根结点数据域小于右子树所有结点的数据域)。如果是镜像树,则输出YES,并输出对应的树的后续序列,否则,输出NO。
思路
通过给定的插入序列,构建出二叉排序树。对镜像树的先序遍历只需要在原树的先序遍历时交换左右子树的访问顺序即可
void preOrderMirror(node* root, vector<int>&vi) {
if(root == NULL) return ;
vi.push_back(root->data);
preOrderMirror(root->right, vi); //先遍历右子树,再遍历左子树
preOrderMirror(root->left, vi);
}
- 使用vector来存放初始序列、先序序列、镜像树先序序列,可以方便相互之间的比较。若使用数组,则比较操作就需要使用循环才能实现
- 也可以再读入数据的同时建立其镜像二叉树,只需要将插入时的比较逻辑反过来即可。这样先序遍历和后序遍历只需要各写一个函数
- 定义根结点时将其设为结点(一开始是没有元素的);在新建结点时要注意令其左右子结点地址设为NULL
#include<cstdio>
#include<vector>
using namespace std;
struct node {
int data;
node *left, *right;
};
void insert(node* &root, int data) {
if(root == NULL) { //到达空结点时,即为需要插入的位置
root = new node;
root->data = data;
root->left = root->right = NULL;
return ;
}
if(data < root->data) insert(root->left, data); //插左子树
else insert(root->right, data);
}
void preOrder(node* root, vector<int>&vi) {
if(root == NULL) return ;
vi.push_back(root->data);
preOrder(root->left, vi);
preOrder(root->right, vi);
}
//镜像树先序遍历,结果存放于vi
void preOrderMirror(node* root, vector<int>&vi) {
if(root == NULL) return ;
vi.push_back(root->data);
preOrderMirror(root->right, vi);
preOrderMirror(root->left, vi);
}
void postOrder(node* root, vector<int>&vi) {
if(root == NULL) return ;
postOrder(root->left, vi);
postOrder(root->right, vi);
vi.push_back(root->data);
}
//镜像树后序遍历,结果存放于vi
void postOrderMirror(node* root, vector<int>&vi) {
if(root == NULL) return;
postOrderMirror(root->right, vi);
postOrderMirror(root->left, vi);
vi.push_back(root->data);
}
//origin存放初始序列
//pre、post为先序、后序,preM、postM为镜像树先序、后序
vector<int>origin, pre, post, preM, postM;
int main() {
int n, data;
node* root = NULL; //确定头结点
scanf("%d", &n); //输出结点个数
for(int i = 0; i < n; i++) {
scanf("%d", &data);
origin.push_back(data);
insert(root, data); //将data插入二叉树
}
preOrder(root, pre); //求先序
preOrderMirror(root, preM); //求镜像先序
postOrder(root, post); //求后序
postOrderMirror(root, postM); //求镜像后序
if(origin == pre) {
printf("YES\n");
for(int i = 0; i < post.size(); i++) {
printf("%d", post[i]);
if(i < post.size() - 1) printf(" ");
}
} else if(origin == preM) {
printf("YES\n");
for(int i = 0; i < postM.size(); i++) {
printf("%d", postM[i]);
if(i < postM.size() - 1) printf(" ");
}
} else {
printf("NO\n");
}
return 0;
}
整体思路比较清楚,最后结果判断两次,输出按后序序列输出即可。