1. 二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
- 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
- 它的左右子树也分别为二叉搜索树
- 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。后续我们学习 map/set/multimap/multiset 系列容器底层就是二叉搜索树,其中 map/set 不支持插入相等值,multimap/multiset 支持插入相等值。
2. 二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树 (或者接近完全二叉树),其高度为:log2 N。
最差情况下,二叉搜索树退化为单支树 (或者类似单支),其高度为:N。
所以综合而言二叉搜索树增删查改时间复杂度为:O(N)。
因此单纯的二叉搜索树的效率不能满足我们的要求,后续二叉搜索树的变形比如平衡搜索二叉树即 AVLTree,红黑树 RBTree,他们由于一些特殊的设计使得二叉搜索树的效率接近 log2N。
二分查找的缺陷:
需要存储在支持下标随机访问的结构中,并且有序。比如有序数组。插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。
正是二分查找的缺陷体现了二叉搜索树的价值。
3. 二叉搜索树的插入
插入的过程:
- 树为空,则直接新增结点,赋值给 root 指针
- 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
- 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走),具体设计自己决定。
示例数据:int a[]={8,3,1,10,6,4,7,14,13};
4. 二叉搜索树的查找
从根开始比较,查找 x,x 比根的值大则往右边行走查找,x 比根值小则往左边行走查找最多查找高度次,走到到空,还没找到,这个值不存在。
如果不支持插入相等的值,找到 x 即可返回。
如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x。
遍历方式:
5. 二叉搜索树的删除
过程:
首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。
如果查找元素存在则分以下四种情况分别处理(假设要删除的结点为 N):
- 要删除结点 N 左右孩子均为空
- 要删除的结点 N 左孩子位空,右孩子结点不为空
- 要删除的结点 N 右孩子位空,左孩子结点不为空
- 要删除的结点 N 左右孩子结点均不为空
其实在代码实现上可以划分为两种情况:
- 没有孩子和只有一个孩子视为一种情况,因为可以将没有孩子视作只有一个孩子,只不过这个孩子是 nullptr 罢了。
- 有两个孩子。
以下是具体的解决方案:
- 以只有右孩子为例子,把 N 结点的父亲对应孩子指针指向 N 的右孩子,直接删除 N 结点。没有孩子时 N 结点的父亲对应孩子的指针指向 nullptr 符合逻辑。
- 无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。找 N 左子树的值最大结点 R(最右结点) 或者 N 右子树的值最小结点 R(最左结点) 替代 N,因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 的两个结点的值交换,转而变成删除 R 结点。
6. 二叉搜索树 key 和 key/value 使用场景
6.1 key 搜索场景:
只有 key 作为关键码,结构中只需要存储 key 即可,关键码即为需要搜索到的值,搜索场景只需要判断 key 在不在。key 的搜索场景实现的二叉搜索树支持增删查,但是不支持修改,修改 key 破坏搜索树结构了。
简单举例:
- 车牌抬杆识别系统,判断车牌字符是否存在(后台将车牌号录入搜索二叉树)
- 检查一篇英文文章是否有错(词典录入二叉树)
- 编译器的语法检查(将库,自定义,关键词放入二叉树)
总之 key 的搜索二叉树一般用于快速查询数据是否存在。
6.2 key/val 搜索场景
每一个关键码 key,都有与之对应的值 value,value 可以任意类型对象。树的结构中 (结点) 除了需要存储 key 还要存储对应的 value,增/删/查还是以 key 为关键字走二叉搜索树的规则进行比较,可以快速查找到 key 对应的 value。key/value 的搜索场景实现的二叉搜索树支持修改,但是不支持修改 key,修改 key 破坏搜索树性质了,可以修改 value。
简单举例:
- 检查中英文互译系统(key 即 英文,value 即中文)
- 商场车库系统(收费自助类,通过存车票号,和入场时间计算租金)
- 读取一篇文章中英文出现的次数
7. 二叉搜索树的实现代码
7.1 key 模型代码实现
这里的 set 不是完全 set 的底层封装,本代码只是二叉搜索树 key 模型的实现代码。
#pragma once
#include <assert.h>
#include <iostream>
using namespace std;
namespace twg {
template<class K>
struct BSTnode {
BSTnode(const K& key = K()) { _key = key; left = nullptr; right = nullptr; }
K _key;
BSTnode* left;
BSTnode* right;
};
template<class K>
class set {
typedef BSTnode<K> Node;
public:
bool Insert(const K& key) {
if (_root == nullptr) {
_root = (key);
}
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
;
}
}
cur = (key);
(parent->right == ) {
parent->right = cur;
}
{
parent->left = cur;
}
;
}
{
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
(cur->left == ) {
(parent == ) {
_root = cur->right;
}
(parent->left == cur) {
parent->left = cur->right;
}
{
parent->right = cur->right;
}
cur;
cur = ;
;
}
{
Node* rightMinf = cur;
Node* rightMin = cur->left;
(rightMin->left) {
rightMinf = rightMin;
rightMin = rightMin->left;
}
(parent->left == cur) {
parent->left = rightMin;
}
{
parent->right = rightMin;
}
rightMinf->left = rightMin->right;
rightMin->left = cur->left;
rightMin->right = cur->right;
cur;
cur = ;
;
}
}
}
cout << << endl;
;
}
{
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
;
}
}
;
}
{
(_root);
(_root);
}
:
{
(root == )
;
(root->left);
cout << root->_key << endl;
(root->right);
}
Node* _root = ;
};
}
7.2 key/val 代码实现
二叉搜索树 key/val 模型代码实现
#pragma once
#include <iostream>
using namespace std;
namespace twg {
template<class K, class V>
struct BSTnode
{
BSTnode(const K& key, const V& val):_key(key),_val(val),left(nullptr),right(nullptr){}
K _key;
V _val;
BSTnode* left;
BSTnode* right;
};
template<class K, class V>
class map {
typedef BSTnode<K, V> Node;
public:
map() = default;
map(const map<K, V>& a) {
_root = (a._root);
}
~() {
(_root);
}
map<K, V>& =(map<K, V> a) {
std::(_root, a._root);
*;
}
{
(_root == ) {
_root = (key, val);
;
}
Node* cur = _root;
Node* parent = ;
(cur) {
(key <= cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
}
cur = (key, val);
(cur->_key <= parent->_key) {
parent->left = cur;
}
{
parent->right = cur;
}
;
}
{
_inorder(_root);
cout << endl;
}
{
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
;
}
}
;
}
{
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
(cur->right != ) {
(parent == ) {
_root = cur->right;
}
{
(parent->left == cur) {
parent->left = cur->right;
}
{
parent->right = cur->right;
}
}
cur;
cur = ;
;
}
Node* leftMax = cur->right;
Node* leftMaxf = cur;
(leftMax->right) {
leftMaxf = leftMax;
leftMax = leftMax->right;
}
cur->_val = leftMax->_val;
cur->_key = leftMax->_key;
leftMaxf->right = leftMax->left;
leftMax;
leftMax = ;
;
}
}
cout << << endl;
;
}
V& []( K& key) {
Node* cur = _root;
Node* parent = ;
(cur) {
(key < cur->_key) {
parent = cur;
cur = cur->left;
}
(key > cur->_key) {
parent = cur;
cur = cur->right;
}
{
cur->_val;
}
}
cur = (key, ());
(parent == ) {
_root = cur;
}
{
(key < parent->_key)
parent->left = cur;
parent->right = cur;
}
cur->_val;
}
:
{
(root == )
;
Node* newNode = (root->_key, root->_val);
newNode->left = (root->left);
newNode->right = (root->right);
newNode;
}
{
(root == )
;
(root->left);
(root->right);
root;
}
_inorder( Node* root) {
(root == )
;
_inorder(root->left);
cout << root->_key << << root->_val << endl;
_inorder(root->right);
}
Node* _root = ;
};
}
结语
二叉搜索树的'左小右大'特性的背后,是高效数据查询的核心逻辑 —— 它既弥补了二分查找的插入删除短板,也为后续平衡树(AVL、红黑树)打下基础。掌握双场景适配与增删查改的边界处理后,你已能应对简单数据存储需求。