【C++】二叉搜索树及其模拟实现
目录
- 一、核心概念
- 1.1 基本定义
- 1.2 二叉搜索树的性能分析
- 1.3 基本框架搭建
- 1.3.1 结点结构的定义
- 1.3.2 二叉搜索树的类框架
- 二、二叉搜索树的模拟实现
- 2.1 插入 insert
- 2.2 中序遍历 InOrder
- 2.3 查找操作 Find
- 2.4 删除操作
- 2.5 插入的递归写法
- 三、<key, value> 的应用场景
- 3.1 应用场景
个人主页<—请点击
C++专栏<—请点击
一、核心概念
1.1 基本定义
二叉搜索树是一种特殊的二叉树,满足以下性质:左子树所有节点的值小于根节点的值;右子树所有节点的值大于根节点的值;左右子树也都是二叉搜索树。
如上图所示,就是一颗二叉搜索树。值得一提的是它的中序遍历是有序的,对于上面的树:1 3 4 6 7 8 10 13 14
。
1.2 二叉搜索树的性能分析
查找操作:最佳情况:平衡树,O(log n)
,最坏情况:退化成链表,O(n)
,平均情况:随机数据,O(log n)
。
插入操作的时间复杂度与查找相同,需要额外的节点创建开销。
删除操作:最复杂的操作,有三种情况,叶子节点:直接删除;一个子节点:用子节点替代;两个子节点:找合适的结点替代。时间复杂度:O(h)
,h
为树高。
二叉搜索树在平均情况下性能很好O(log n)
,但最坏情况下会退化为O(n)
。
1.3 基本框架搭建
1.3.1 结点结构的定义
template<class K>
struct BSTNode
{K _key;BSTNode<K>* _left;BSTNode<K>* _right;BSTNode(const K& key):_key(key),_left(nullptr),_right(nullptr){ }
};
1.3.2 二叉搜索树的类框架
template<class K>
class BSTree
{typedef BSTNode<K> Node;
public:
private:Node* _root = nullptr;
};
二、二叉搜索树的模拟实现
2.1 插入 insert
插入过程分析:树为空,则直接新增结点,赋值给_root
指针;树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点;如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点,但要保持方向统一。
我们在实现的时候不允许插入相等的值。
bool insert(const K& key)
{if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr; //刚开始根节点的父结点为空Node* cur = _root;while (cur){parent = cur; //记录父节点if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else return false;}//此时cur为nullptrcur = new Node(key);if (key > parent->_key) parent->_right = cur;else parent->_left = cur;return true;
}
代码测试:
BSTree<int> st;
st.insert(8);
st.insert(3);
st.insert(10);
st.insert(1);
st.insert(6);
测试结果:
如上图,符合插入规则。
2.2 中序遍历 InOrder
中序遍历就比较简单了,可以使用递归实现。但是这里有一个问题,中序遍历需要我们传递根节点,但是根节点是私有的,我们不能传递。
解决方案可以在类中实现一个子函数,调用中序遍历传递根节点。
代码实现:
void _InOrder(){InOrder(_root);cout << endl;}private:void InOrder(Node* root){if (root == nullptr){return;}InOrder(root->_left);cout << root->_key << " ";InOrder(root->_right);}
代码测试:
void test1()
{BSTree<int> st;st.insert(8);st.insert(3);st.insert(10);st.insert(1);st.insert(1);st.insert(6);st._InOrder();
}
测试结果:
实现了这么多,也可以发现二叉搜索树的效率是不可控的,一旦插入的结点是有序或者接近有序时,树的结构就会接近一个单链表,这就导致查找等时间复杂度接近达到O(n)
。
2.3 查找操作 Find
查找操作就十分简单了,一次遍历比较一遍就好了。单独一个key
结构返回true
或者false
就可以了。
bool Find(const K& key)
{Node* cur = _root;while (cur){if (key > cur->_key)cur = cur->_right;else if (key < cur->_key)cur = cur->_left;else return true;}return false;
}
2.4 删除操作
修改操作在单个key
结构中通常是不被允许的,因为一旦修改,这棵树就极有可能不再是二叉搜索树了。
二叉搜索树的删除操作分为三种情况,第一种就是当前结点没有子节点,此时直接删除就可以;第二种就是有一个子节点,此时需要用子节点替换要删除的结点;第三种就是有两个子节点,此时可以用左子树的最大结点或者右子树的最小节点替换要删除的结点。
前两种都好理解,我们来看第三种:
我们在实现的时候选择右子树中的最小结点代替,来解决这种情况。
代码实现:
bool Erase(const K& key)
{Node* cur = _root;Node* parent = nullptr;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{//找到了,准备删除if (cur->_left == nullptr) //使用cur的右节点代替cur,包含左右子树为空的情况{if (cur != _root){// 左为空,父结点指向cur的右if (cur == parent->_left){parent->_left = cur->_right;}else parent->_right = cur->_right;}else _root = cur->_right; //更换根节点delete cur;}else if (cur->_right == nullptr){if (cur != _root){// 右为空,父结点指向cur的左if (cur == parent->_left){parent->_left = cur->_left;}else parent->_right = cur->_left;}else _root = cur->_left; //更换根节点delete cur;}else //左右子树都不为空{//找到右子树中的最小值代替curNode* minRightParent = cur;Node* minRight = cur->_right;while (minRight->_left){minRightParent = minRight;minRight = minRight->_left;}//此时minRight为cur右子树中的最小结点//此时直接交换cur和minRight中的值_key,//再删除minRightswap(cur->_key, minRight->_key);//处理minRight可能存在的右子树if (minRight == minRightParent->_left)minRightParent->_left = minRight->_right;else minRightParent->_right = minRight->_right;delete minRight;}return true;}}return false;
}
注意:当要删除的cur
有两个子节点时,不能直接删除cur
结点,这样会导致树的结构断裂。只能用替换法删除。
代码测试:
void test1()
{BSTree<int> st;st.insert(8);st.insert(3);st.insert(10);st.insert(1);st.insert(1);st.insert(6);st._InOrder();st.Erase(3);st._InOrder();st.Erase(10);st._InOrder();st.Erase(1);st._InOrder();st.Erase(6);st._InOrder();
}
测试结果:
2.5 插入的递归写法
bool InsertR(const K& key){return _insert(_root, key);}private:bool _insert(Node*& root, const K& key){if (root == nullptr){Node* newNode = new Node(key);root = newNode;return true;}if (key > root->_key)return _insert(root->_right, key);else if (key < root->_key)return _insert(root->_left, key);else return false;}
注意:这里如果_insert
的函数参数是Node*
,那么就会发生值传递,最终插入操作完成后_root
还是会为空。
三、<key, value> 的应用场景
场景:需要快速查找、插入、删除键值对。实际应用:编程语言中的std::map、std::unordered_map
。
在<key,value>
结构中每一个关键码key
,都有与之对应的值value
,value
可以是任意类型对象。树的结构中(结点)除了需要存储key
还要存储对应的value
,增/删/查
还是以key
为关键字执行二叉搜索树的规则进行比较,可以快速查找到key
对应的value
。key/value
的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key
,修改key
破坏搜索树性质了,可以修改value
。
代码实现:
namespace key_value
{template<class K, class V>struct BSTNode{K _key;V _val;BSTNode<K, V>* _left;BSTNode<K, V>* _right;BSTNode(const K& key, const V& val):_key(key),_val(val), _left(nullptr), _right(nullptr){}};template<class K, class V>class BSTree{typedef BSTNode<K, V> Node;public:~BSTree() //析构函数{_Destroy(_root);}bool insert(const K& key, const V& val){if (_root == nullptr){_root = new Node(key, val);return true;}Node* parent = nullptr; //刚开始根节点的父结点为空Node* cur = _root;while (cur){parent = cur; //记录父节点if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else return false;}//此时cur为nullptrcur = new Node(key, val);if (key > parent->_key) parent->_right = cur;else parent->_left = cur;return true;}bool Erase(const K& key){Node* cur = _root;Node* parent = nullptr;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{//找到了,准备删除if (cur->_left == nullptr) //使用cur的右节点代替cur,包含左右子树为空的情况{if (cur != _root){// 左为空,父结点指向cur的右if (cur == parent->_left){parent->_left = cur->_right;}else parent->_right = cur->_right;}else _root = cur->_right; //更换根节点delete cur;}else if (cur->_right == nullptr){if (cur != _root){// 右为空,父结点指向cur的左if (cur == parent->_left){parent->_left = cur->_left;}else parent->_right = cur->_left;}else _root = cur->_left; //更换根节点delete cur;}else //左右子树都不为空{//找到右子树中的最小值代替curNode* minRightParent = cur;Node* minRight = cur->_right;while (minRight->_left){minRightParent = minRight;minRight = minRight->_left;}//此时minRight为cur右子树中的最小结点//此时直接交换cur和minRight中的值_key,//再删除minRightswap(cur->_key, minRight->_key);swap(cur->_val, minRight->_val); //同时交换_val//处理minRight可能存在的右子树if (minRight == minRightParent->_left)minRightParent->_left = minRight->_right;elseminRightParent->_right = minRight->_right;delete minRight;}return true;}}return false;}Node* Find(const K& key) //返回结点{Node* cur = _root;while (cur){if (key > cur->_key)cur = cur->_right;else if (key < cur->_key)cur = cur->_left;elsereturn cur;}return nullptr;}void InOrder(){_InOrder(_root);cout << endl;}bool InsertR(const K& key, const V& val){return _insert(_root, key, val);}private:bool _insert(Node*& root, const K& key, const V& val){if (root == nullptr){Node* newNode = new Node(key, val);root = newNode;return true;}if (key > root->_key)return _insert(root->_right, key, val);else if (key < root->_key)return _insert(root->_left, key, val);elsereturn false;}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " " << root->_val << endl;_InOrder(root->_right);}void _Destroy(Node* root){if (root == nullptr){return;}_Destroy(root->_left);_Destroy(root->_right);delete root;}Node* _root = nullptr;};
}
核心改动:对类模板参数增加了value
,查找函数不再只是单纯的查找在不在,而是查找完成后返回结点。进行结点删除操作时,如果要删除的结点有两个子节点,再增加交换val
的操作。插入函数中插入节点增加了val
值。
插入测试:
void test2()
{key_value::BSTree<string, string> s;s.insert("conceal", "隐藏");s.insert("claim", "声称");s.insert("fantastic", "极好的");s.insert("forum", "论坛");s.insert("effective", "有效的");s.InOrder();
}
删除测试:
void test3()
{key_value::BSTree<int, int> s;int a[] = { 7,12,3,6,9,2,15,23,21,35,21,11,14,13 };for (auto& e : a){s.insert(e, e);}s.Erase(21);s.Erase(9);s.Erase(7);s.Erase(3);s.InOrder();
}
查找测试:
void test4()
{key_value::BSTree<int, int> s;int a[] = { 7,12,3,6,9,2,15,23,21,35,21,11,14,13 };for (auto& e : a){s.insert(e, e);}auto t = s.Find(21);if (t) cout << t->_val << endl;else cout << "不存在该键值" << endl;auto b = s.Find(16);if (b) cout << b->_val << endl;else cout << "不存在该键值" << endl;
}
3.1 应用场景
例如统计次数:
void test5()
{char c[] = { 'a','b','g','e','w','a','b','c','a','a','b','g' };key_value::BSTree<char, int> s;for (auto& ch : c){auto cur = s.Find(ch);if (!cur) s.insert(ch, 1);else cur->_val++;}s.InOrder();
}
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~