目录
[Part1. 二叉搜索树核心定义](#Part1. 二叉搜索树核心定义)
[Part1.1. 查找效率特性](#Part1.1. 查找效率特性)
[Part1.2. 不同查找容器对比](#Part1.2. 不同查找容器对比)
[Part1.3. 重复键值两种处理策略](#Part1.3. 重复键值两种处理策略)
[Part2. 二叉搜索树四大核心操作](#Part2. 二叉搜索树四大核心操作)
[Part2.1. 节点插入](#Part2.1. 节点插入)
[Part2.1. 中序遍历特性](#Part2.1. 中序遍历特性)
[Part2.2. 节点删除](#Part2.2. 节点删除)
[Part3. key/value二叉搜索树](#Part3. key/value二叉搜索树)
[Part4. 结语](#Part4. 结语)
前言
二叉搜索树作为二叉树的特化升级版,在查找上有着很大的优势,但同时也有着不小的问题,接下来让我们来看看吧。
let's go!!!!!!!!!
Part1. 二叉搜索树核心定义
二叉搜索树(Binary Search Tree,简称 BST)是满足特殊排序规则的二叉树:
规则<1>:节点左子树所有节点值 ≤ 当前根节点值。
规则<2>:节点右子树所有节点值 ≥ 当前根节点值。
规则<3>:左右子树本身也必须是二叉搜索树。
Part1.1. 查找效率特性
=》理想平衡 BST:查找次数等于树的高度 h,时间复杂度 O(logN)
=》极端不平衡 BST(链式退化):最坏退化成单链表,查找复杂度劣化为 O(N)。
优化方案:平衡二叉搜索树(AVL树、红黑树,(C++ STL 的 map/set 底层为红黑树)),多叉平衡树 B 树(企业级通常会有一千多个分支)。
Part1.2. 不同查找容器对比
除 BST 外,常用查找容器:
1. 哈希表:查询速度极快,O(1)级别。
2. 有序数组+二分查找:O(logN)查找,插入、删除操作效率极低,不适合频繁增删场景。
Part1.3. 重复键值两种处理策略
二叉搜索树对相同数值节点分两种实现方案,对应 STL 容器:
1. 去重模式:不允许插入重复值,对应 std::set / std::map
2. 允许重复:支持存储多个相同 key,对应 std::multiset / std::multimap
Part2. 二叉搜索树四大核心操作
完整实现需要封装四大接口:插入、查找、遍历、删除
Part2.1. 节点插入
节点的插入遵循二叉搜索树的规则来插入就好,接下来来看看代码吧:
cppbool Insert(const K& key) { if (_root == nullptr)//当根节点为空时 创建根节点 { _root = new Node(key,value); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else//去重操作 { return false; } } cur = new Node(key); if (parent->_key > key) { parent->_left = cur; } else { parent->_right = cur; } return true; }
Part2.1. 中序遍历特性
二叉搜索树中序遍历(左→根→右),结果严格升序;
升序顺序完全取决于插入数据的大小关系,与插入先后顺序无关。
cppvoid InOrder()const//由于递归需要根节点 但是由于封装又不能暴露 于是就搞一个中介函数来作为其访问根节点的媒介 { _InOrder(_root); std::cout << std::endl; } void _InOrder(Node* root)const { if (root == nullptr) { return; } _InOrder(root->_left); std::cout << root->_key<<" "; _InOrder(root->_right); }
Part2.2. 节点删除
删除是 BST 最难实现的接口,按待删除节点的子树存在情况分为 4 类,分别是:
<1>. 左右子树均为空。 <2>. 只有右子树,左子树为空。
<3>. 只有左子树,右子树为空。 <4>. 左右子树均不为空。
**p.s.**其中<1>可以归属与<2>或者<3>,就当成其有一个为nullptr的左或者右节点就好了。
对于<2>,我们使用parent指向待删除结点的父亲节点,cur则指向待删除结点,我们先判断cur是parent的左子还是右子,在让parent连接cur的右节点。(<3>同理)。
对于<4>,我们有两种方案(替换法):
=》方案A:取左子树最大值(左树最右节点),替换待删节点的值,再删除该最大值节点
=》方案B:取右子树最小值(右树最左节点),替换待删节点的值,再删除该最小值节点
我们来看看代码,同时看看其中的细节吧:
cppbool erase(const K& key) { Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > key)//查找 { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else { if (cur->_left == nullptr)//无左节点场景 { if (cur == _root) { _root = cur->_right; } else { if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } else if (cur->_right == nullptr)//无右节点场景 { if (cur == _root) { _root = cur->_left; } else { if (parent->_left == cur) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } } delete cur; } else { Node* repalce = cur->_right; Node* repalce_parent = cur;//将这个设为cur为根节点的场景 while (repalce->_left != nullptr) { repalce_parent = repalce; repalce = repalce->_left; } cur->_key = repalce->_key;//替代 if (repalce_parent->_left == repalce)//看看这个是左还是右 在连接 { repalce_parent->_left = repalce->_right; } else { repalce_parent->_right = repalce->_right; } delete repalce; } return true; } } return false; }
Part3. key/value二叉搜索树
普通 BST 只存储单个数值,对应 set;而 map 采用 key-value 模型,适配业务中「唯一标识+附属数据」场景:
1. key:唯一排序标识
树的排序、查找、去重全部基于 key,key 不可修改,作为数据唯一区分依据。2. value:附属业务数据
可自由修改,仅作为 key 附带的存储内容,不参与树的排序逻辑。这个的实现与上述相似,只是多了一个value数值,我们来看看代码吧:
cppnamespace Key_Value { template<class K,class V> struct BSTNode { K _key; V _value; BSTNode<K,V>* _left; BSTNode<K,V>* _right; BSTNode() :_key(K()) ,_value(V()) , _left(nullptr) , _right(nullptr) { } BSTNode(const K& key,const V& value) :_key(key) , _value(value) , _left(nullptr) , _right(nullptr) { } }; template<class K,class V> class BSTree { public: using Node = BSTNode<K,V>; BSTree() = default; ~BSTree() { Destroy(_root); _root = nullptr; } BSTree(const BSTree& Tree) { _root = Copy(Tree._root); } BSTree& operator=(BSTree tem) { std::swap(_root, tem._root); return *this; } bool Insert(const K& key,const V& value) { if (_root == nullptr) { _root = new Node(key,value); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else { return false; } } cur = new Node(key,value); if (parent->_key > key) { parent->_left = cur; } else { parent->_right = cur; } return true; } bool Find(const K& key)const { Node* cur = _root; while (cur) { if (cur->_key > key) { cur = cur->_left; } else if (cur->_key < key) { cur = cur->_right; } else { return true; } } return false; } void InOrder()const { _InOrder(_root); std::cout << std::endl; } bool erase(const K& key) { Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key < key) { parent = cur; cur = cur->_right; } else { if (cur->_left == nullptr) { if (cur == _root) { _root = cur->_right; } else { if (parent->_left == cur) { parent->_left = cur->_right; } else { parent->_right = cur->_right; } } delete cur; } else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; } else { if (parent->_left == cur) { parent->_left = cur->_left; } else { parent->_right = cur->_left; } } delete cur; } else { Node* repalce = cur->_right; Node* repalce_parent = cur; while (repalce->_left != nullptr) { repalce_parent = repalce; repalce = repalce->_left; } cur->_key = repalce->_key; if (repalce_parent->_left == repalce) { repalce_parent->_left = repalce->_right; } else { repalce_parent->_right = repalce->_right; } delete repalce; } return true; } } return false; } private: void _InOrder(Node* root)const { if (root == nullptr) { return; } _InOrder(root->_left); std::cout << root->_key << ":"<<root->_value<<" "; _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; } Node* new_node = new Node(root->_key); Copy(root->_left); Copy(root->_right); return new_node; } Node* _root = nullptr; }; }
Part4. 结语
二叉搜索树作为未来学习AVL数、红黑树的基石,了解它的删除、插入等的逻辑还是非常有必要的。
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~
