目录
[1 构造函数](#1 构造函数)
[2 析构函数](#2 析构函数)
[3 查找节点](#3 查找节点)
[4 插入节点](#4 插入节点)
[5 删除节点](#5 删除节点)
[6 中序遍历](#6 中序遍历)
二叉搜索树
二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
a.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
b.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
c.它的左右子树也分别为二叉搜索树
二叉搜索树操作

cpp
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
二叉搜索树的查找
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
二叉搜索树的插入

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看似删除节点有4种情况,但实际上a和b和c可以合并,这样就只有2种情况了:
a:待删除的结点无孩子/只有一个孩子:删除结点并使父亲结点指向被删除结点的孩子结点(无孩子视为孩子是空结点,任意指向一个即可)

b:待删除的结点有左右孩子:采用替换法,寻找删除结点右子树的最小结点(右子树最左结点),将最小结点的值和删除结点的值替换,然后删除最小结点(此时最小结点,要么没有孩子,要么只有一个孩子,符合a情况可以直接删除)



二叉搜索树的实现
声明
cpp
template<class T>
struct BSTNode
{
BSTNode(const T& data = T())
: _pLeft(nullptr), _pRight(nullptr), _data(data)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _data;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
typedef Node* PNode;
public:
BSTree(); // 构造函数
~BSTree(); // 析构函数
PNode Find(const T& data); // 查找值为data的节点
bool Insert(const T& data); // 插入节点
bool Erase(const T& data); // 删除节点
void InOrder(); // 中序遍历(打印)
private:
PNode _pRoot;
void _Destroy(PNode& root); // 递归销毁
void _InOrder(PNode root); // 递归中序遍历
};
1 构造函数
初始化根节点为空
cpp
template<class T>
BSTree<T>::BSTree()
: _pRoot(nullptr)
{}
2 析构函数
递归释放所有节点,防止内存泄漏
cpp
template<class T>
void BSTree<T>::_Destroy(PNode& root)
{
if (root == nullptr)
return;
_Destroy(root->_pLeft);
_Destroy(root->_pRight);
delete root;
root = nullptr;
}
template<class T>
BSTree<T>::~BSTree()
{
_Destroy(_pRoot);
}
3 查找节点
根据二叉搜索树性质,从根开始比较,小于向左,大于向右,相等则返回节点指针
cpp
template<class T>
typename BSTree<T>::PNode BSTree<T>::Find(const T& data)
{
PNode cur = _pRoot;
while (cur)
{
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return cur; // 找到
}
return nullptr; // 未找到
}
4 插入节点
空树则直接插入根节点;否则找到合适位置(叶子节点下方),插入新节点
cpp
template<class T>
bool BSTree<T>::Insert(const T& data)
{
if (_pRoot == nullptr)
{
_pRoot = new Node(data);
return true;
}
PNode cur = _pRoot;
PNode parent = nullptr;
// 查找插入位置
while (cur)
{
parent = cur;
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return false; // 已存在,插入失败
}
// 创建新节点并连接到父节点
PNode newNode = new Node(data);
if (data < parent->_data)
parent->_pLeft = newNode;
else
parent->_pRight = newNode;
return true;
}
5 删除节点
先查找待删除节点,若不存在则返回false。删除分三种情况:
-
只有左孩子或无孩子:让父节点指向左孩子,然后删除该节点
-
只有右孩子:让父节点指向右孩子,然后删除该节点
-
左右孩子都存在:找到右子树的最小节点(或左子树的最大节点),用其值覆盖待删除节点,再递归删除该替代节点(转变为前两种情况)
cpp
template<class T>
bool BSTree<T>::Erase(const T& data)
{
if (_pRoot == nullptr)
return false;
// 1. 查找待删除节点及其父节点
PNode cur = _pRoot;
PNode parent = nullptr;
while (cur)
{
if (data == cur->_data)
break;
parent = cur;
if (data < cur->_data)
cur = cur->_pLeft;
else
cur = cur->_pRight;
}
if (cur == nullptr) // 未找到
return false;
// 2. 情况1:只有右孩子或为叶子(处理叶子时右孩子为空,左孩子也为空)
if (cur->_pLeft == nullptr)
{
// 若删除的是根节点,特殊处理
if (parent == nullptr)
_pRoot = cur->_pRight;
else if (cur == parent->_pLeft)
parent->_pLeft = cur->_pRight;
else
parent->_pRight = cur->_pRight;
delete cur;
}
// 情况2:只有左孩子
else if (cur->_pRight == nullptr)
{
if (parent == nullptr)
_pRoot = cur->_pLeft;
else if (cur == parent->_pLeft)
parent->_pLeft = cur->_pLeft;
else
parent->_pRight = cur->_pLeft;
delete cur;
}
// 情况3:左右孩子都存在
else
{
// 找右子树中最小的节点(最左)
PNode minParent = cur;
PNode minNode = cur->_pRight;
while (minNode->_pLeft)
{
minParent = minNode;
minNode = minNode->_pLeft;
}
// 覆盖值
cur->_data = minNode->_data;
// 删除minNode(它一定没有左孩子,按情况1处理)
if (minParent->_pLeft == minNode)
minParent->_pLeft = minNode->_pRight;
else
minParent->_pRight = minNode->_pRight;
delete minNode;
}
return true;
}
6 中序遍历
递归遍历(左-根-右),可得到升序序列
cpp
template<class T>
void BSTree<T>::_InOrder(PNode root)
{
if (root == nullptr)
return;
_InOrder(root->_pLeft);
std::cout << root->_data << " ";
_InOrder(root->_pRight);
}
template<class T>
void BSTree<T>::InOrder()
{
_InOrder(_pRoot);
std::cout << std::endl;
}
完整代码
cpp
#include <iostream>
using namespace std;
template<class T>
struct BSTNode
{
BSTNode(const T& data = T())
: _pLeft(nullptr), _pRight(nullptr), _data(data)
{}
BSTNode<T>* _pLeft;
BSTNode<T>* _pRight;
T _data;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
typedef Node* PNode;
public:
BSTree();
~BSTree();
PNode Find(const T& data);
bool Insert(const T& data);
bool Erase(const T& data);
void InOrder();
private:
PNode _pRoot;
void _Destroy(PNode& root);
void _InOrder(PNode root);
};
// 构造函数
template<class T>
BSTree<T>::BSTree()
: _pRoot(nullptr)
{}
// 析构辅助函数
template<class T>
void BSTree<T>::_Destroy(PNode& root)
{
if (root == nullptr)
return;
_Destroy(root->_pLeft);
_Destroy(root->_pRight);
delete root;
root = nullptr;
}
// 析构函数
template<class T>
BSTree<T>::~BSTree()
{
_Destroy(_pRoot);
}
// 查找
template<class T>
typename BSTree<T>::PNode BSTree<T>::Find(const T& data)
{
PNode cur = _pRoot;
while (cur)
{
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return cur;
}
return nullptr;
}
// 插入
template<class T>
bool BSTree<T>::Insert(const T& data)
{
if (_pRoot == nullptr)
{
_pRoot = new Node(data);
return true;
}
PNode cur = _pRoot;
PNode parent = nullptr;
while (cur)
{
parent = cur;
if (data < cur->_data)
cur = cur->_pLeft;
else if (data > cur->_data)
cur = cur->_pRight;
else
return false;
}
PNode newNode = new Node(data);
if (data < parent->_data)
parent->_pLeft = newNode;
else
parent->_pRight = newNode;
return true;
}
// 删除
template<class T>
bool BSTree<T>::Erase(const T& data)
{
if (_pRoot == nullptr)
return false;
PNode cur = _pRoot;
PNode parent = nullptr;
while (cur)
{
if (data == cur->_data)
break;
parent = cur;
if (data < cur->_data)
cur = cur->_pLeft;
else
cur = cur->_pRight;
}
if (cur == nullptr)
return false;
// 只有右孩子或叶子
if (cur->_pLeft == nullptr)
{
if (parent == nullptr)
_pRoot = cur->_pRight;
else if (cur == parent->_pLeft)
parent->_pLeft = cur->_pRight;
else
parent->_pRight = cur->_pRight;
delete cur;
}
// 只有左孩子
else if (cur->_pRight == nullptr)
{
if (parent == nullptr)
_pRoot = cur->_pLeft;
else if (cur == parent->_pLeft)
parent->_pLeft = cur->_pLeft;
else
parent->_pRight = cur->_pLeft;
delete cur;
}
// 左右孩子都有
else
{
PNode minParent = cur;
PNode minNode = cur->_pRight;
while (minNode->_pLeft)
{
minParent = minNode;
minNode = minNode->_pLeft;
}
cur->_data = minNode->_data;
if (minParent->_pLeft == minNode)
minParent->_pLeft = minNode->_pRight;
else
minParent->_pRight = minNode->_pRight;
delete minNode;
}
return true;
}
// 中序遍历辅助
template<class T>
void BSTree<T>::_InOrder(PNode root)
{
if (root == nullptr)
return;
_InOrder(root->_pLeft);
cout << root->_data << " ";
_InOrder(root->_pRight);
}
// 中序遍历
template<class T>
void BSTree<T>::InOrder()
{
_InOrder(_pRoot);
cout << endl;
}
// 测试
int main()
{
BSTree<int> bst;
bst.Insert(5);
bst.Insert(3);
bst.Insert(7);
bst.Insert(1);
bst.Insert(4);
bst.Insert(6);
bst.Insert(8);
cout << "中序遍历: ";
bst.InOrder(); // 1 3 4 5 6 7 8
cout << "查找4: " << (bst.Find(4) ? "找到" : "未找到") << endl;
cout << "查找9: " << (bst.Find(9) ? "找到" : "未找到") << endl;
bst.Erase(5);
cout << "删除5后: ";
bst.InOrder(); // 1 3 4 6 7 8
bst.Erase(1);
cout << "删除1后: ";
bst.InOrder(); // 3 4 6 7 8
return 0;
}
二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到 的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
a.以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树;
b.在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方 式在现实生活中非常常见:
a.比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英 文单词与其对应的中文就构成一种键值对;
b.再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出 现次数就是就构成一种键值对。
性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最好情况
如果二叉搜索树接近完全二叉树,那么树高大约是 log₂N,平均比较次数也是 log₂N。这时 BST 很高效。
最坏情况
如果插入顺序很差,比如有序插入:1, 2, 3, 4, 5。那 BST 会退化成一条链(因为 BST 不自平衡 )。这时平均比较次数接近 N/2。
也就是说:本来希望像二分查找一样快,结果退化后,接近顺序查找
这就是 BST 最大的问题。能不能不管按什么顺序插入,都让性能保持接近最优?答案就是:
AVL 树,红黑树
它们的作用就是:在插入删除后,尽量维持树的平衡,避免退化。
二叉树进阶面试题
-
二叉树创建字符串。606. 根据二叉树创建字符串 - 力扣(LeetCode)
-
二叉树的分层遍历1。102. 二叉树的层序遍历 - 力扣(LeetCode)
-
二叉树的分层遍历2。107. 二叉树的层序遍历 II - 力扣(LeetCode)
-
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。236. 二叉树的最近公共祖先 - 力扣(LeetCode)
-
二叉树搜索树转换成排序双向链表。二叉搜索树与双向链表_牛客题霸_牛客网
-
根据一棵树的前序遍历与中序遍历构造二叉树。 105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
-
根据一棵树的中序遍历与后序遍历构造二叉树。106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
-
二叉树的前序遍历,非递归迭代实现 。144. 二叉树的前序遍历 - 力扣(LeetCode)
-
二叉树中序遍历 ,非递归迭代实现。94. 二叉树的中序遍历 - 力扣(LeetCode)
-
二叉树的后序遍历 ,非递归迭代实现。145. 二叉树的后序遍历 - 力扣(LeetCode)
