搜索二叉树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
由于搜索二叉树中,每个结点左子树上所有结点的值都小于该结点的值,右子树上所有结点的值都大于该结点的值,因此对搜索二叉树进行中序遍历后,得到的是升序序列,这也是使用搜索二叉树的意义。
搜索二叉树常用操作
以下面这个树为例,我们来介绍一下搜索二叉树常用的操作。
1.搜索二叉树的查找
因为搜索二叉树存在左子树节点的值小于根节点的值,右子树节点的值大于根节点的值的性质,所以在二叉搜索树当中查找一个我们想要的数据的效率是非常快的,查找功能也可以算是二叉搜索树当中最重要的功能。
当我们需要在一个搜索二叉树中查找想要的数据时只需要按下面的过程:
- 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
- 最多查找高度次,走到到空,还没找到,这个值不存在。
2.搜索二叉树的插入
插入的具体过程如下:
- 树为空,则直接新增节点,赋值给root指针
- 树不空,按二叉搜索树性质查找插入位置,插入新节点
3.搜索二叉树的删除
搜索二叉树最重要的就是左子树节点的值小于根节点的值,右子树节点的值大于根节点的值的性质,当我们删除树中的数据时不能破坏搜索二叉树的性质,否则这颗树也就失去了存在的意义,因此删除操作的过程较为复杂,需要考虑比较多的情况。
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
- A:要删除的结点无孩子结点
- B:要删除的结点只有左孩子结点
- C:要删除的结点只有右孩子结点
- D:要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况A可以与情况B或者C合并起来,因此真正的删除过程
如下:
- 情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除。
- 情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除。
- 情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除。
模拟实现
下面我们将要模拟实现一个搜索二叉树,使其具有刚刚介绍的最基本的功能。
结点类
要实现二叉搜索树,我们首先需要实现一个结点类:
- 结点类当中包含三个成员变量:结点值、左指针、右指针。
- 结点类当中只需实现一个构造函数即可,用于构造指定结点值的结点。
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
搜索二叉树类
//二叉搜索树
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
//构造函数
BSTree();
//拷贝构造函数
BSTree(const BSTree<K>& t);
//赋值运算符重载函数
BSTree<K>& operator=(BSTree<K> t);
//析构函数
~BSTree();
//插入函数
bool Insert(const K& key);
//删除函数
bool Erase(const K& key);
//查找函数
Node* Find(const K& key);
//中序遍历
void InOrder();
private:
Node* _root; //指向二叉搜索树的根结点
};
构造函数
构造函数非常简单,构造一个空树即可。
//构造函数
BSTree()
:_root(nullptr)
{}
拷贝构造函数
拷贝构造函数实现也很简单,拷贝一棵和所给二叉搜索树相同的树即可。
//拷贝树
Node* _Copy(Node* root)
{
if (root == nullptr) //空树直接返回
return nullptr;
Node* copyNode = new Node(root->_key); //拷贝根结点
copyNode->_left = _Copy(root->_left); //拷贝左子树
copyNode->_right = _Copy(root->_right); //拷贝右子树
return copyNode; //返回拷贝的树
}
//拷贝构造函数
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root); //拷贝t对象的二叉搜索树
}
赋值运算符重载函数
传统方法
//传统写法
const BSTree<K>& operator=(const BSTree<K>& t)
{
if (this != &t) //防止自己给自己赋值
{
_root = _Copy(t._root); //拷贝t对象的二叉搜索树
}
return *this; //支持连续赋值
}
现代写法
//现代写法
BSTree<K>& operator=(BSTree<K> t) //编译器接收右值的时候自动调用拷贝构造函数
{
swap(_root, t._root); //交换这两个对象的二叉搜索树
return *this; //支持连续赋值
}
析构函数
析构函数完成对象中二叉搜索树结点的释放,注意释放时采用后序释放,当二叉搜索树中的结点被释放完后,将对象当中指向二叉搜索树的指针及时置空即可。
//释放树中结点
void _Destory(Node* root)
{
if (root == nullptr) //空树无需释放
return;
_Destory(root->_left); //释放左子树中的结点
_Destory(root->_right); //释放右子树中的结点
delete root; //释放根结点
}
//析构函数
~BSTree()
{
_Destory(_root); //释放二叉搜索树中的结点
_root = nullptr; //及时置空
}
打印函数
在实现搜索二叉树的查找、插入、删除函数之前首先实现一个简单的函数提供打印这个二叉树的功能,方便测试下面其他的函数是否实现正常。
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
插入函数
非递归
bool Insert(const K& key)
{
//空树的情况 直接申请值为key的结点作为二叉搜索树的根结点 插入成功,返回true
if (_root == nullptr)
{
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
//key值等于当前结点的值 插入失败
if (key == cur->_key)
return false;
//key值大于当前结点的值 往该结点的右子树走
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//key值小于当前结点的值 往该结点的左子树走
else
{
parent = cur;
cur = cur->_left;
}
}
cur = new Node(key); //申请值为key的结点
if (key < parent->_key) //key值小于当前parent结点的值
{
parent->_left = cur; //将结点连接到parent的左边
}
else //key值大于当前parent结点的值
{
parent->_right = cur; //将结点连接到parent的右边
}
return true; //插入成功,返回true
}
递归
递归实现二叉搜索树的插入操作也是很简单的,但是要特别注意的一点就是,递归插入函数的子函数接收参数root时,必须采用引用接收,因为只有这样我们才能将二叉树当中的各个结点连接起来。
//插入函数(递归)
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
//传引用时即可以直接修改节点的子节点指向,不需要再单独使用父节点控制
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
root = new Node(key);
return true;
}
if (root->_key < key)
{
return _InsertR(root->_right, key);
}
else if (root->_key > key)
{
return _InsertR(root->_left, key);
}
else
{
return false;
}
}
查找函数
非递归
//查找函数
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_key) //key值小于该结点的值
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_key) //key值大于该结点的值
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //找到了值为key的结点
{
return cur; //查找成功,返回结点地址
}
}
return nullptr; //树为空或查找失败,返回nullptr
}
递归
//查找函数(递归)
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool _FindR(Node* root, const K& key)
{
//空树查找失败
if (root == nullptr)
return false;
//根节点值等于key值 查找成功
if (root->_key == key)
return true;
//key值大于节点值 去右树找
if (root->_key < key)
return _FindR(root->_right, key);
//key值小于节点值 去左树找
else
return _FindR(root->_left, key);
}
删除函数
非递归实现
bool Erase(const K& key)
{
Node* parent = nullptr; //标记待删除结点的父结点
Node* cur = _root; //标记待删除结点
while (cur)
{
if (key < cur->_key) //key值小于当前结点的值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key) //key值大于当前结点的值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //找到了待删除的节点
{
//1.左为空
if (cur->_left == nullptr)
{
if (cur == _root)//要删除的节点是根节点的情况
{
_root = cur->_right;
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (parent->_left == cur) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_right;//父结点的左指针指向待删除结点的右子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
//2.待删除结点的右子树为空
else if (cur->_right == nullptr)
{
if (cur == _root)//要删除的节点是根节点的情况 此时parent为nullptr
{
_root = cur->_left; //父结点的左指针指向待删除结点的左子树即可
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (cur == parent->_left) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
//3.待删除结点的左右子树均不为空
else
{
//替换法删除
Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
//寻找待删除结点右子树当中值最小的结点
while (minRight->_left)
{
//一直往左走
minParent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
//注意一个隐含条件:此时minRight的_left为空
if (minRight == minParent->_left) //minRight是其父结点的左孩子
{
minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
}
else //minRight是其父结点的右孩子
{
minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
}
delete minRight; //释放minRight
return true; //删除成功,返回true
}
}
}
return false; //没有找到待删除结点,删除失败,返回false
}
递归实现
//删除函数(递归)
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
//key值大于节点值 去右树删除
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
//key值小于节点值 去左树删除
if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else //开始删除
{
Node* del = root;
// 开始准备删除
if (root->_right == nullptr)
{
root = root->_left;
}
else if (root->_left == nullptr)
{
root = root->_right;
}
else
{
Node* maxleft = root->_left;
while (maxleft->_right)
{
maxleft = maxleft->_right;
}
//swap(root->_key, maxleft->_key);
root->_key = maxleft->_key;
return _EraseR(root->_left, maxleft->_key);
}
delete del;
return true;
}
}
搜索二叉树的应用
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
首先我们先将上面实现的搜索二叉树改为KV结构。
namespace key_value
{
template<class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* _left;
BSTreeNode<K,V>* _right;
K _key;
V _value;
BSTreeNode(const K& key,const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
,_value(value)
{}
};
template<class K,class V>
class BSTree
{
typedef BSTreeNode<K,V> Node;
public:
//插入函数(非递归)
bool Insert(const K& key, const V& value)
{
//空树的情况 直接申请值为key的结点作为二叉搜索树的根结点 插入成功,返回true
if (_root == nullptr)
{
_root = new Node(key,value);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur != nullptr)
{
//key值等于当前结点的值 插入失败
if (key == cur->_key)
return false;
//key值大于当前结点的值 往该结点的右子树走
else if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
//key值小于当前结点的值 往该结点的左子树走
else
{
parent = cur;
cur = cur->_left;
}
}
cur = new Node(key,value); //申请值为key的结点
if (key < parent->_key) //key值小于当前parent结点的值
{
parent->_left = cur; //将结点连接到parent的左边
}
else //key值大于当前parent结点的值
{
parent->_right = cur; //将结点连接到parent的右边
}
return true; //插入成功,返回true
}
Node* Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key < cur->_key) //key值小于该结点的值
{
cur = cur->_left; //在该结点的左子树当中查找
}
else if (key > cur->_key) //key值大于该结点的值
{
cur = cur->_right; //在该结点的右子树当中查找
}
else //找到了值为key的结点
{
return cur; //查找成功,返回结点地址
}
}
return nullptr; //树为空或查找失败,返回nullptr
}
//删除函数(非递归)
bool Erase(const K& key)
{
Node* parent = nullptr; //标记待删除结点的父结点
Node* cur = _root; //标记待删除结点
while (cur)
{
if (key < cur->_key) //key值小于当前结点的值
{
//往该结点的左子树走
parent = cur;
cur = cur->_left;
}
else if (key > cur->_key) //key值大于当前结点的值
{
//往该结点的右子树走
parent = cur;
cur = cur->_right;
}
else //找到了待删除的节点
{
//1.左为空
if (cur->_left == nullptr)
{
if (cur == _root)//要删除的节点是根节点的情况
{
_root = cur->_right;
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (parent->_left == cur) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_right;//父结点的左指针指向待删除结点的右子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
//2.待删除结点的右子树为空
else if (cur->_right == nullptr)
{
if (cur == _root)//要删除的节点是根节点的情况 此时parent为nullptr
{
_root = cur->_left; //父结点的左指针指向待删除结点的左子树即可
}
else //待删除结点不是根结点,此时parent不为nullptr
{
if (cur == parent->_left) //待删除结点是其父结点的左孩子
{
parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
}
else //待删除结点是其父结点的右孩子
{
parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
}
}
delete cur; //释放待删除结点
return true; //删除成功,返回true
}
//3.待删除结点的左右子树均不为空
else
{
//替换法删除
Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
//寻找待删除结点右子树当中值最小的结点
while (minRight->_left)
{
//一直往左走
minParent = minRight;
minRight = minRight->_left;
}
cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
//注意一个隐含条件:此时minRight的_left为空
if (minRight == minParent->_left) //minRight是其父结点的左孩子
{
minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
}
else //minRight是其父结点的右孩子
{
minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
}
delete minRight; //释放minRight
return true; //删除成功,返回true
}
}
}
return false; //没有找到待删除结点,删除失败,返回false
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
BSTree()
:_root(nullptr)
{}
BSTree(const BSTree<K,V>& t)
{
_root = _Copy(t._root);
}
~BSTree()
{
Destroy(_root);
}
protected:
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key);
newRoot->_left = _Copy(root->_left);
newRoot->_right = _Copy(root->_right);
return newRoot;
}
void Destroy(Node*& root)
{
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
private:
Node* _root = nullptr;
};
}
对修改好的KV结构搜索二叉树进行测试
//测试函数1
void BSTreeTest4()
{
using namespace key_value;
// 输入单词,查找单词对应的中文翻译
key_value::BSTree<string, string> dict;
dict.Insert("string", "字符串");
dict.Insert("tree", "树");
dict.Insert("left", "左边、剩余");
dict.Insert("right", "右边");
dict.Insert("sort", "排序");
// 插入词库中所有单词
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(str);
if (ret == nullptr)
{
cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
}
else
{
cout << str << "中文翻译:" << ret->_value << endl;
}
}
}
测试结果正常:
测试函数2
void BSTreeTest5()
{
using namespace key_value;
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (const auto& str : arr)
{
// 先查找水果在不在搜索树中
// 1、不在,说明水果第一次出现,则插入<水果, 1>
// 2、在,则查找到的节点中水果对应的次数++
//BSTreeNode<string, int>* ret = countTree.Find(str);
auto ret = countTree.Find(str);
if (ret == NULL)
{
countTree.Insert(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
测试结果正常
搜索二叉的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N