【探寻C++之旅】第九章:二叉搜索树
请君浏览
-
- 前言
- 1. 二叉搜索树的概念
- 2. 二叉搜索树的作用
- 3. 二叉搜索树的使用及相应的代码实现
-
- 3.1 二叉搜索树的插入
- 3.2 二叉搜索树的查找
- 3.3 二叉搜索树的删除
- 4. ⼆叉搜索树key和key/value使⽤场景
-
- 4.1 key搜索场景
- 4.2 key/value搜索场景
- 尾声
前言
今天,我们继续踏入追寻C++的冒险历程。上一章我们简单介绍并且了解了C++三大特性之一的多态,至此关于C++的三大特性已经介绍完了。那么本章将为大家讲解一种特殊的数据结构——二叉搜索树,以便为后面章节的学习打下基础。下面让我们一起来进入二叉搜索树的学习。
1. 二叉搜索树的概念
二叉搜索树是一棵特殊的二叉树,从名字上我们可以看出这颗特殊的二叉树的特殊点就在于搜索二字。接下来我们先了解一下什么是二叉搜索树,再看一看二叉搜索有什么作用。
⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:
- 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值
- 若它的右⼦树不为空,则右⼦树上所有结点的值都⼤于等于根结点的值
- 它的左右⼦树也分别为⼆叉搜索树
下面我们来看一颗颗二叉搜索树:
在上面这棵二叉搜索树中我们可以观察到,无论对于哪个结点,它左子树中的所有值都是小于该结点的值,右子树中的所有值都是大于该结点的值。在⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义,后续我们学习map/set/multimap/multiset
系列容器底层就是⼆叉搜索树,其中map/set
不⽀持插⼊相等值,multimap/multiset⽀持插⼊相等值
2. 二叉搜索树的作用
那么二叉搜索树有什么作用呢?第一个自然是搜索,当我们在一颗二叉搜索树中去寻找某个值时,它的时间复杂度只与这棵树的高度有关,这是因为当我们需要查找的值如果小于当前结点,那么只需要去它的左子树中去找,反之只需要去它的右子树中寻找。我们来具体的分析一下:
- 最好的情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为 log2N
- 最坏的情况下,二叉搜索树退化为单支树(或者类似于单支),其高度为 N;
所以根据时间复杂度的计算要求而言,二叉搜索树的查找时间复杂度为 O(N);那么这样的效率自然是无法满足我们的需求,毕竟这跟一遍遍历没有什么区别,但是这是在二叉搜索树的高度过高的情况下,那么为了避免二叉搜索树的高度过高,能使它接近于完全二叉树的形态,就有了一种基于二叉搜索树,对其进行变形所形成的一种新的数据结构——AVL树(平衡二叉搜索树)和红黑树。它们解决了二叉搜索树有可能高度过高的缺陷,适⽤于我们在内存中存储和搜索数据。 我们现在所讲的二叉搜索树就是为后面学习AVL树和红黑树以及set和map这两个容器打下基础。
说到查找,相信大家第一个想到的快速查找的方法就是二分查找,虽然二分查找也能实现**O(log2N)**级别的查找效率,但是二分查找有两大缺陷:
- 需要存储在⽀持下标随机访问的结构中,并且有序。
- 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。
光是有序一条便使得二分查找算法在对无序的元素进行查找时需要先进行排序。这里便体现出了AVL树(平衡二叉搜索树)的价值。
上面介绍了查找,二叉搜索树其实还有着排序的作用,认真观察上面的二叉搜索树不难发现,当我们对这棵树进行中序遍历时,便是得到了一个升序的排列。
下面我们先来了解二叉搜索树的增删查操作,看一下它的原理,为后面的学习打下基础。至于为什么没有改这个操作是因为搜索二叉树对于每个结点的值都是有要求的,不能随意更改,不然就会破坏二叉搜索树的性质。
3. 二叉搜索树的使用及相应的代码实现
3.1 二叉搜索树的插入
插⼊的具体过程如下:
- 树为空,则直接新增结点,赋值给
root
指针。 - 树不为空,按⼆叉搜索树的性质,插⼊值⽐当前结点⼤往右⾛,插⼊值⽐当前结点⼩往左⾛,直到找到空位置,插⼊新结点。
- 如果⽀持插⼊相等的值,插⼊值跟当前结点相等的值可以往右⾛,也可以往左⾛(保持逻辑一致性,确定往哪边走后便不要再更改),找到空位置,插⼊新结点。
下面我们通过两张张图来感受一下二叉搜索树的插入:
由于要把结点插入,我们需要用父节点指向需要插入的结点,因此在设计代码时需要注意要用一个变量去保存父节点的值。下面我们来看一下如何自己用代码去实现(不允许插入相同值):
#includeusing namespace std;template<class K>struct BSTreeNode{K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;BSTreeNode(K key):_key(K),_left(nullptr),_right(nullptr){}};template<class K>class BSTree{typedef BSTreeNode<K> Node;public: //...bool insert(const K& key) { if (_root == nullptr) { _root = new Node(key); return true; } Node* parent = _root; Node* cur = _root; while (cur) { if (key > cur->_key) { parent = cur; cur = cur->_right; } else if(key < cur->_key) { parent = cur; cur = cur->_left; } else { return false; } } if (key > parent->_key) { parent->_right = new Node(key); } else { parent->_left = new Node(key); } return true;} //...private:Node* _root = nullptr;};
3.2 二叉搜索树的查找
二叉搜索树的查找原理非常简单:从根开始⽐较,例如查找x,x⽐根的值⼤则往右边⾛查找,x⽐根值⼩则往左边⾛查找。 最多查找⾼度次,若是⾛到空位置,还没找到,则证明这个值不存在。
需要特别注意的是:
- 如果不⽀持插⼊相等的值,找到x即可返回。
- 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序遍历时的第⼀个x。也就是说在我们进行查找时如果找到了第一个目标值,程序继续进行,继续往下查找(此时查找的方向取决于我们在进行插入时对相等元素插入的方向一致),最后返回离查找到空位置最近的x。如下图,查找3,要找到1的右孩⼦的那个3返回
下面我们来看一下如何自己用代码去实现(不允许插入相同值):
#includeusing namespace std;template<class K>struct BSTreeNode{K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;BSTreeNode(K key):_key(K),_left(nullptr),_right(nullptr){}};template<class K>class BSTree{typedef BSTreeNode<K> Node;public: //... 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; } //...private:Node* _root = nullptr;};
3.3 二叉搜索树的删除
要在二叉搜索树中删除元素,⾸先查找元素是否在⼆叉搜索树中,如果不存在,则返回false。其次,还要确保删除该元素后不破坏二叉搜索树的性质。如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
- 要删除结点N左右结点均为空
- 要删除的结点N左结点为空,右结点不为空
- 要删除的结点N右结点为空,左结点不为空
- 要删除的结点N左右孩⼦结点均不为空
对应以上四种情况的解决⽅案:
- 左右都为NULL:把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)
- 左为NULL:把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点
- 右为NULL:把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点
- 左右都不为NULL(重点):⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替换法删除。找N左⼦树的值最⼤结点R(最右结点)或者N右⼦树的值最⼩结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满⾜⼆叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转⽽变成删除R结点,R结点符合情况2或情况3,可以直接删除。
下面我们来看一下如何自己用代码去实现(不允许插入相同值):
#includeusing namespace std;template<class K>struct BSTreeNode{K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;BSTreeNode(K key):_key(K),_left(nullptr),_right(nullptr){}};template<class K>class BSTree{typedef BSTreeNode<K> Node;public: //... bool erase(const K& key){if (_root == nullptr){return false;}Node* parent = _root;Node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{ //0-1个孩⼦的情况,删除情况1 2 3均可以直接删除,改变⽗亲对应孩⼦指针指向即可if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (key > parent->_key){parent->_right = cur->_right;}else{parent->left = cur->_right;}}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (key > parent->_key){parent->_right = cur->_left;}else{parent->left = cur->_left;}}delete cur;return true;} //2个孩⼦的情况删除情况4,替换法删除 //假设这⾥我们取左⼦树的最大结点作为替代结点去删除else{Node* maxleft = cur->left;Node* maxleftParent = cur;while (maxleft->_right){maxleftParent = maxleft;maxleft = maxleft->_right;}cur->_key = maxleft->_key;if (key > maxleftParent->_key){maxleftParent->_right = maxleft->_left;}else{maxleftParent->_left = maxleft->_left;}}return true;}return false;}} //...private:Node* _root = nullptr;};
4. ⼆叉搜索树key和key/value使⽤场景
4.1 key搜索场景
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的⼆叉树搜索树⽀持增删查,但是不⽀持修改,修改key破坏搜索树结构了。
- 场景1:⼩区⽆⼈值守⻋库,⼩区⻋库买了⻋位的业主⻋才能进⼩区,那么物业会把买了⻋位的业主的⻋牌号录⼊后台系统,⻋辆进⼊时扫描⻋牌在不在系统中,在则抬杆,不在则提⽰⾮本⼩区⻋辆,⽆法进⼊。
- 场景2:检查⼀篇英⽂⽂章单词拼写是否正确,将词库中所有单词放⼊⼆叉搜索树,读取⽂章中的单词,查找是否在⼆叉搜索树中,不在则波浪线标红提⽰。
4.2 key/value搜索场景
每⼀个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字⾛⼆叉搜索树的规则进⾏⽐较,可以快速查找到key对应的value。key/value的搜索场景实现的⼆叉树搜索树⽀持修改,但是不⽀持修改key,修改key破坏搜索树性质了,可以修改value。
- 场景1:简单中英互译字典,树的结构中(结点)存储key(英⽂)和vlaue(中⽂),搜索时输⼊英⽂,则同时查找到了英⽂对应的中⽂。
- 场景2:商场⽆⼈值守⻋库,⼊⼝进场时扫描⻋牌,记录⻋牌和⼊场时间,出⼝离场时,扫描⻋牌,查找⼊场时间,⽤当前时间-⼊场时间计算出停⻋时⻓,计算出停⻋费⽤,缴费后抬杆,⻋辆离场。
- 场景3:统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。
⼆叉搜索树key和key/value使⽤场景分别对应着两种容器:set和map ,也就是我们下一章所要讲解的内容,本章讲解二叉搜索树便是为了后面的章节做一个铺垫。
上面我们所讲的代码都是key二叉搜索树,下面是key/value二叉搜索树的代码,感兴趣的可以了解一下:
template<class K, class V> struct BSTNode { // pair _kv; K _key; V _value; BSTNode<K, V>* _left; BSTNode<K, V>* _right; BSTNode(const K& key, const V& value) :_key(key) , _value(value) , _left(nullptr) , _right(nullptr) {} };template<class K, class V>class BSTree{typedef BSTNode<K, V> Node;public: BSTree() = default; BSTree(const BSTree<K, V>& t) { _root = Copy(t._root); } BSTree<K, V>& operator=(BSTree<K, V> t) { swap(_root, t._root); return *this; } ~BSTree() { Destroy(_root); _root = nullptr; } 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->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(key, value); if (parent->_key < key) { parent->_right = cur; } else { parent->_left = cur; } return true; } Node* Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_key < key) { cur = cur->_right; } else if (cur->_key > key) { cur = cur->_left; } else { return cur; } } return nullptr; } bool Erase(const K& key) { Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else { if (cur->_left == nullptr) { if (parent == nullptr) { _root = cur->_right; } else { if (parent->_left == cur) parent->_left = cur->_right; else parent->_right = cur->_right; } delete cur; return true; } else if (cur->_right == nullptr) { if (parent == nullptr) { _root = cur->_left; } else { if (parent->_left == cur) parent->_left = cur->_left; else parent->_right = cur->_left; } delete cur; return true; } else { Node* rightMinP = cur; Node* rightMin = cur->_right; while (rightMin->_left) { rightMinP = rightMin; rightMin = rightMin->_left; } c ur->_key = rightMin->_key; if (rightMinP->_left == rightMin) rightMinP->_left = rightMin->_right; else rightMinP->_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 << \":\" << root->_value << endl; _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 nullptr; Node* newRoot = new Node(root->_key, root->_value); newRoot->_left = Copy(root->_left); newRoot->_right = Copy(root->_right); return newRoot; }Node* _root = nullptr;}
尾声
若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!