二叉搜索树 --- 概念 + 模拟
云在天上,我在你心上!
大家好,很高兴又和大家见面了。下面将带来二叉搜索树的相关知识。本文将详细介绍二叉搜索树的实现原理及其模拟方法。
前言:
大家学习前可以先学习一下二叉树相关的内容。如果觉得c++相关概念不清楚的话,可以看看博主前面的文章,看后一定会有收获的!
1.二叉搜素树
1.1二叉搜索树的概念
二叉搜索树又称二叉排序树,它要么是一颗空树,要么是具有以下性质的二叉树:
1.若它的左子树不为空,则左子树上所有的节点的值都小于根节点的值
2.若它的右子树不为空,则右子树上所有的节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树
像上面这张图就是一个典型的二叉搜索树的图片了。
1.2二叉搜索树操作
1.2.1二叉搜索树的查找
a.从根节点开始比较,查找对应节点,比根大的往右边走,比根小的往左边走
b.最多查找高度次,走到空,还没找到,这个值就不存在
1.2.2二叉搜索树的插入
插入的具体过程如下:
a.树为空,则直接新增节点,复制给root指针
b.树不为空,按二叉搜索树性质查找插入位置,插入新节点
1.2.3二叉搜索树的删除
首先查找元素是否存在二叉树中,如果不存在,则返回,否则要删除的节点分为下面四种情况:
a.要删除的节点无孩子节点
b.要删除的节点只有左孩子节点
b.要删除的节点只有右孩子节点
d.要删除的节点有左右孩子节点
看起来要删除的节点有4种情况,实际a和b或者c合并起来,因此真正的删除过程如下:
1.没有孩子和只有一个孩子的情况:判断被删除节点和自己父节点的关系,被删除节点是父节点的右(左)孩子,就将被删除节点的孩子链接到自己父节点的右(左)边。
2.有两个孩子的情况:找被删除节点的左子树的最大节点A,然后将它的值和被删除节点交换,然后需要处理被删除节点。如果A节点是A的父节点的右子树,A的左边的子树链接到A的父节点的右边,如果A节点是A的父节点的左子树,A的左边的子树链接到A父节点的左边。(这里主要是因为A节点是不可能还有右子树了的)
(当然也可以找右子树的最小值,然后处理方法差不多)
1.2.4模拟实现
template<class T>
struct BSTreeNode
{BSTreeNode(const T& val = T()): _val(val), _left(nullptr), _right(nullptr){}T _val;BSTreeNode<T>* _left;BSTreeNode<T>* _right;
};template<class T>
class BSTree
{typedef BSTreeNode<T> Node;
private:Node* Copy(Node* root){if (root == nullptr) return nullptr;Node* tmp = new Node(root->_val);tmp->_left = Copy(root->_left);tmp->_right = Copy(root->_right);return tmp;}// 递归销毁void Destroy(Node*& root){if (root == nullptr) return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}// 中序遍历void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_left);cout << root->_val << " ";_InOrder(root->_right);}
public:BSTree(): _root(nullptr){}BSTree(const BSTree<T>& bt){_root = Copy(bt._root);}bool Insert(const T& val){if (_root == nullptr){_root = new Node(val);return true;}// 插入Node* cur = _root;Node* prev = nullptr;while (cur != nullptr){if (val > cur->_val) // 插入值大于当前节点{prev = cur;cur = cur->_right;}else if (val < cur->_val) // 插入值小于当前节点{prev = cur;cur = cur->_left;}else{// 值相等??return false;}}cur = new Node(val);if (prev->_val < val){prev->_right = cur;}else{prev->_left = cur;}return true;}bool Find(const T& val){if (_root == nullptr) return false;Node* cur = _root;while (cur != nullptr){if (cur->_val < val) // 查找值大于节点值{cur = cur->_right;}else if (cur->_val > val){cur = cur->_left;}else return true;}return false;}bool Erase(const T& val){// 删除值为val的节点if (_root == nullptr) return false;Node* cur = _root;Node* prev = nullptr;while (cur != nullptr){if (cur->_val < val){prev = cur;cur = cur->_right;}else if (cur->_val > val){prev = cur;cur = cur->_left;}else{// cur节点就是将被删除的节点if (cur->_left == nullptr){if (cur == _root)// 处理cur节点是根节点的特殊情况{_root = _root->_right;}else{if (prev->_left == cur){prev->_left = cur->_right;}else{prev->_right = cur->_right;}}}else if (cur->_right == nullptr){if (cur == _root)// 处理cur节点是根节点的特殊情况{_root = _root->_left;}if (prev->_left == cur){prev->_left = cur->_left;}else{prev->_right = cur->_left;}}else{// 两个孩子Node* p = cur;Node* l = cur->_right;while (l->_right != nullptr){p = l;l = l->_right;}// l指向左边最大值// 交换值std::swap(l->_val, cur->_val);// 处理l节点if (p->_left == l) p->_left = l->_left;else p->_right = l->_left;cur = l;// 现在删除l即可}delete cur;return true;}}}~BSTree(){Destroy(_root);}void InOrder(){_InOrder(_root);}
private:Node* _root;};
当然这里面也有一些接口可以使用递归实现,比如查找,插入和删除三个接口的设计:
bool _Insert(Node*& root, const T& val)//这里的root如果单纯传递Node*的话,还需要找父亲
{if (root == nullptr){root = new Node(val);return true;}if (root->_val < val) return _Insert(root->_right, val);else if (root->_val > val) return _Insert(root->_left, val);else return false;
}bool _Find(Node* root, const T& val)
{if (root == nullptr) return false;if (root->_val < val) return _Find(root->_right, val);else if (root->_val > val) return _Find(root->_left, val);else return true;
}bool _Erase(Node*& root, const T& val)
{// 删除值为val的节点if (root == nullptr) return false;if (root->_val < val) return _Erase(root->_right, val);else if (root->_val > val) return _Erase(root->_left, val);else{// 查找到了该节点Node* del = root;//将root这个将被覆盖的节点保存下来,后面拿去释放//1.左为空//2.右为空//3.左右都不为空if (root->_left == nullptr){root = root->_right;}else if (root->_right == nullptr){root = root->_left;}else{//去找左子树的最大值Node* LeftMax = root->_left;while (LeftMax->_right){LeftMax = LeftMax->_right;}std::swap(root->_val, LeftMax->_val);// 交换到了左子树里面去了,递归处理将它删除掉即可return _Erase(root->_left, val);}delete del;}
bool Insert(const T& val){return _Insert(_root, val);}bool Find(const T& val){return _Find(_root, val);}bool Erase(const T& val){return _Erase(_root, val);}
这里可能不好理解的地方应该是删除这里为什么最后用了一个return _Erase(root->_left, val)
主要是我们完成前面画图的交换值后,其实对于要删除的节点的左子树再进行一个递归删除刚好附用了前面的代码,直接就解决了问题。刚好就删除了对方。
实在不行,大家也可以这样写,像前面那样,处理一下即可:
bool _Erase(Node*& root, const T& val)
{// 删除值为val的节点if (root == nullptr) return false;if (root->_val < val) return _Erase(root->_right, val);else if (root->_val > val) return _Erase(root->_left, val);else{// 查找到了该节点Node* del = root;//将root这个将被覆盖的节点保存下来,后面拿去释放//1.左为空//2.右为空//3.左右都不为空if (root->_left == nullptr){root = root->_right;}else if (root->_right == nullptr){root = root->_left;}else{// 两个孩子Node* p = root;Node* l = root->_right;while (l->_right != nullptr){p = l;l = l->_right;}// l指向左边最大值// 交换值std::swap(l->_val, root->_val);// 处理l节点if (p->_left == l) p->_left = l->_left;else p->_right = l->_left;del = l;// 现在删除l即可}delete del;}
}
实现完上面的部分,我们也完成了大部分的了,我们再来实现一下赋值运算符重载吧:
// 赋值运算符重载
BSTree<T>& operator=(const BSTree<T>& t)
{if (&t != this){BSTree<T> ret(t);std::swap(ret._root, _root);}return *this;
}
这里我们使用了一个现代的写法,通过拷贝构造创建出一个临时的变量ret,然后将ret的_root和自身的_root值交换掉,退出函数的时候,临时变量销毁调用自己的析构函数就完成了数据的销毁。
2.二叉树的应用
2.1K模型:
K模型即只有Key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
2.1.1.以词库中所有的单词集合中的每个单词作为Key,构建一棵二叉搜索树。
2.1.2在二叉搜索树中检索该单词是否存在,存在则拼写正确,不正确则拼写错误。
2.2KV模型:
每一个关键码Key都有与之对应的值Value,即<Key, Value>的键值对,该种方式在现实生活中非常的常见:
2.2.1.比如词典中就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文就是<word, chinese>就构成了一种键值对。
2.2.2再比如统计单词次数,统计成功后,给定单词就可以快速找到其出现的次数,单词与其出现次数就是<word, count>就构成了一种键值对。
2.3模拟实现KV模型的搜索二叉树:
2.3.1模拟实现
template<class K, class V>
class BSTree
{
private:typedef BSTreeNode<K, V> Node;private:Node* Copy(const Node* root){// 复制if (root == nullptr) return nullptr;Node* tmp = new Node(root->_key, root->_val);tmp->_left = Copy(root->_left);tmp->_right = Copy(root->_right);return tmp;}bool _Insert(Node*& root, const K& key, const V& value){if (root == nullptr){root = new Node(key, value);return true;}if (key > root->_key) return _Insert(root->_right, key, value);else if (key < root->_key) return _Insert(root->_left, key, value);else return false;}Node* _Find(Node* root, const K& key){// 查找keyif (root == nullptr) return nullptr;if (key > root->_key) return _Find(root->_right, key);else if (key < root->_key) return _Find(root->_left, key);else return root;}bool _Erase(Node*& root, const K& key){if (root == nullptr) return false;if (key > root->_key) return _Erase(root->_right, key);else if (key < root->_key) return _Erase(root->_left, key);else{// 该节点为要删除的节点Node* del = root;// 记录该节点if (root->_left == nullptr){root = root->_right;}else if (root->_right == nullptr){root = root->_left;}else{// 找该节点的左子树最大值Node* Max = root->_left;while (Max->_right){Max = Max->_right;}std::swap(root->_key, Max->_key);std::swap(root->_val, Max->_val);return _Erase(root->_left, key);}delete del;return true;}}void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_left);cout << root->_key << ":" << root->_val << " ";_InOrder(root->_right);}void _Destroy(Node*& root){if (root == nullptr) return;_Destroy(root->_left);_Destroy(root->_right);delete root;root = nullptr;}
public:BSTree(): _root(nullptr){}BSTree(const BSTree<K, V>& t){// 拷贝构造函数_root = Copy(t._root);}// 赋值运算符重载BSTree<K, V>& operator=(const BSTree<K, V> tmp){std::swap(_root, tmp->_root);return *this;}~BSTree(){Destroy();}// 插入bool Insert(const K& key, const V& value){return _Insert(_root, key, value);}// 查找Node* Find(const K& key){return _Find(_root, key);}// 删除bool Erase(const K& key){return _Erase(_root, key);}// 中序遍历void InOrder(){return _InOrder(_root);}void Destroy(){_Destroy(_root);}private:Node* _root;
};
这个部分的实现和前面的实现差不多,只是变成了KV模型了。大家可以参考前面的实现看看。
3.二叉搜索树的性能分析
我们的插入和删除都必须要先查找到对应的节点,查找的性能就决定了对应操作的性能
对于有着n个节点的二叉搜索树,若每个元素查找的概率相同,则二叉搜索树平均查找长度是节点在二叉搜索树的深度的函数,即节点越深,则比较的次数越多。
但是对于同一个关键码集合,如果各个关键码插入的次序不用,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为logN
最差情况下,二叉搜索树退化为如上图的单支树(或者类似单支树),其平均比价次数为N
问题:如果退化成单支树,二叉搜索树的性能就没有了。还能够改进吗?不论按照什么次序插入,二叉搜索树的性能都能达到最优?这个将在后续文章的AVL树和红黑树中讲到。大家敬请期待!
结语
这篇文章很好的讲解了二叉搜索树的相关概念和对应的模拟实现,关于其后续的内容讲解将在之后的文章中给大家带来!创作不易,希望大家能够多多点赞 + 收藏 + 关注!!!
希望大家能够有所收获,不断进步!下一篇文章中,我们再会!!