我们经过C语言阶段的时候已经了解过基础的二叉树的知识点了
那么,现在我们使用C++来认识更加复杂的树吧。
二叉搜索树(BST)
它的英文全称为:Binary Search Tree(BST)
1概念:二叉搜索树又称为二叉排序树,它是一颗空树或者是具有以下特征:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它们各自的左右子树又分别是搜索二叉树。 (上图只是因为有写是空树)
有意思的点来了,我们如果使用中序遍历的话,会得到有趣的结果:
复习一下:
中序遍历:左--根--右
前序遍历:根--左--右
后序遍历:左--右--根
层序遍历:一层一层遍历
那么,我们用中序遍历的话:像上面的图:
得出来的数组:【1,3,4,6,7,8,10,13,14】这是不是就变成了有序的了?因此它也可以用来排序。
好了,有了上面我们的基本了解:目前我们的任务就是如何构建起来这么一颗树来?
好,现在让我们一步步去完善构建起来吧!
定义树节点的结构变量
template<class K> struct BSTreeNode { BSTreeNode<K>* _left; //左子树 BSTreeNode<K>* _right; //右子树 K _key; //插入的数 BSTreeNode(const K& key) //初始化 :_left(nullptr) ,_right(nullptr) ,_key(key) { } };
我们这里使用到模板,因为我们并不知道实际要的是什么类型的变量。
这里跟我们之前的都差不多,所以就不多讲解了,看着代码应该就能理解了。
定义BSTree的成员框架
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
private:
Node* _root; //根节点
};
初始化BSTree
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
BSTree()
:_root(nullptr)
{ }
private:
Node* _root; //根节点
};
BSTree的插入部分
我们来分析如何来插?
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
比如说插入12和16这两个数字:
上面分析得到:
1.我们根据二叉搜索树的性质:如果插入数字比目前的根小:就去左子树找,插入数字比目前的根大:就去右子树那边去找。如果插入数字与目前的根数字相等:就说明插入数字有误,不符合二叉搜索树的性质。
bool Insert(const K& key) { //空树的情况 if (_root == nullptr) { _root = new Node(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 { return false; } } cur = new Node(key); //创建新节点 if (parent->_key > key) { parent->_left = cur; } else { parent->_right = cur; } return true; }
1.我们需要在比较更新cur之前,得保存起来原来的cur值。赋值给新定义的一个parent的节点指针:原因看下图:
如果你不定义保存原来,你就没办法回去了:eg:你要插入16,到了14那里,比较了比14大,找右子树,在进行下一个,cur指向到空,就说明就是要插的位置,但是如果你不保存之前cur的位置即14,你怎么顺利插进去?所以我们才定义了parent保存了前一个cur的位置。插值进去时:就只要parent->right=16,成功插进去!
BSTree删除部分:
1.删除部分就不像我们之前那样简单了,这其中还藏有非常多的坑,现在我们来一一分析处理:
2.首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
这样讲可能有点抽象:现在我们来有具体的例子来说明一下:
eg:要删14,这个就符合只有左孩子节点的情况,删除了怎么做呢?是不是得把13变为10的右孩子?
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
同样,这个跟情况b类似。
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除(找左边最大结点,或者找右边最小结点,然后交换值)
接下来的我会使用左边最大结点的方法。
再来一个,这里会存在特殊情况,要删除8的话,直接让parent->left=leftmax->left;
bool Erase(const K&key) { /*if (_root == __nullptr) { return false; }*/ Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key < key) { parent = cur; //保存前一个cur cur = cur->_right; } else if (cur->_key > key) { parent = cur; //保存前一个cur cur = cur->_left; } else //找到了 { //左为空 if (cur->_left == nullptr) { //左边为空,且删的就是根 // 8 // 10 // 9 12 if (cur == _root) { _root = cur->_right; } else { if (parent->_right == cur) { parent->_right = cur->_right; } else { parent->_left = cur->_right; } } } //右为空 else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_right; } else { if (parent->_right == cur) { parent->_right = cur->_left; } else { parent->_left = cur->_left; } } } else //两边都为空 { Node* parent = cur; Node* Maxleft = cur->_left; while (Maxleft->_right) { parent = Maxleft; Maxleft = Maxleft->_right; } swap(cur->_key, Maxleft->_key); //特殊情况处理 // 8 // 3 10 ---->删8 // 1 if (parent->_left == Maxleft) { parent->_left = Maxleft->_left; } else //上面的情况的另一边 { parent->_right = Maxleft->_left; } cur = Maxleft; //更新cur } delete cur; return true; } } return false; }
代码里面的解释已经很清楚了,就不再多讲解了。
中序遍历的实现
void Inorder() { _Inorder(_root); cout << endl; } void _Inorder(Node*root) { if (root == nullptr) { return; } _Inorder(root->_left); //遍历左子树 cout << root->_key << " "; //根 _Inorder(root->_right); //遍历右子树 }
这里使用了子函数的形式,为了可以让类外面可以进行调用,如果你不是这样实现的话,由于你的中序遍历里面使用到了私有的成员变量,只能在类内部使用,类外部不能使用。
ps:C++中,凡是写这种树形,这种递归,就要单独写一个子函数,递归要控制参数的变化
另一种写法:我们上面的写法是非递归的写法,那么,我们如果写成递归的话,又该怎么写呢?
BSTree递归写法:
递归插入部分
1.这里不要忘记了引用:上一个root指针的别名,改变的指针
平时之前的引用:不会开空间:谈论的是语法层,语法层引用不开空间,平时谈论引用与指针的区别:谈用法。
它在底层是开空间的,并且改不了指向。那么为什么递归又可以呢?
因为递归是在多个栈帧里面的,各个层的_root不是同一个,不同域里面可以定义同名变量,不同栈帧相当于不同引用。
bool insertR(const K&key)
{
return _insertR(_root, key);
}
//加引用
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;
}
递归查找:
bool FindR(const K& key)
{
return _FindR(_root, key);
}
//查找
bool _FindR(Node*&root,const K&key)
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _FindR(root->right, key);
}
else if (root->_key > key)
{
return _FindR(root->left, key);
}
else //相等,即找到了,返回true
{
return true;
}
return false;
}
递归删除部分
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
//删除
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key > key) //找删除的值在哪个位置
{
return _EraseR(root->left, key);
}
else if (root->_key < key) //找删除的值在哪个位置
{
return _EraseR(root->right, key);
}
else //找到了
{
Node* del = root;
//左为空
// 8
// 11
// 9 12
if (root->left == nullptr)
{
root = root->right;
}
//右为空
// 8
// 6
// 5 7
//
if (root->right == nullptr)
{
root = root->left;
}
//左右不为空
else
{
//找左边最大的值
Node* leftmax = root->left;
while (leftmax->right)
{
leftmax = leftmax->right;
}
//交换
swap(leftmax->_key, root->_key);
return _EraseR(root->left, key);
//注意:这里可不敢弄成return _EraseR(leftmax, key);
}
delete del;
return true;
}
}
解释:
return _EraseR(root->left, key); //注意:这里可不敢弄成return _EraseR(leftmax, key);
因为还有这么一种情况呢:如果你使用了leftmax,你都删除了,就会越界访问了,报错
结点的复制
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;
}
好了,基本的二叉搜索树,我们就基本完成了,现在我们来介绍一下关于它的应用。
在此之前,我们先认识两个模型:这两个模型也会在后面学习map与set,红黑树等等有涉及到的,这里也相当于做铺垫了:
两个模型:
1. K模型:
K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:
每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对
现在,我们就来改造成KV模型,模拟一下字典的形式:
namespace key_val
{
//模板参数有两个,一个K,一个V
template<class K,class V>
struct BSTreeNode
{
BSTreeNode<K,V>* left;
BSTreeNode<K,V>* right;
K _key;
V _value;
//初始化
BSTreeNode(const K& key, const V& value)
:left(nullptr)
, right(nullptr)
, _key(key)
,_value(value)
{
}
};
template<class K,class V>
class BSTree
{
typedef BSTreeNode<K,V> Node;
public:
//构造函数
BSTree()
:_root(nullptr)
{
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
bool insertR(const K& key,const V& value)
{
return _insertR(_root, key,value);
}
Node* FindR(const K& key)
{
return _FindR(_root, key);
}
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->left, key);
}
else if (root->_key < key)
{
return _EraseR(root->right, key);
}
else //找到了
{
Node* del = root;
//左为空
if (root->left == nullptr)
{
root = root->right;
}
//右为空
if (root->right == nullptr)
{
root = root->left;
}
//左右为空
else
{
Node* leftmax = root->left;
while (leftmax->right)
{
leftmax = leftmax->right;
}
swap(leftmax->_key, root->_key);
return _EraseR(root->left, key);
}
delete del;
return true;
}
}
//查找
Node* _FindR(Node*& root, const K& key)
{
if (root == nullptr)
return nullptr;
if (root->_key < key)
{
return _FindR(root->right, key);
}
else if (root->_key > key)
{
return _FindR(root->left, key);
}
else
{
return root;
}
}
//加引用
bool _insertR(Node*& root, const K& key, const V& value)
{
if (root == nullptr)
{
root = new Node(key,value);
return true;
}
if (root->_key < key)
{
return _insertR(root->right, key,value);
}
else if (root->_key > key)
{
return _insertR(root->left, key,value);
}
else
return true;
}
void _Inorder(Node* root)
{
if (root == nullptr)
return;
_Inorder(root->left);
cout << root->_key <<":" << root->_value <<endl;
_Inorder(root->right);
}
private:
Node* _root;
};
void BSTreeTest1()
{
BSTree<string, string> dict;
dict.insertR("apple", "苹果");
dict.insertR("orange", "橙子");
dict.insertR("banana", "香蕉");
dict.insertR("fruit", "水果");
dict.insertR("eat", "吃");
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.FindR(str);
if (ret)
{
cout<< ret->_value << endl;
}
else
{
cout << "无此单词" << endl;
}
}
}
其实这个是非常基础并且简陋的KV模型,所以比较简单,本质上这里也就是在,模板,插入那里加多了一个参数。而且 KV模型后面我们也会有讲解,所以在这里就不多讲解了。相信大家看着代码也能想明白的!
最后,到了本次鸡汤环节:
图片文字与大家共勉!