目录
概念
性能
效率分析
二分缺陷
功能
插入
查找
删除
实现
应用
概念
二叉搜索树(又称:二叉排序树),是由一些具有特别性质的二叉树衍变而来。
只要一棵二叉树具备以下性质,即可称作二叉搜索树。
【1】若左子树不为空,左子树上所有结点的值都小于等于根结点的值
【2】若右子树不为空,右子树上所有结点的值都大于等于根结点的值
【3】根的左右子树也都为二叉搜索树
⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体问题具体分析。
C++的STL中的map\set\mutimap\mutiset的底层就是二叉搜索树,muti-支持重复。
性能
就二叉搜索树的性质来看,
一系列值以根节点为中心,比根小的放左边,比根大的放右边,各自分散到根两侧的左右子树。
似乎能品出一丝二分查找的味道?
二分查找也是从中间值开始,分为左右两个区间,小数往左区间找,大数往右区间找。
有些异曲同工之妙。
效率分析
正是本质相同,二叉搜索树与二分查找在查找的功能上可以进行类比,
但二叉搜索树的查找的时间复杂度可以直接看作二分查找的O(log2 N)吗?
最优情况下,二叉搜索树为完全二叉树(或接近完全二叉树)时,其高度为log2 N,时间复杂度可看作O(log2 N)
最坏情况下,二叉搜索树退化为单支树(或接近单支)时,高度为N,时间复杂度就是O(N)了
综合来看,二叉搜索树受结构影响,查找的时间复杂度是O(N)。
可见二叉搜索树如此的效率并不总能达到我们的需求,
所以后续平衡二叉树AVL树和红黑树就是在二叉搜索树的基础上进行变形调整,尽量将二叉搜索树平衡为完全二叉树结构,以保证效率。(后续展开学习)
二分缺陷
那么假如不知道红黑树和AVL树的,稳定log2 N的二分查找是否就一定优于二叉搜索树呢?
二分查找的缺陷:
【1】存储值的容器支持随机访问,且值必须有序。
【2】查找后要进行修改,插入、删除的效率低下。因为存在支持随机访问的容器中(vector、deque等),插入删除一般就要挪动数据。
这也就体现了二叉搜索树的进阶--平衡二叉树的价值。
功能
插入
二叉搜索树要插入数据,只要根据其特性进行即可。
具体过程:
树空,第一个插入的结点就是根结点。
树不空,则每次进行比较,插入值小于当前结点,往左走;插入值大于当前结点,往右走。直到找到空位置,插入新结点。
如果要插入相等值,则改动比较逻辑,小于等于往左走,大于往右走(反之也可)。自行抉择相等值放左树还是右数,可不能既要又要。
bool insert(const Key& x)
{Node* node = new Node(x);Node* cur = _root;Node* parent = cur; //插入新结点,得知道往哪里插if (_root == nullptr){_root = node;return true;}while (cur){if (x > cur->_key){parent = cur;cur = cur->right;}else if (x < cur->_key){parent = cur;cur = cur->left;}else{return false;}}if (x > parent->_key) //知道插给谁,还要知道 插左or插右{parent->right = node;}else if (x < parent->_key){parent->left = node;}return true;
}
查找
1.查找x,从根开始⽐较,x⽐根值⼤则走右边继续找,x⽐根值⼩则走左边继续找。
2. 最多查找⾼度次,⾛到空,还没找到,这个值不存在。
3. 如果不⽀持插⼊相等的值,找到x即可返回。
4. 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。
Node* find(const Key& x)
{Node* cur = _root;while (cur){if (x > cur->_key){cur = cur->right;}else if (x < cur->_key){cur = cur->left;}else{return cur;}}return nullptr;
}
删除
要删除x,首先要找到x,查找逻辑同上。
关键在于怎么删。
由于我们有单链表的基础,应该知道仅仅找到被删结点是不够的,还需要有被删结点的Parent结点,才能进行结点删除的操作。所以要定义两个指针cur和parent.
删除结点时会碰到4种情况:(假设被删结点为N)
【1】N为叶子结点,左右为空;
【2】N的左为空,右不为空;
【3】N的左不为空,右为空;
【4】N的左右不为空。
删除方案:
【1】把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)
【2】把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点
【3】把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点
【4】⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。
找到N左子树的最大值R,或者N右子树的最小值R来替代N。二选一,都能满足结构特性。
替代N的意思就是将N和R的两个结点的值交换,转⽽变成删除R结点,R结点符合情况2或情况3,可以直接删除。
代码部分请看实现
实现
template<class Key>
struct BSTree_Node
{BSTree_Node(const Key& x = Key()):left(nullptr),right(nullptr),_key(x){}BSTree_Node* left;BSTree_Node* right;Key _key;
};template<class Key>
class BSTree
{typedef BSTree_Node<Key> Node;public:BSTree() = default; //生成默认构造bool insert(const Key& x){Node* node = new Node(x);Node* cur = _root;Node* parent = cur;if (_root == nullptr){_root = node;return true;}while (cur){if (x > cur->_key){parent = cur;cur = cur->right;}else if (x < cur->_key){parent = cur;cur = cur->left;}else{return false;}}if (x > parent->_key){parent->right = node;}else if (x < parent->_key){parent->left = node;}return true;}Node* find(const Key& x){Node* cur = _root;while (cur){if (x > cur->_key){cur = cur->right;}else if (x < cur->_key){cur = cur->left;}else{return cur;}}return nullptr;}bool erase(const Key& x){Node* cur = _root;Node* parent = nullptr;while (cur){if (x < cur->_key){parent = cur;cur = cur->left;}else if (x > cur->_key){parent = cur;cur = cur->right;}else //左右不为空,最难部分,需要好好理清楚{if (cur->left == nullptr){if (parent == nullptr) //根的父亲为nullptr{_root = cur->right;}else{//if (parent->_left == cur)这样也行if (cur->_key < parent->_key){parent->left = cur->right;}else{parent->right = cur->right;}}delete cur;return true;}else if (cur->right == nullptr){if (parent == nullptr) //根的父亲为nullptr{_root = cur->left;}else{//if (parent->_left == cur)这样也行if (cur->_key < parent->_key){parent->left = cur->left;}else{parent->right = cur->left;}}delete cur;return true;}else{Node* rightMin_Parent = cur;Node* rightMin = cur->right;while (rightMin->left){rightMin_Parent = rightMin;rightMin = rightMin->left;}swap(cur->_key, rightMin->_key);if (rightMin_Parent->left == rightMin){rightMin_Parent->left = rightMin->right;}else{rightMin_Parent->right = rightMin->right;}delete rightMin;return true;}}}return false;}void Inorder(){inorder(_root);cout << endl;}private:void inorder(Node* root){if (root == nullptr)return;inorder(root->left);cout << root->_key << " ";inorder(root->right);}private:Node* _root = nullptr;
};
应用
key搜索场景
只有key作为关键码,结构中只要存key即可,关键码即为需要搜索到的值,搜索场景只需判断key在不在。key的搜索场景实现的⼆叉树搜索树⽀持增删查,但是不⽀持修改,修改key就破坏搜索树结构了。
【1】小区大门车辆栏杆。扫描车牌,检索是否是小区的车,找到了则抬杠放行,反之不抬杆。
【2】检查一篇英文文章单词拼写是否正确,将词库放入搜索树,读取文章中的单词进行搜索。
key/value搜索场景:
key搜索本身的应用不够广,但如果我们在结点中增添一个值,就有很大的操作空间了。
key存搜索数据,val存该数据的一些附加信息。
通过key可快速找到其对应的value。
template<class Key, class Val>
struct BSTree_Node
{BSTree_Node(const Key& x = Key(),const Val& y = Val()):left(nullptr),right(nullptr),_key(x),_val(y){}BSTree_Node* left;BSTree_Node* right;Key _key;Val _val;
};
【1】简单中英互译词典。key存英文,val存中文。搜索英文,将val存的中文一起显示即可。
【2】商场无人停车场\车库。key存车牌,val存入场时间。出场时即可通过车牌找到入场时间,结合退场时间从而算出停车时长,计算停车费用,缴费后抬杆放行。
【3】统计英文文章词频。读取一个词,查找是否存在。
不存在说明第一次出现设val为1,存在val++。