标题:[数据结构] 二叉搜索树(BSTree)
@水墨不写bug
(图片来源于网络)
目录
正文开始:
(一) 二叉搜索树简介
我们之前在学习二叉树的时候,主要突出了二叉树的结构特点,也就是二叉树的节点内存有val,两个指针分别指向左右子树:(具体内容见《手撕二叉树》)
tamplate<class T>//二叉树节点定义
struct BTreeNode
{
T _val;
BTreeNode<T>* _left;
BTreeNode<T>* _right;
}
tempalte<class T>//二叉树结构
class BTree
{
public:
typedef BTreeNode<T> Node;
private:
Node* _root;
}
如果二叉树仅仅空有复杂的结构特点,那么它的结构特点就会成为它的缺点——你会发现使用二叉树这个复杂的结构存储数据甚至不如用简单的链表结构存储数据。
但事实是二叉树拥有这样的特性:对任意的非叶子节点,他的左子树的根的val < 这个节点的val ;他的右子树的根的val > 这个节点的val;
一个二叉搜索树或者是空树,或者是具有如下性质的二叉搜索树:
如果左子树不为空,则左子树的所有节点的值小于根节点的值;
如果右子树不为空,则右子树的所有节点的值大于根节点的值;
这个二叉搜索树的左右子树都为二叉搜索树。
(二)二叉搜索树头文件
二叉搜索树与普通二叉树的区别在于有一些特殊的规则来限制普通二叉树,遵守一定规则的普通二叉树就可以成为二叉搜索树。
而这个规则就是在插入时将比根节点小的值插入在左子树,将比根节点大的值插入在右子树。
这里不再解释,直接给出搜索二叉树的基本操作的类模板声明,根据类模板的声明逐一实现搜索二叉树:
template<class K, class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& key, const V& value);
Node* Find(const K& key);
bool Erase(const K& key);
void _InOrder(Node* root);
void InOrder();
private:
Node* _root = nullptr;
};
其实仔细想一想,你会发现想要遵守这样的规则,只需要在插入删除的时候注意即可。因此,实现搜索二叉树只需要注重实现插入删除即可,其他的接口的实现与普通二叉树是一致的。
(三)模拟实现二叉搜索树
(1)find:根据key搜索特定值
find是比较思路比较简便的,通过一个指针cur向下查找即可:
如果查找的key小于cur->key,cur = cur->left,在左子树查找;
如果查找的key大于cur->key,cur = cur->right,在右子树查找;
如果查找的key等于cur->key,找到了,返回true,
如果cur走向空都没有找到,表明查找的key不存在,返回false;
//根据key查找节点 bool find(const T& key) { Node* cur = _root; while (cur != nullptr) { if (cur->_key < key) { cur = cur->_right; } else if (cur->_key > key) { cur = cur->_left; } else if (cur->_key == key) { //找到相同值 return true; } return false; } }
(2)insert:插入节点
对于一颗普通的二叉搜索树,想要插入节点,需要首先找到插入的位置,找到插入位置的步骤和find的思路基本一致:
通过cur指针向下查找,如果插入节点的key大于当前节点,则在右子树查找插入位置;如果插入节点小于当前节点,则在左子树查找插入位置;如果插入节点的key等于当前节点的key,则表示有相同值,插入失败。
如果找到插入位置,这个时候发现无法找到parent了,所以在一开始,就需要一个parent指针来记录parent节点,方便后续插入节点。在插入节点时另外需要判断插入在parent的左还是右,这需要额外的一次判断即可。
特殊情况:如果整棵树为空,则直接new新节点并给_root即可。
//插入节点 bool insert(const T& key) { //空树的特殊处理 if (_root == nullptr) { _root = new Node(key); return true; } Node* cur = _root, parent = nullptr; while (cur != nullptr) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key == key) { //有相同值,插入失败 return false; } } cur = new Node(key); if (key < parent->_key) { parent->_left = cur; } else { parent->_right = cur; } return true; }
(3)erase:删除节点
删除节点的整体框架与插入节点类似:
//删除特定节点 bool Erase(const T& key) { Node* cur = _root; Node* parent = nullptr; while (cur != nullptr) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key == key) { //执行删除 } //没有找到,无法删除 return false; } }
但是需要注意的是:在找到并删除节点的时候,需要判断删除节点的位置情况。
——>如果删除节点是叶子节点,则可以直接删除;
——>如果删除节点只有一个孩子,则这个被删除的节点需要将自己的孩子托付给parent即可;
——>如果删除节点有两个孩子,这就需要仔细考虑了;
被删除的节点需要在自己的子孙中找到一个可以托付的节点,这个节点与被删除节点都满足大于左节点的key,小于右节点的key。于是,可以选择左树的最大值或者右树的最小值。找到他们即可:
//删除特定节点 bool Erase(const T& key) { Node* cur = _root; Node* parent = nullptr; while (cur != nullptr) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else if (cur->_key == key) { //找到相同值,执行删除 //没有孩子,可以直接删除这个节点 // 只有一个节点,也可以归纳为没有孩子的情况 if (cur->_left == nullptr) { if (parent == nullptr) _root = cur->_right; else { if (cur == parent->_left) parent->_left = cur->_right; else parent->_right = cur->_right; } delete cur; return true; } else if (cur->_right == nullptr) { if (parent == nullptr) _root = cur->_left; { if (cur == parent->_left) parent->_left = cur->_left; else parent->_right = cur->_left; } delete cur; return true; } else { //有两个孩子的情况 //需要找左树最大值或者---<右树最小值> Node* PRightMin = cur; Node* RightMin = cur->_right; while (RightMin->_left) { PRightMin = RightMin; RightMin = RightMin->_left; } if (RightMin = PRightMin->_left) PRightMin->_left = RightMin->_right; else PRightMin->_right = RightMin->_right; cur->_key = RightMin->_key; delete RightMin; return true; } } //没有找到,无法删除 return false; } }
(4)inorder:中序遍历,验证二叉树是否是搜索二叉树
中序遍历有两种写法:递归和迭代;
递归法写法简单,但是可能导致栈溢出;
迭代法写法较复杂,但是高效。
此处我们采用简便的递归法。递归,需要在函数内部调用自己,这也就需要我们能够得到root节点,这要求函数必须传递参数:
//中序遍历 void Inorder(Node* _root) { if (_root == nullptr) return; Inorder(_root->_left); cout << _root->_key; Inorder(_root->_right); }
如果想要调用这个函数,就需要传递_root,但是这个成员是私有的,无法访问。仅仅为了调用一个函数而将私有成员改变为公有成员,会破坏封装,是不明智的选择。
我们可以如下方这样解决:
template<class T> class BSTree { public: typedef BSTreeNode<T> Node; //中序遍历 void Inorder() { _Inorder(_root); cout << endl; } private: void _Inorder(Node* _root) { if (_root == nullptr) return; _Inorder(_root->_left); cout << _root->_key<<" "; _Inorder(_root->_right); } Node* _root = nullptr; };
套一层私有函数,这样就不仅解决了访问权限问题还方便了调用,少传一个参数。
完~
未经作者同意禁止转载