目录
1.内容安排说明
二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:
1.map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
2.二叉搜索树的特性了解,有助于更好的理解map和set的特性
3.二叉树中部分面试题稍微有些难度,在前面讲解大家不同容易接受,且时间长容易忘
4.有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。
因此本节借二叉树搜索树,对二叉树部分进行收尾总结。
2.二叉搜索树
二叉搜索树概念
二叉搜索树(BST,Binary Search Tree)又称二叉排序树或二叉查找树,它或者是一棵空树,或者是具有以下性质的二叉树:
❍ 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
❍ 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
❍ 它的左右子树也分别为二叉搜索树
❍ 二叉搜索树的中序遍历为递增排序
二叉搜索树操作
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
二叉搜索树的查找
1️⃣ 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
2️⃣ 最多查找高度次,走到空,还没找到,这个值不存在
二叉搜索树的插入
插入的具体过程如下:
1️⃣ 树为空,则直接新增节点,赋值给root指针
2️⃣ 树不空,按二叉搜索树性质查找插入位置,插入新节点
二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回,否则要删除的节点可能分下面四种情况:
1️⃣ 要删除的节点无孩子节点
2️⃣ 要删除的结点只有左孩子节点
3️⃣ 要删除的结点只有右孩子结点
4️⃣ 要删除的结点有左、右孩子结点
看起来有待删除节点有4种情况,实际情况 a 可以和情况 b 或者 c 合并起来,因此真正的删除过程如下:
❍ 情况b:删除该结点且使被删除节点的双亲节点指向被删除节点的左孩子节点--直接删除
❍ 情况c:删除该结点且使被删除节点的双亲节点指向被删除节点的右孩子节点--直接删除
❍ 情况d:在它的右子树中寻找中序下的第一个节点(关键码最小)|| 左子树的最大的节点,用它的值填补到被删除节点中,再来处理该结点的删除问题--(替换法删除)
或者
二叉搜索树的实现
//
// SearchBinaryTree.h
// 二叉搜索树
//
// Created by 南毅 on 2024/7/10.
//
#include<iostream>
using namespace std;
//二叉搜索树节点
template<class K>
struct BSTNode{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key),
_left(nullptr),
_right(nullptr){
}
};
//二叉搜索树
template<class K>
class BSTree{
private:
typedef BSTNode<K> Node;
Node* _root = nullptr;
void _InOrder(Node* root){
if(root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key <<" ";
_InOrder(root->_right);
}
public:
void InOrder(){
_InOrder(_root);
cout << endl;
}
//二叉搜索树的查找
bool Find(const K& key){
Node* cur = _root;
while (cur) {
if(cur->_key < key)
{
cur = cur -> _right;
}
else if(cur->_key > key)
{
cur = cur -> _left;
}else
{
return true;
}
}
return false;
}
//二叉树的插入
bool Insert(const K& key){
//如果树不存在
if(_root == nullptr)
{
_root = new Node(key);
return true;
}
//树存在
//1.找到待插入节点的位置(借助二叉搜索树性质)
//2.与父母亲 相连
Node* parents = nullptr;
Node* cur = _root;
while (cur) {
if(cur->_key < key)
{
parents = cur;
cur = cur -> _right;
}
else if(cur->_key > key)
{
parents = cur;
cur = cur -> _left;
}else
{
//二叉树搜索性质:如果有重复不插入
//插入失败
return false;
}
}
//找到待插入节点位置
cur = new Node(key);
//比父母节点大 做右子树
if(parents->_key < key){
parents->_right = cur;
}else
{
parents->_left = cur;
}
//插入成功
return true;
}
//删除二叉搜索树中某个节点
bool Erase(const K& key){
Node* parent = nullptr;
Node* cur = _root;
//找到 待删除的节点
while(cur){
if(cur->_key < key){
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key){
parent = cur;
cur = cur->_left;
}
else{
//删除
//0-1个孩子情况
if(cur->_left == nullptr){
//如果是删除根节点
if(parent == nullptr){
_root = cur->_right;
}else{
//删除的不是根节点
if(parent->_left == cur){
parent->_left = cur->_right;
}else{
parent->_right = cur->_right;
}
}
delete cur;
return true;
}
else if(cur->_right == nullptr)
{
if(parent == nullptr){
_root = cur->_left;
}else{
if(parent->_left == cur){
parent->_left = cur->_left;
}else{
parent->_right = cur->_left;
}
}
delete cur;
return true;
}
else{
// 2个孩子的情况
// 找左最大 右最小
// 这里我们找右最小 作为替代节点
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while(rightMin->_left){
rightMinP = rightMin;
rightMin = rightMin->_left;
}
// 替换
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin) {
rightMinP->_left = rightMin->_right;
}else{
rightMinP->_right = rightMin->_right;
}
delete rightMin;
return true;
}
}
}
return false;
}
};
二叉搜索树的应用
1️⃣ K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
❍ 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
❍ 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2️⃣ KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
❍ 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
❍ 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出
现次数就是<word, count>就构成一种键值对。
namespace keyValue
{
template<class K,class V>
struct BSTNode{
K _key;
V _value;
BSTNode<K,V>* _left;
BSTNode<K,V>* _right;
BSTNode(const K& key,const V& value)
:_key(key),
_value(value),
_left(nullptr),
_right(nullptr){
}
};
//二叉搜索树
template<class K,class V>
class BSTree{
private:
typedef BSTNode<K,V> Node;
Node* _root = nullptr;
void _InOrder(Node* root){
if(root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key <<" ";
_InOrder(root->_right);
}
// 后续遍历删除
void Destroy(Node* root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key, root->_value);
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
return newRoot;
}
public:
BSTree() = default;
BSTree(const BSTree<K, V>& t)
{
_root = Copy(t._root);
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
void InOrder(){
_InOrder(_root);
cout << endl;
}
//二叉搜索树的查找
Node* Find(const K& key){
Node* cur = _root;
while (cur) {
if(cur->_key < key)
{
cur = cur -> _right;
}
else if(cur->_key > key)
{
cur = cur -> _left;
}else
{
return cur;
}
}
return nullptr;
}
//二叉树的插入
bool Insert(const K& key,const V& value){
//如果树不存在
if(_root == nullptr)
{
_root = new Node(key,value);
return true;
}
//树存在
//1.找到待插入节点的位置(借助二叉搜索树性质)
//2.与父母亲 相连
Node* parents = nullptr;
Node* cur = _root;
while (cur) {
if(cur->_key < key)
{
parents = cur;
cur = cur -> _right;
}
else if(cur->_key > key)
{
parents = cur;
cur = cur -> _left;
}else
{
//二叉树搜索性质:如果有重复不插入
//插入失败
return false;
}
}
//找到待插入节点位置
cur = new Node(key,value);
//比父母节点大 做右子树
if(parents->_key < key){
parents->_right = cur;
}else
{
parents->_left = cur;
}
//插入成功
return true;
}
//删除二叉搜索树中某个节点
bool Erase(const K& key){
Node* parent = nullptr;
Node* cur = _root;
//找到 待删除的节点
while(cur){
if(cur->_key < key){
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key){
parent = cur;
cur = cur->_left;
}
else{
//删除
//0-1个孩子情况
if(cur->_left == nullptr){
//如果是删除根节点
if(parent == nullptr){
_root = cur->_right;
}else{
//删除的不是根节点
if(parent->_left == cur){
parent->_left = cur->_right;
}else{
parent->_right = cur->_right;
}
}
delete cur;
return true;
}
else if(cur->_right == nullptr)
{
if(parent == nullptr){
_root = cur->_left;
}else{
if(parent->_left == cur){
parent->_left = cur->_left;
}else{
parent->_right = cur->_left;
}
}
delete cur;
return true;
}
else{
// 2个孩子的情况
// 找左最大 右最小
// 这里我们找右最小 作为替代节点
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while(rightMin->_left){
rightMinP = rightMin;
rightMin = rightMin->_left;
}
// 替换
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin) {
rightMinP->_left = rightMin->_right;
}else{
rightMinP->_right = rightMin->_right;
}
delete rightMin;
return true;
}
}
}
return false;
}
};
}
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:$log_2 N$
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:$\frac{N}{2}$
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。