文章目录
二叉查找树也叫二叉搜索树,英文名为 Binary Search Tree,简称 BST。
前言
本系列专注更新基本数据结构,现有以下文章:
1 简介
对于一棵二叉树,如果树中的每个节点,其左子树中的所有节点的值都小于该节点的值,而右子树中的所有节点的值都大于该节点的值,且左右子树也分别是二叉查找树,那么该二叉树被称为二叉查找树。即同时满足以下三个条件的二叉树被称为二叉查找树。
- 如果某二叉树有左子树,并且左子树中所有节点的值均小于其根节点的值;
- 如果某二叉树有右子树,并且右子树中所有节点的值均大于其根节点的值;
- 左右子树都是二叉查找树。
特殊的,空树也是二叉查找树。
2 基本实现
二叉查找树是一种集合了链表插入节点的灵活性与有序数组查找的高效性的数据结构。在链表中插入节点,我们只需遍历链表找到插入的指定位置,然后更改指针的指向即可,链表插入节点的最坏时间复杂度为 O ( n ) O(n) O(n)。在有序数组中,我们可以使用二分法快速定位到指定的元素。
在二叉查找树中,我们可以利用二叉查找树的特征二分查找指定的节点。基于这种二分查找,我们又可以快速定位到插入节点的指定位置。接下来就具体看看二叉查找树的查找、插入以及一些其他的基本操作。
2.1 数据表示
节点类
在介绍二叉查找树的基本操作之前,先来看一下二叉节点树中的节点类是如何定义的。
每个节点都包含一个值、一条左链接、一条右链接和一个节点计数器。左链接指向一棵由小于该节点的所有值组成的二叉查找树,右链接指向一棵由大于该节点的所有值组成的二叉查找树。节点计数器给出了以该节点为根的子树的总节点数。
struct Node {
int val;
Node* left;
Node* right;
int N;
Node(): val(0), left(nullptr), right(nullptr) {}
Node(int _val, int _N): val(_val), left(nullptr), right(nullptr), N(_N) {}
Node(int _val ,Node* _left, Node* _right, int _N): val(_val), left(_left), right(_right), N(_N) {}
};
二叉查找树类
二叉查找树中的成员函数比较多,公有成员是二叉查找树中函数的接口,而私有成员是具体的实现(私有类内声明,类外定义)。
接下来会针对每一个成员函数进行具体的讲解。
class BST {
private:
Node* root;
int size(Node* x);
bool search(Node* x, int target);
Node* put(Node* x, int val);
Node* min(Node* x);
Node* max(Node* x);
void inOrder(Node* x);
Node* floor(Node* x, int val);
Node* ceil(Node* x, int val);
Node* select(Node* x, int k);
int rank(Node* x, int val);
Node* deleteMin(Node* x);
Node* deleteMax(Node* x);
Node* deleteNode(Node* x, int val);
void range(Node* x, queue<int>& que, int lo, int hi);
public:
BST (): root(nullptr) {}
// 获得二叉搜索树节点数的接口
int getSize() {
return size(root);
}
// 查找节点的接口
bool getSearch(int target) {
return search(root, target);
}
// 插入节点的接口
void getPut(int val) {
root = put(root, val);
}
// 获得节点最小值的接口
int getMin() {
return min(root)->val;
}
// 获得节点最小值的接口
int getMax() {
return max(root)->val;
}
// 输出二叉查找树的中序遍历结果
void getInOrder() {
inOrder(root);
}
// floor 的接口,不存在就返回 -1
int getFloor(int val) {
Node* x = floor(root, val);
return x == nullptr ? -1 : x->val;
}
// ceil 的接口,不存在就返回 -1
int getCeil(int val) {
Node* x = ceil(root, val);
return x == nullptr ? -1 : x->val;
}
// select 的接口,不存在就返回 -1
int getSelect(int k) {
Node* x = select(root, k);
return x == nullptr ? -1 : x->val;
}
// rank 的接口
int getRank(int val) {
return rank(root, val);
}
// deleteMin 的接口
void getDeleteMin() {
root = deleteMin(root);
}
// deleteMax 的接口
void getDeleteMax() {
root = deleteMax(root);
}
// deleteNode 的接口
void getDeleteNode(int val) {
root = deleteNode(root, val);
}
// range 的接口
queue<int> getRange(int lo, int hi) {
queue<int> que;
range(root, que, lo, hi);
return que;
}
};
2.2 查找
二叉查找树的查找功能具备二分查找的快速查找功能,在一次二分查找后查找区间就会缩减一半,随着我们不断在二叉查找树中向下查找,当前节点所表示的子树的大小也在减少,当查找命中(找到指定的节点值)或者未命中时查找过程才会停止。
二叉查找通常有两种实现方法:递归和迭代。
在递归查找中:
- 如果树是空的,则查找未命中;
- 如果被查找的值和根节点的值相等,则查找命中;
- 否则,我们就递归的在左子树或右子树中查找。具体地,如果指定的节点值小于根节点值则在左子树中递归查找,否则在右子树中递归查找。
在迭代查找中:
- 如果树是空的,则查找未命中;
- 如果被查找的值和根节点的值相等,则查找命中;
- 否则,我们就在左子树或右子树中迭代查找,如果指定的节点值小于根节点值则在左子树查找,否则在右子树中查找,直至查找命中或者未命中。
代码
// 查找节点的接口
bool getSearch(int target) {
return search(root, target);
}
// 递归搜索节点,找到返回 true,否则返回 false
bool BST::search(Node* x, int target) {
if (x == nullptr) {
return false;
}
if (x->val == target) {
return true;
}
else if (target < x->val) {
return search(x->left, target);
}
else {
return search(x->right, target);
}
}
// 迭代查找
bool BST::search2(Node* x, int target) {
Node* curr = x;
while (curr != nullptr) {
if (curr->val == traget) {
return true;
}
else if (target < curr->val) {
curr = curr->left;
}
else {
curr = curr->right;
}
}
return false;
}
getSearch
函数是供类对象调用的查找二叉树中是否存在指定节点值的函数接口,查找功能由 search
函数(递归)或 search2
函数(迭代)具体实现。如果查找到指定节点值 target
则返回 true
,否则返回 false
。
注:下方的二叉查找树的所有实现均有接口和具体实现组成,以
get
开头的为接口函数。
2.3 插入
插入的递归实现与查找的递归实现很相似:如果树是空的,就返回一个含有插入值的节点;如果需要被插入的节点值小于根节点的值,我们就在左子树中递归的插入,否则就在右子树中递归的插入。
不同于查找代码的是,我们还需要自底向上更新每个节点的计数器。递归调用递的过程可以想象成沿着树向下走,根据需要插入的节点值与每个节点的值比较来确定是向左走还是向右走。递归调用的归的过程可以想象成沿着树往上爬。对于 查找 的方法,这对应着一系列的 返回指令,但是对于 插入 方法,这意味着重置搜索路径上每个父节点指向子节点的链接,并增加路径上每个节点中的计数器的值。
插入同样有递归和迭代两种实现方法。以下给出的是插入的递归实现,迭代实现可做练习。
代码
// 插入节点的接口
void getPut(int val) {
root = put(root, val);
}
// 递归查找 val,找到则更新,否则创建节点
Node* BST::put(Node* x, int val) {
if (x == nullptr) {
return new Node(val, 1);
}
if (val < x->val) {
x->left = put(x->left, val);
}
else if (val > x->val) {
x->right = put(x->right, val);
}
else {
x->val = val;
}
// 自下而上重置每一层节点的 N 值
x->N = 1 + size(x->left) + size(x->right);
return x;
}
3 有序性相关的方法和删除
3.1 查找最值
根据二叉查找树的定义与性质可知,二叉查找树中的最小值为最左侧叶子节点对应的值,最大值为最右侧叶子节点对应的值。因此,分别在二叉查找树的左子树和右子树中递归的查找即可找出树中节点的最小值和最大值。
代码
// 获得节点最小值的接口
int getMin() {
return min(root)->val;
}
// 获得节点最大值的接口
int getMax() {
return max(root)->val;
}
// 返回二叉查找树中节点值最小的节点
Node* BST::min(Node* x) {
if (x->left == nullptr) {
return x;
}
return min(x->left);
}
// 返回二叉查找树中节点值最大的节点
Node* BST::max(Node* x) {
if (x->right == nullptr) {
return x;
}
return max(x->right);
}
3.2 向上取整/向下取整
对数 x
向上取整指的是返回大于或等于 x
最小的值,向下取整指的是返回小于或者等于 x
最大的值。
在二叉查找树中,我们也可以对某一个数进行取整操作。具体地,定义函数 getFloor(val)
表示在二叉查找书中返回小于或者等于 val
最大的值,即向下取整。定义函数 getCeil(val)
表示在二叉查找书中返回大于或者等于 val
最小的值,即向上取整。
如果给定的节点值 val
小于二叉查找树的根节点的值,那么小于或者等于 val
最大的值一定在根节点的左子树中;如果给定的节点值 val
大于二叉查找树根节点的值,那么只有当根节点右子树中存在小于等于 val
的节点时,小于等于 val
的最大值才会出现在右子树中。此段便是 floor
函数的逻辑。
将 “左” 变为 “右”,同时将小于变为大于就能得到 ceil
函数。
代码
// floor 的接口,不存在就返回 -1
int getFloor(int val) {
Node* x = floor(root, val);
return x == nullptr ? -1 : x->val;
}
// ceil 的接口,不存在就返回 -1
int getCeil(int val) {
Node* x = ceil(root, val);
return x == nullptr ? -1 : x->val;
}
// 向下取整,找出小于等于 val 的最大节点值
Node* BST::floor(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (x->val == val) {
return x;
}
if (val < x->val) {
return floor(x->left, val);
}
Node* t = floor(x->right, val);
return t != nullptr ? t : x;
}
// 向上取整,找出大于等于 val 的最小节点值
Node* BST::ceil(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (x->val == val) {
return x;
}
if (val > x->val) {
return ceil(x->right, val);
}
Node* t = ceil(x->left, val);
return t != nullptr ? t : x;
}
3.3 选择操作
假设我们想找到排名为 k
的节点(即树中正好有 k
个小于它的节点值):
- 如果左子树中的节点数 t 大于
k
,那么我们就继续递归地在左子树中查找排名为k
的节点; - 如果 t 等于
k
,我们就返回根节点; - 如果 t 大于
k
,我们就递归地在右子树中查找排名为t-k-1
的节点。
代码
// select 的接口,不存在就返回 -1
int getSelect(int k) {
Node* x = select(root, k);
return x == nullptr ? -1 : x->val;
}
// 选择操作:找到排名为 k 的节点(树中正好有 k 个小于它的节点值)
Node* BST::select(Node* x, int k) {
if (x == nullptr) {
return nullptr;
}
int t = size(x->left);
if (t > k) {
return select(x->left, k);
}
else if (t < k) {
return select(x->right, k - t - 1);
}
else {
return x;
}
}
3.4 排名
排名操作和选择操作正相反,它会返回给定节点值的排名,在实现中:
- 如果给定的节点值和根节点的值相等,我们返回左子树中的节点总数
t
; - 如果给定的节点值小于根节点的值,我们会返回该节点值在左子树中的排名(递归计算);
- 如果给定的节点值大于根节点的值,我们会返回
t+1
加上它在右子树中的排名(递归计算)。
代码
// rank 的接口
int getRank(int val) {
return rank(root, val);
}
// 排名操作:返回以 root 为根节点的子树中小于 val 的节点的数量
int BST::rank(Node* x, int val) {
if (x == nullptr) {
return 0;
}
if (val < x->val) {
return rank(x->left, val);
}
else if (val > x->val) {
return 1 + size(x->left) + rank(x->right, val);
}
else {
return size(x->left);
}
}
3.5 删除最值
二叉查找树中最难实现的也是比较重要的方法是删除方法。作为准备,我们先来实现删除最小节点值对应的节点。 我们定义函数 deleteMin
来删除最小节点值对应的节点,我们接收一个节点,最后返回删除 “最小” 节点后二叉查找树的新的头节点。
在实现中,我们要不断深入根节点的左子树直至遇到一个空节点,然后将指向该节点的链接指向该节点的链接指向该节点的右子树。结合代码仔细体会。
代码
// deleteMin 的接口
void getDeleteMin() {
root = deleteMin(root);
}
// deleteMax 的接口
void getDeleteMax() {
root = deleteMax(root);
}
// 删除最小值,返回删除后的二叉搜索树新的根节点
Node* BST::deleteMin(Node* x) {
if (x->left == nullptr) {
return x->right;
}
x->left = deleteMin(x->left);
x->N = size(x->left) + size(x->right) + 1;
return x;
}
// 删除最大值,返回删除后新的跟节点
Node* BST::deleteMax(Node* x) {
if (x->right == nullptr) {
return x->left;
}
x->right = deleteMax(x->right);
x->N = size(x->left) + size(x->right) + 1;
return x;
}
3.6 删除操作
我们可以用类似的方法删除任意只有一个子节点,或者没有子节点的节点,但是如何删除一个拥有两个子节点的节点呢?删除之后我们要处理两棵子树,但被删除节点的父节点只有一条空出来的链接。
T.Hibbard 在 1962 年提出了解决该问题的第一个方法,在删除节点 x
后用它的后继节点填补它的位置。因为 x
有一个右子节点,因此它的后继节点就是其右子树中的最小节点。这样的替换仍然能够保证数的有序性,因为节点 x
的值和它后继节点的值之间不存在其他的键。我们能够用 4 个简单的步骤完成 x
替换为它的后继节点的任务:
- 将即将被删除的节点
x
保存到t
; - 将
x
指向它的后继节点中min(t->right)
; - 将
x
的右链接指向deleteMin(t->right)
,也就是删除后所有节点仍都大于x->val
的子二叉树; - 将
x
的左链接设为t->left
。
代码
// deleteNode 的接口
void getDeleteNode(int val) {
root = deleteNode(root, val);
}
// 删除指定节点并返回新的根节点
Node* BST::deleteNode(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (val < x->val) {
x->left = deleteNode(x->left, val);
}
else if (val > x->val) {
x->right = deleteNode(x->right, val);
}
else {
if (x->left == nullptr) {
return x->right;
}
if (x->right == nullptr) {
return x->left;
}
Node* t = x;
x = min(t->right);
x->right = deleteMin(t->right);
x->left = t->left;
}
x->N = 1 + size(x->left) + size(x->right);
return x;
}
3.7 范围查找
二叉查找树最后一个常用操作是范围查找,不同于记录所有二叉树查找树的节点值,我们只需要将节点值在指定范围内的节点记录下来即可。
在具体实现中:
- 如果当前节点值在范围内,直接记录到答案队列中;
- 如果查找区间左端点落在根节点的左子树中,则在左子树中递归记录节点;
- 如果查找区间右端点落在根节点的右子树中,则在右子树中递归记录节点。
代码
// range 的接口
queue<int> getRange(int lo, int hi) {
queue<int> que;
range(root, que, lo, hi);
return que;
}
// 范围查找,返回二叉搜索树中在 [lo, hi] 范围内的节点值
void BST::range(Node* x, queue<int>& que, int lo, int hi) {
if (x == nullptr) {
return;
}
if (lo < x->val) {
range(x->left, que, lo, hi);
}
if (lo <= x->val && hi >= x->val) {
que.push(x->val);
}
if (hi > x->val) {
range(x->right, que, lo, hi);
}
}
有二叉查找树的定义可知,如果对二叉查找树进行中序遍历,那么将得到非递减的序列。中序遍历代码为:
void inorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
inorderTraversal(root->left);
std::cout << root->key << " ";
inorderTraversal(root->right);
}
4 手写二叉查找树
上文提到的二叉查找树所有实现的完整代码如下:
bst.hpp
#ifndef BST__H
#define BST__H
#include <queue>
using namespace std;
struct Node {
int val;
Node* left;
Node* right;
int N;
Node(): val(0), left(nullptr), right(nullptr) {}
Node(int _val, int _N): val(_val), left(nullptr), right(nullptr), N(_N) {}
Node(int _val ,Node* _left, Node* _right, int _N): val(_val), left(_left), right(_right), N(_N) {}
};
class BST {
private:
Node* root;
int size(Node* x);
bool search(Node* x, int target);
Node* put(Node* x, int val);
Node* min(Node* x);
Node* max(Node* x);
void inOrder(Node* x);
Node* floor(Node* x, int val);
Node* ceil(Node* x, int val);
Node* select(Node* x, int k);
int rank(Node* x, int val);
Node* deleteMin(Node* x);
Node* deleteMax(Node* x);
Node* deleteNode(Node* x, int val);
void range(Node* x, std::queue<int>& que, int lo, int hi);
public:
BST (): root(nullptr) {}
// 获得二叉搜索树节点数的接口
int getSize() {
return size(root);
}
// 查找节点的接口
bool getSearch(int target) {
return search(root, target);
}
// 插入节点的接口
void getPut(int val) {
root = put(root, val);
}
// 获得节点最小值的接口
int getMin() {
return min(root)->val;
}
// 获得节点最大值的接口
int getMax() {
return max(root)->val;
}
// 输出二叉查找树的中序遍历结果
void getInOrder() {
inOrder(root);
}
// floor 的接口,不存在就返回 -1
int getFloor(int val) {
Node* x = floor(root, val);
return x == nullptr ? -1 : x->val;
}
// ceil 的接口,不存在就返回 -1
int getCeil(int val) {
Node* x = ceil(root, val);
return x == nullptr ? -1 : x->val;
}
// select 的接口,不存在就返回 -1
int getSelect(int k) {
Node* x = select(root, k);
return x == nullptr ? -1 : x->val;
}
// rank 的接口
int getRank(int val) {
return rank(root, val);
}
// deleteMin 的接口
void getDeleteMin() {
root = deleteMin(root);
}
// deleteMax 的接口
void getDeleteMax() {
root = deleteMax(root);
}
// deleteNode 的接口
void getDeleteNode(int val) {
root = deleteNode(root, val);
}
// range 的接口
queue<int> getRange(int lo, int hi) {
queue<int> que;
range(root, que, lo, hi);
return que;
}
};
#endif
bst.cpp
#include <iostream>
#include "bst.hpp"
using namespace std;
int BST::size(Node* x) {
if (x == nullptr) {
return 0;
}
return x->N;
};
// 递归搜索节点,找到返回 true,否则返回 false
bool BST::search(Node* x, int target) {
if (x == nullptr) {
return false;
}
if (x->val == target) {
return true;
}
else if (target < x->val) {
return search(x->left, target);
}
else {
return search(x->right, target);
}
}
// 递归查找 val,找到则更新,否则创建节点
Node* BST::put(Node* x, int val) {
if (x == nullptr) {
return new Node(val, 1);
}
if (val < x->val) {
x->left = put(x->left, val);
}
else if (val > x->val) {
x->right = put(x->right, val);
}
else {
x->val = val;
}
// 自下而上重置每一层节点的 N 值
x->N = 1 + size(x->left) + size(x->right);
return x;
}
// 返回二叉查找树中节点值最小的节点
Node* BST::min(Node* x) {
if (x->left == nullptr) {
return x;
}
return min(x->left);
}
// 返回二叉查找树中节点值最大的节点
Node* BST::max(Node* x) {
if (x->right == nullptr) {
return x;
}
return max(x->right);
}
// 二叉查找的中序遍历
void BST::inOrder(Node* x) {
if (x == nullptr) {
return;
}
inOrder(x->left);
cout << x->val << endl;;
inOrder(x->right);
}
// 向下取整,找出小于等于 val 的最大节点值
Node* BST::floor(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (x->val == val) {
return x;
}
if (val < x->val) {
return floor(x->left, val);
}
Node* t = floor(x->right, val);
return t != nullptr ? t : x;
}
// 向上取整,找出大于等于 val 的最小节点值
Node* BST::ceil(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (x->val == val) {
return x;
}
if (val > x->val) {
return ceil(x->right, val);
}
Node* t = ceil(x->left, val);
return t != nullptr ? t : x;
}
// 选择操作:找到排名为 k 的节点(树中正好有 k 个小于它的节点值)
Node* BST::select(Node* x, int k) {
if (x == nullptr) {
return nullptr;
}
int t = size(x->left);
if (t > k) {
return select(x->left, k);
}
else if (t < k) {
return select(x->right, k - t - 1);
}
else {
return x;
}
}
// 排名操作:返回以 root 为根节点的子树中小于 val 的节点的数量
int BST::rank(Node* x, int val) {
if (x == nullptr) {
return 0;
}
if (val < x->val) {
return rank(x->left, val);
}
else if (val > x->val) {
return 1 + size(x->left) + rank(x->right, val);
}
else {
return size(x->left);
}
}
// 删除最小值,返回删除后的二叉搜索树新的根节点
Node* BST::deleteMin(Node* x) {
if (x->left == nullptr) {
return x->right;
}
x->left = deleteMin(x->left);
x->N = size(x->left) + size(x->right) + 1;
return x;
}
// 删除最大值,返回删除后新的跟节点
Node* BST::deleteMax(Node* x) {
if (x->right == nullptr) {
return x->left;
}
x->right = deleteMax(x->right);
x->N = size(x->left) + size(x->right) + 1;
return x;
}
// 删除指定节点并返回新的根节点
Node* BST::deleteNode(Node* x, int val) {
if (x == nullptr) {
return nullptr;
}
if (val < x->val) {
x->left = deleteNode(x->left, val);
}
else if (val > x->val) {
x->right = deleteNode(x->right, val);
}
else {
if (x->left == nullptr) {
return x->right;
}
if (x->right == nullptr) {
return x->left;
}
Node* t = x;
x = min(t->right);
x->right = deleteMin(t->right);
x->left = t->left;
}
x->N = 1 + size(x->left) + size(x->right);
return x;
}
// 范围查找,返回二叉搜索树中在 [lo, hi] 范围内的节点值
void BST::range(Node* x, queue<int>& que, int lo, int hi) {
if (x == nullptr) {
return;
}
if (lo < x->val) {
range(x->left, que, lo, hi);
}
if (lo <= x->val && hi >= x->val) {
que.push(x->val);
}
if (hi > x->val) {
range(x->right, que, lo, hi);
}
}
5 总结
二叉查找树是一种集合链表插入节点的灵活性与有序数组查找的高效性的数据结构。在上面分析的几种操作中,最重要的是查找、插入以及删除这三种操作。
查找操作的时间复杂度为 O ( l o g n ) O(logn) O(logn), n n n 是二叉查找树中节点的数量。插入和删除的操作最坏情况是,二叉查找树退化成一条链,此时如果在链表的尾部插入元素或者删除链表尾部的元素,时间复杂度为 O ( n ) O(n) O(n)。
那能否有一种比二叉查找树中插入和删除操作更优的数据结构呢?有,就是平衡二叉树。
参考资料
【书籍】算法第四版
写在最后
如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。
如果大家有更清晰以及独到的理解,欢迎评论区交流。
最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。