多态讲完了,我们来讲点轻松的(也许)。
我们之前讲过二叉树,而二叉树中,又有一种特殊的树称之为二叉搜索树。
一、二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
例如下图,左边的是一颗二叉搜索树,右边不是:
为什么这样的树称作二叉搜索树呢?因为这树天生就十分适合搜索。我们可以这么想使用左边的树,如果我们想找到22的话,首先22比25小,所以一定在左子树,而22大于20,所以一定在右子树。然后我们就可以找到了。我们可以发现,这种树的效率天生就很高,这棵树最多找高度次。也就是说最多找N次(注意不是logN次,因为我们可能碰到下图中右边的树),下图中的两棵树都是二叉搜素树。也就是说时间复杂度为O(N)
普通的二叉搜索树的时间复杂度为O(N),但是我们可以对其进行一些特殊变换,使之成为AVL树,红黑树之类的(之后我们再讲)。这样时间复杂度就可以降为了O(logN)了。
这个树还有另一个名字是二叉排序树,这是因为当我们对这棵树进行中序遍历的时候,结果是升序的。
二、二叉搜索树的实现
1.二叉树的结点定义
对于二叉树的结点,我们类似于list的结点定义一样,使用一个struct类。
template<class K>
struct BSTreeNode
{BSTreeNode(const K& val = K()):_key(val),_left(nullptr),_right(nullptr){}K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;
};
2.二叉搜索树的结构
为了方便我们后续的结点定义,我们将结点给typedef一下。然后我们的成员变量其实也就这一个根节点。只要有根节点就可以构建出二叉搜索树了。
private:typedef BSTreeNode<K> Node;Node* _root;
3.二叉搜索树的构造函数
public:BSTree():_root(nullptr){}
如上代码所示,对于构造函数其实是比较简答的,我们只需要将二叉树的根结点初始化为空即可
4.二叉搜索树的插入
对于插入接口,我们需要考虑的情况有如下两种:
- 二叉树为空的时候,直接new一个结点插入上去即可
- 如果二叉树不为空,那么我们就需要进行比较
例如上面的例子,我们此时插入一个16
16小于25,向左走。
16小于20,向左走。
16大于10,向右走,并插入空
注意:如果恰好等于当前结点的值,那么我们可以认为插入失败了,因为此时已经不满足二叉搜索树的定义了。
代码实现:
我们现在想的方法是对二叉树进行迭代,迭代到空new一个新节点插入,但是如果我们只是对二叉树进行迭代最终我们会发现如此迭代下去,最终的当前结点一定会为空指针,这并非我们所期望的。我们肯定是还需要它的父节点的,这样才能让我目标值插入进去。所以我们还需要一个值来记录当前所迭代位置的父节点。即我们下面写的parent。
有了parent以后,我们现在就是new一个新的结点,然后直接parent链接起来。不过此时又分为两种情形,是左孩子还是右孩子,其实并不好确定。于是我们可以使用parent当前的值与目标值进行比对从而判断出是插入左孩子还是右孩子。
public:bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}else{Node* parent = _root;Node* cur = _root;while (cur != nullptr){if (cur->_key == key){return false;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}cur = new Node(key);if (parent->_key > key){parent->_left = cur;}else if (parent->_key < key){parent->_right = cur;}return true;}}
5.二叉搜索树的中序遍历
插入了以后,我们可以使用中序遍历对其进行输出,由于中序遍历就是排序后的情形,所以我们可以很方便的看到我们当前二叉树的结果。
对于排序,我们最好写一个子函数来实现,因为没有子函数的话,参数不好进行控制,会使得代码十分繁琐。
如下代码所示,对于中序遍历我们其实还是比较容易看懂的。无非就是先左子树,然后根,然后右子树即可。我们最好将子函数放在私有的部分中,因为在类外我们并不需要直接使用这个函数
public:void InOrder(){_InOrder(_root);}
private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}
6.二叉搜索树的查找
对于查找的逻辑,是非常简单的,它与插入也是比较相似的,当我们当前结点的值小于目标的时候,迭代到右子树,当大于目标的时候,迭代到左子树。当相等的时候返回true即可。但若当cur都为nullptr了的时候还没有找到,那就是没有找到,返回false即可
public:bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_key == key){return true;}else if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}}return false;}
7.二叉搜索树的删除
对于二叉搜索树的删除操作,其实是比较困难的。
首先查找元素是否在二叉搜索树中,如果不存在就返回false
如果存在,那么此时要删除的节点可能分为以下四种情况:
- 要删除的节点无子节点
- 要删除的节点只有左子节点
- 要删除的节点只有右子节点
- 要删除的节点有左、右子节点
第一种情况是最好处理的,释放节点,直接把父节点指向改为空。
第二种情况,要删除的节点只有左子节点,我们还是用上面的例子,此时18就是一个只有左子节点的节点
如果我们要删除2,只需要将2的父节点和2的左子节点链接,然后直接删除2即可。
第三种情况,要删除的节点只有右子节点,此时13就是一个只有右子节点的节点
如果我们要删除13,只需要将13的父节点和13的右子节点链接,然后直接删除13即可
总结,第二种和第三种情况的删除方式其实也适用于第一种情况,因为无子节点,也就是左右都为空,此时父节点链接的是一个空指针,并删除目标节点即可。
第四种:要删除的节点有左、右节点,此时23符合要求
我们的目标是,在23的子树中寻找一个值,将23与这个值交换。这种方法叫做替换法。
要寻找的值必须满足交换后二叉搜索树的性质不变,所以我们需要在23的左子树找出一个最大值,或者在23的右子树找出一个最小值。
前面提到过对二叉搜索树中序遍历可以得到一个有序的序列,所以23的子树的最大值,也就是在左子树中做一次中序遍历下的最后一个数,也就是21。简单来说,我们只需要从23向左走一步,然后一直向右走,就可以找到左子树的最大值了
而23的右子树的最小值,也就是在右子树中做一次中序遍历下的第一个数,也就是26。和上面类似的,我们只需要从26向右走一步,然后一直向左走,就可以找到右子树的最小值了。
因此,我们可以将23和21替换,或者将23和26替换
这里就只展示23和21替换的情况,当学会规律后,另一种情况也就学会了。
替换后,还需要把23的父节点和23的子节点链接,这里又有两种情况需要区分,否则导致错误:
- 替换后23的父节点位于23原先的位置
- 替换后23的父节点不位于23原先的位置
可以看出,在这种情况下23的父节点位于23原先的位置,那么就要将此时23的左子节点赋值给父节点的left,然后就可以直接删除23了
我们改造一下这颗二叉搜索树.使父节点不位于原先的位置(另一种情况)
假设我们要删去12(改造后12的左子树的最大值为11)
此时,就需要将12的左子节点赋值给父节点的right,然后直接删除12。
这里是一个易错点,需要注意情况的判断。以上是对二叉搜索树的操作部分。
因此我们引出了两种写法,
以下是先考虑parent与cur这两代人之间的关系所写的代码
public:bool Erase(const K& key){Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key > key){parent = cur;cur = cur->_left;}else if (cur->_key < key){parent = cur;cur = cur->_right;}else{if (parent == nullptr){if (cur->_left == nullptr){_root = cur->_right;delete cur;return true;}else if (cur->_right == nullptr){_root = cur->_left;delete cur;return true;}else{Node* leftMaxParent = cur;Node* leftMax = cur->_left;if (leftMax->_right == nullptr){leftMax->_right = cur->_right;delete cur;_root = leftMax;return true;}while (leftMax->_right){leftMaxParent = leftMax;leftMax = leftMax->_right;}std::swap(leftMax->_key, cur->_key);leftMaxParent->_right = leftMax->_left;delete leftMax;leftMax = nullptr;return true;}}if (parent->_left == cur){if (cur->_left == nullptr){parent->_left = cur->_right;delete cur;return true;}else if (cur->_right == nullptr){parent->_left = cur->_left;delete cur;return true;}else {Node* leftMaxParent = cur;Node* leftMax = cur->_left;if (leftMax->_right == nullptr){leftMax->_right = cur->_right;delete cur;parent->_left = leftMax;return true;}while (leftMax->_right){leftMaxParent = leftMax;leftMax = leftMax->_right;}std::swap(leftMax->_key, cur->_key);leftMaxParent->_right = leftMax->_left;delete leftMax;leftMax = nullptr;return true;}}else{if (cur->_left == nullptr){parent->_right = cur->_right;delete cur;return true;}else if (cur->_right == nullptr){parent->_right = cur->_left;delete cur;return true;}else{Node* leftMaxParent = cur;Node* leftMax = cur->_left;if (leftMax->_right == nullptr){leftMax->_right = cur->_right;delete cur;parent->_right = leftMax;return true;}while (leftMax->_right){leftMaxParent = leftMax;leftMax = leftMax->_right;}std::swap(leftMax->_key, cur->_key);leftMaxParent->_right = leftMax->_left;delete leftMax;leftMax = nullptr;return true;}}}}return false;}
以下是根据先考虑cur与其孩子之间的关系所写的代码
public:bool Erase(const K& key){Node* cur = _root;Node* parent = nullptr;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 (parent == nullptr){_root = cur->_right;}else if (parent->_left == cur){parent->_left = cur->_right;}else if (parent->_right == cur){parent->_right = cur->_right;}}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else if (parent->_left == cur){parent->_left = cur->_left;}else if (parent->_right = cur){parent->_right = cur->_left;}}else{Node* leftMax = cur->_left;Node* leftMaxParent = cur;while (leftMax->_right){leftMaxParent = leftMax;leftMax = leftMax->_right;}std::swap(cur->_key, leftMax->_key);if (leftMaxParent->_left == leftMax){leftMaxParent->_left = leftMax->_left;}else{leftMaxParent->_right = leftMax->_left;}cur = leftMax;}delete cur;return true;}}}
这两种方法都可以完成这个接口,但是代码量有显著的差异。这里推荐先考虑cur和其后代之间的关系。因为替代法这个出现于cur与其后代之间的关系之中。
8.二叉搜索树的查找(递归)
前面,都是讲的非递归,此处为补充(能用迭代还是用迭代去找)
二叉树其实本身还是比较适合递归的。为了可以控制参数,我们要写一个子函数去解决,当根节点为空的时候,直接返回false即可,当前的结点小于要查找的结点,那么去右子树找,当大于时候,去左子树找即可。
public:bool FindR(const K& key){return _FindR(_root, key);}
private:bool _FindR(Node* root, const K& key){if (root == nullptr){return false;}if (root->_key == key){return true;}else if (root->_key > key){return _FindR(root->_left, key);}else{return _FindR(root->_right, key);}}
9.二叉搜索树的插入(递归)
大体思路同上,也是补充而已
public:bool InsertR(const K& key){return _InsertR(_root, key);}
private:bool _InsertR(Node*& root, const K& key){if (root == nullptr){root = new Node(key);return true;}if (root->_key < key){return _InsertR(root->_right, key);}else if (root->_key > key){return _InsertR(root->_left, key);}else{return false;}}
10.二叉搜索树的删除(递归)
这个写的挺妙的
public: bool EraseR(const K& key){return _EraseR(_root, key);}
private:bool _EraseR(Node*& root, const K& key){if (root == nullptr){return false;}if (root->_key < key){return _EraseR(root->_right, key);}else if (root->_key > key){return _EraseR(root->_left, key);}else{Node* del = root;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(leftMax->_key, root->_key);return _EraseR(root->_left, key);}delete del;return true;}}
11.二叉搜索树的销毁
拉欧i据 直接按照后序的方式进行销毁即可。
public:~BSTree(){_Destory(_root);}
private:void _Destory(Node*& root){if (root == nullptr){return;}_Destory(root->_left);_Destory(root->_right);delete root;root == nullptr;}
这里最好传引用,因为这样可以置空root指针
12.拷贝构造函数
拷贝构造函数,我们需要实现一个深拷贝,和之前的二叉树基本一样,我们讲deque时也做过了。
这里我们使用先序的方式进行构造:
public:BSTree(const BSTree<K>& t){_root = Copy(t._root);}
private:Node* Copy(Node* root){if (root == nullptr){return nullptr;}Node* Copyroot = new Node(root->_key);Copyroot->_left = Copy(root->_left);Copyroot->_right = Copy(root->_right);return Copyroot;}
13.运算符重载
这里主要用的还是赋值运算符重载吧,
现代写法启动:
public:BSTree<K>& operator=(BSTree<K> t){std::swap(_root, t._root);return *this;}
14.二叉搜索树的实现
template<class K>
struct BSTreeNode
{BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key;BSTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}
};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* prev = nullptr;Node* cur = _root;while (cur){prev = cur;if (key > cur->_key)cur = cur->_right;else if (key < cur->_key)cur = cur->_left;elsereturn false;}cur = new Node(key);if (key > prev->_key)prev->_right = cur;elseprev->_left = cur;return true;}bool insertR(const K& key) //递归版本{return _insertR(_root, key);}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;elsereturn true;}return false;}bool findR(const K& key) //递归版本{return _findR(_root, key);}bool erase(const K& key) //替换法:把目标替换成左子树的最大值或右子树的最小值{Node* parent = nullptr;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{if (cur->_left == nullptr) //左子树为空{if (cur == _root)_root = cur->_right;else{if (cur == parent->_right)parent->_right = cur->_right;elseparent->_left = cur->_right;}}else if(cur->_right == nullptr) //右子树为空{if (cur == _root)_root = cur->_left;else{if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;}}else //左右子树都不为空{parent = cur;Node* leftmid = cur->_left;while (leftmid->_right){parent = leftmid;leftmid = leftmid->_right;}swap(cur->_key, leftmid->_key);if (parent == cur)parent->_left = leftmid->_left;elseparent->_right = leftmid->_left;cur = leftmid;}delete cur;return true;}}return false;}bool eraseR(const K& key) //递归版本{return _eraseR(_root, key);}void InOrder(){_InOrder(_root);cout << endl;}BSTree() = default;BSTree(const BSTree<K>& t){_root = copy(t._root);}~BSTree(){Destroy(_root);}private:Node* copy(Node* root){if (root == nullptr)return nullptr;Node* newroot = new Node(root->_key);newroot->_left = copy(root->_left);newroot->_right = copy(root->_right);return newroot;}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->_key << " ";_InOrder(root->_right);}bool _findR(Node* root, const K& key){if (root == nullptr)return false;if (root->_key == key)return true;if (key > root->_key)return _findR(root->_right, key);elsereturn _findR(root->_left, key);}bool _insertR(Node*& root, const K& key) //参数中指针一定要用引用,不然链接不上{if (root == nullptr){root = new Node(key);return true;}if (key > root->_key)return _insertR(root->_right, key);else if (key < root->_key)return _insertR(root->_left, key);elsereturn false;}bool _eraseR(Node*& root, const K& key){if (root == nullptr)return false;if (key > root->_key)return _eraseR(root->_right, key);else if (key < root->_key)return _eraseR(root->_left, key);else{if (root->_left == nullptr){Node* tmp = root;root = root->_right; //由于引用,root是父节点子树的别名,所以可以直接修改delete tmp;}else if (root->_right == nullptr){Node* tmp = root;root = root->_left;delete tmp;}else{Node* leftmid = root->_left;while (leftmid->_right)leftmid = leftmid->_right;swap(root->_key, leftmid->_key);return _eraseR(root->_left, key);}return true;}}
private:Node* _root = nullptr;
};
三、二叉搜索树的应用
1)key模型
即只有key作为关键码,只需要在二叉搜索树中存储一个key即可。例如我们将业主的信息作为key,将整个小区所有业主的key存储进二叉搜索树中,如果此时要查询一个人是否是小区的业主,只需要将他的key在二叉搜索树中查找一下就可以知道这个人是否是业主了。后面要学习的set,就是一个key模型
2)key/value模型
每一个关键码key,都有与之对应的值value,也就是要在二叉搜索树中存储<key, value>的键值对。例如字典就是一个key/value模型,英文单词作为key,中文作为value,通过key就可以查找到value。又例如哈希表或者类似的用于统计某个物品出现次数的结构,通过查找该物品就可以快速得到其出现次数,这也是一个key/value模型。后面要学习的map,就是一个key/value模型我们可以将二叉搜索树改造为key/value的结构,让节点中多存储一个value,再进行一些修改即可
template<class K, class V>
struct BSTreeNode
{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _val;BSTreeNode(const K& key, const V& val):_left(nullptr), _right(nullptr), _key(key), _val(val){}
};template<class K, class V>
class BSTree
{typedef BSTreeNode<K, V> Node;
public:bool insert(const K& key, const V& val){if (_root == nullptr){_root = new Node(key, val);return true;}Node* prev = nullptr;Node* cur = _root;while (cur){prev = cur;if (key > cur->_key)cur = cur->_right;else if (key < cur->_key)cur = cur->_left;elsereturn false;}cur = new Node(key, val);if (key > prev->_key)prev->_right = cur;elseprev->_left = cur;return true;}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;}bool erase(const K& key){Node* parent = nullptr;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{if (cur->_left == nullptr){if (cur == _root)_root = cur->_right;else{if (cur == parent->_right)parent->_right = cur->_right;elseparent->_left = cur->_right;}}else if (cur->_right == nullptr){if (cur == _root)_root = cur->_left;else{if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;}}else{parent = cur;Node* leftmid = cur->_left;while (leftmid->_right){parent = leftmid;leftmid = leftmid->_right;}swap(cur->_key, leftmid->_key);if (parent == cur)parent->_left = leftmid->_left;elseparent->_right = leftmid->_left;cur = leftmid;}delete cur;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->_val << endl;_InOrder(root->_right);}private:Node* _root = nullptr;
};
本篇文章到此结束,如有错误感谢指正。