title: 二叉查找树(BST)
date: 2020-01-13 20:36:30
tags: 数据结构
1.1二叉查找树定义
二叉查找树(Binary Search Tree, BST)是特殊的二叉树,又称排序二叉树、二叉搜索树、二叉排序树。
递归定义:
①可为一棵空树
②非空则由根结点、左子树、右子树组成。左右子树均为一棵二叉查找树,且根结点、根左孩子、根右孩子大小为 左孩子<=根结点<=右孩子
2 二叉查找树的基本操作及其实现
基本操作包括查找、插入、建树、删除。
2.1查找操作
//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, x); //左子树搜索
} else { //如果x比根结点的数据域大 在右子树
search(root->rchild, x); //右子树搜索
}
}
与一般二叉树不同 二叉查找树 因为每个节点与左右孩子的大小关系是固定的所以搜索的时候和一般的不太一样。
2.2插入操作
二叉查找树查找一个点是沿着固定的路径查找的,所以需要查找的值查找成功则结点已存在,反之,查找失败则查找失败的地方是结点需要插入的地方。
故在查找操作的基础上,在root == NULL的时候插入新建的结点。代码如下:
//insert 函数将在二叉树中插入一个数据域为x的新结点(注意参数root要加引用&)
void insert(node* &root, int x) {
if(root == NULL) { //空树 说明查找失败 也即插入位置
root = newNode(x);
return;
}
if(x == root->data) { //查找成功 说明结点已存在 直接返回
return;
} else if(x < root->data) { //如果x比根节点的数据域小 说明x需要插在左子树
insert(root->lchild, x); //往左子树搜索x
} else {
insert(root->rchild, x); //往左子树搜索
}
}
查找插入位置插入结点 注意**insert(node* &root, int x)**写法
2.3二叉查找树的建立
建立一个二叉查找树,先后插入n个结点的过程
//二叉查找树的建立
node* Create(int data[], int n) {
node* root = NULL;
for(int i = 0; i < n; i++) {
insert(root, data[i]); //将data[0]~data[n-1]
}
return root; //返回根节点
}
注意一组相同的数字不同的顺序得到的二叉查找树可能不同。
2.4二叉查找树的删除
把二叉查找树中比结点权值小的最大结点称为该结点的前驱,把比结点权值大的最小结点称为该节点的后继。结点的前驱是该结点左子树中的最右结点(就是从左子树根结点沿着右子树rchild不断知道rchild为NULL时的结点),而后继结点是该结点右子树的最左结点(从右子树根结点开始不断沿着lchild往下直到lchild为NULL时的结点)
//寻找以root为根结点的树中的最大权值结点
node* findMax(node* root) {
while(root->rchild != NULL) {
root = root->rchild; //不断往右
}
return root;
}
node* findMin(node* root) {
while(root->lchild != NULL) {
root = root->lchild; //不断往左 直到没有左孩子
}
return root;
}
对待删除结点,在左右子树当中找到前驱和后继结点,就有两种替换方法,目标均为保持二叉排序树的顺序。用前驱替换当前结点,或者用后继替换当前结点。
假设使用结点N的前驱节点替换当前N,问题转化为N的左子树中删除P(N操作就是前驱直接覆盖N),递归下去,知道递归到一个叶子节点(找到前驱)删除即可。
基本思路:
①如果当前结点root为空,说明不存在权值为给定权值x的结点直接返回。
②如果当前结点root的权值恰为给定的权值x找到需要删除的结点N,进入删除操作
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) { //此处会修改root所在结点 故node* &root使用引用 而不是使用副部访问
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->data = pre->data;
deleteNode(root->lchild, pre->data);
} else { //左子树为空 而右子树不为空
node* next = findMin(root->rchild); //找root后继覆盖root
root->data = next->data; //用前驱覆盖root
deleteNode(root->rchild, next->data); //右子树中删除结点next
}
} else if (root->data > x) {
deleteNode(root->lchild, x); //在左子树中删除x
} else {
deleteNode(root->rchild, x); //在右子树中删除x
}
}
这段代码可以优化,如在找到欲删除的结点root的后继结点next,不用继续递归 直接删除该后继就可以。
但要注意一点。总是优先删除前驱或者后继结点容易导致树的左右子树高度极度不平衡,使得二叉查找树变为一条链,解决方法有两个:1.交替删除前驱或者后继 2.或者记录子树高度,总是优先在高度较高的一棵子树中删除结点
3 二叉查找树的性质
对二叉查找树的中序遍历,遍历结果为有序的
如果合理调整二叉查找树的形态,使得树上的每个结点都尽量有两个子结点,这样二叉查找树高度很低,如果变为平衡树就变为平衡二叉树AVL。AVL是一种专用名词 默认为一棵平衡的二叉查找树。