片头
嗨!小伙伴们,大家好~ 今天我们来学习二叉树进阶相关知识,准备好了吗?咱们开始咯~
一、前言
在前面的章节中,我们学习过二叉树:数据结构之二叉树
后面学习map和set的特性时,需要先铺垫二叉搜索树,二叉搜索树也是一种树形结构。
对二叉搜索树的特性了解,有助于更好的理解map和set的特性。
有些OJ题使用C语言实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。
这一章中,我们学习二叉搜索树,对二叉树部分进行收尾总结。
二、二叉搜索树
2.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一颗空树,或者具有以下性质的二叉树:
(1)若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
(2)若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
(3)它的左右子树也分别为二叉搜索树
搜索二叉树也称为排序二叉树。如果对搜索二叉树进行中序遍历,那么自然就拍成了升序。
int a[] = { 8,3,1,10,6,4,7,14,13 };
中序遍历结果如下:1 3 4 6 7 8 10 13 14
看,是不是恰好有序啦?
这就是利用了二叉搜索树的特性:左子树的所有节点值比跟节点小;右子树的所有节点值比根节点大
2.2 实现二叉搜索树
我们先创建 SearchBinaryTree.h 头文件,用来声明和定义二叉搜索树;接着创建 test.cpp 源文件,用来测试二叉搜索树是否合法。
(1)定义节点
template<class T>
struct BSTreeNode
{
//构造函数
BSTreeNode(const T& val)
:_data(val)
,_left(nullptr)
,_right(nullptr)
{ }
T _data; //节点的值
BSTreeNode<T>* _left; //左孩子
BSTreeNode<T>* _right; //右孩子
};
(2)定义SearchBinaryTree类
在SearchBinaryTree.h 头文件中,定义SearchBinaryTree类,重命名 BSTreeNode<T> 为 Node。定义私有成员变量_root。
template<class T>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
//成员函数
private:
//成员变量
Node* _root = nullptr; //根节点,初始化为空
};
(3)查找节点
a、从根开始查找,将节点的值和查找的数进行比较;比根大则往右边查找,比根小则往左边走查找
b、最多查找高度次,如果走到空,还没有找到,则这个节点不存在。
//查找这个值是否存在
bool Find(const T& Key)
{
Node* cur = _root; //定义变量cur,
//用来循环遍历搜索二叉树的每一个节点
while (cur)
{
if (cur->_data > Key) //若Key比当前节点的值小,走左子树部分
{
cur = cur->_left;
}
else if (cur->_data < Key) //若Key比当前节点的值大,走右子树部分
{
cur = cur->_right;
}
else
{
return true; //找到了,返回true
}
}
return false; //没有找到,返回false
}
(4)插入节点
a、树为空,则直接新增节点,赋值给root指针
b、树不为空,按二叉树的性质查找插入位置,插入新节点
//插入新节点
bool Insert(const T& Key)
{
if (_root == nullptr) //若当前根节点为空
{
_root = new Node(Key); //将新节点直接赋给_root
return true;
}
Node* Parent = nullptr; //假设父节点初始值为空
Node* cur = _root; //从根节点开始遍历
while (cur)
{
if (cur->_data > Key) //若Key值比当前节点的值小
{
Parent = cur;
cur = cur->_left; //走左子树部分
}
else if (cur->_data < Key) //若Key值比当前节点的值大
{
Parent = cur;
cur = cur->_right; //走右子树部分
}
else
{
return false; //找到了一个和Key值相等的,返回false
}
}
//找到合适的位置,插入新节点
cur = new Node(Key);
if (Parent->_data < Key) //若当前父节点的值比Key值小
{
Parent->_right = cur; //将新节点放到父节点的右子树上
}
else //当前父节点的值比Key值大
{
Parent->_left = cur; //将新节点放到父节点的左子树上
}
return true; //插入完毕后,返回true
}
(5)中序遍历
提到中序遍历,代码很简单,按照左节点 - 根 - 右节点的方式来递归实现
//中序遍历
void InOrder(Node* root)
{
if (root == nullptr) //若当前节点为空,返回
{
return;
}
InOrder(root->_left); //左节点
cout << root->_data << " "; //值
InOrder(root->_right); //右节点
}
我们可以在test.cpp里面试一下:
那怎么办呢?别着急,咱们可以把中序遍历的核心代码设定为子函数_InOrder并且私有化,用成员函数InOrder调用子函数即可。
template<class T>
class BSTree
{
typedef BSTreeNode<T> Node;
public:
void InOrder()
{
_InOrder(_root); //虽然成员变量私有化了,
//但是类中的成员函数依然可以使用
cout << endl;
}
private:
void _InOrder(Node* root) //将中序遍历设定为子函数即可
{
if (root == nullptr) //如果当前节点为空,直接返回
{
return;
}
_InOrder(root->_left); //左节点
cout << root->_data << " "; //根
_InOrder(root->_right); //右节点
}
Node* _root = nullptr; //根节点,初始化为空
};
我们再试试:
#include"SearchBinaryTree.h"
int main()
{
int a[] = { 8,3,1,10,6,4,7,14,13 };
BSTree<int> t;
for (auto& e : a)
{
t.Insert(e);
}
t.InOrder();
return 0;
}
(6)二叉树的删除
首先,咱们先列一个大致框架,和前面Find函数类似。
如果Key比当前节点的值大,那么往右查找;若Key比当前节点的值小,那么往左查找。
//删除节点
bool erase(const T& Key)
{
Node* parent = nullptr;
Node* cur = _root; //初始时,从根节点开始遍历
while (cur)
{
if (cur->_data < Key)
{
parent = cur; //将子节点赋给父节点
cur = cur->_right;
}
else if (cur->_data > Key)
{
parent = cur; //将子节点赋给父节点
cur = cur->_left;
}
else
{
//删除节点
}
}
return false; //没有找到,返回false
}
讲解删除部分稍微有点复杂,我们具体可以分为以下3种情况:
其中,第1种情况和第2种情况可以合并,因为没有孩子(叶子节点)的情况包括了左孩子为空 / 右孩子为空。
所以,当删除节点的时候,一定要看清楚被删除节点在父节点的左边还是右边
//删除节点
if (cur->_left == nullptr) //0~1个孩子
{
if (parent->_left == cur) //若被删除的节点在父节点的左边
{
parent->_left = cur->_right;
}
else //若被删除的节点在父节点的右边
{
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr) //0~1个孩子
{
if (parent->_left == cur) //如果被删除的节点在父节点的左边
{
parent->_left = cur->_left;
}
else //若被删除的节点在父节点的右边
{
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//被删除的节点有2个孩子的情况
}
现在,被删除的节点没有孩子、有1个孩子的情况已经解决了。那么,如果被删除的节点有2个孩子,怎么办呢?
//被删除的节点有2个孩子的情况
//右子树的最小值
// --> 右子树的最左边的节点 --> 这个节点没有左孩子,可能有右孩子
Node* rightMinP = nullptr;
Node* rightMin = cur->_right; //找右子树
while (rightMin->_left) //寻找右子树中最左边的节点
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_data = rightMin->_data; //将数据替换
if (rightMinP->_left == rightMin) //如果被删除的节点为父节点的左孩子
{
rightMinP->_left = rightMin->_right; //将父节点的左指针指向被删除节点的右孩子
}
else //如果被删除的节点为父节点的右孩子
{
rightMinP->_right = rightMin->_right; //将父节点的右指针指向被删除节点的右孩子
}
delete rightMin; //替换完成,将这个节点删除
大致基本上完成了,但还有2个小问题:
Q1:关于rightMinP初始化的问题
Node* rightMinP = cur; //rightMinP初始化为被删除的节点cur
现在,我们将所有节点依次删除,运行一下程序:
#include"SearchBinaryTree.h"
int main()
{
int a[] = { 8,3,1,10,6,4,7,14,13 };
BSTree<int> t;
for (auto& e : a)
{
t.Insert(e);
}
t.InOrder();
for (auto& e : a)
{
t.erase(e);
t.InOrder();
}
return 0;
}
其实,还有1种情况没有考虑到:
完整代码如下:(有一点点长,不过,其实也不算太难)
//删除节点
bool erase(const T& Key)
{
Node* parent = nullptr;
Node* cur = _root; //初始时,从根节点开始遍历
while (cur)
{
if (cur->_data < Key)
{
parent = cur; //将子节点赋给父节点
cur = cur->_right;
}
else if (cur->_data > Key)
{
parent = cur; //将子节点赋给父节点
cur = cur->_left;
}
else
{
//删除节点
if (cur->_left == nullptr) //0~1个孩子
{
if (parent == nullptr) //若被删除的节点cur为根节点
{
_root = cur->_right; //_root更新为cur的右孩子
}
else {
if (parent->_left == cur) //若被删除的节点在父节点的左边
parent->_left = cur->_right;
else //若被删除的节点在父节点的右边
parent->_right = cur->_right;
}
delete cur;
return true;
}
else if (cur->_right == nullptr) //0~1个孩子
{
if (parent == nullptr) //若被删除的节点cur为根节点
{
_root = cur->_left; //_root更新为cur的左孩子
}
else {
if (parent->_left == cur) //若被删除的节点在父节点的左边
parent->_left = cur->_left;
else //若被删除的节点在父节点的右边
parent->_right = cur->_left;
}
delete cur;
return true;
}
else
{
//被删除的节点有2个孩子的情况
//右子树的最小值
// --> 右子树的最左边的节点 --> 这个节点没有左孩子,可能有右孩子
Node* rightMinP = cur; //rightMinP初始化为被删除的节点cur
Node* rightMin = cur->_right; //找右子树
while (rightMin->_left) //寻找右子树中最左边的节点
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_data = rightMin->_data; //将数据替换
if (rightMinP->_left == rightMin) //如果被删除的节点为父节点的左孩子
{
rightMinP->_left = rightMin->_right; //将父节点的左指针指向被删除节点的右孩子
}
else //如果被删除的节点为父节点的右孩子
{
rightMinP->_right = rightMin->_right; //将父节点的右指针指向被删除节点的右孩子
}
delete rightMin; //替换完成,将这个节点删除
return true;
}
}
}
return false; //没有找到,返回false
}
运行一下:
(7)查找节点(递归版)
二叉搜索树的递归查找很简单,因为外面不能传根,这里像InOrder一样封装起来:
//查找(递归版)
bool FindR(const T& Key)
{
return _FindR(_root, Key);
}
bool _FindR(Node* root, const T& Key)
{
if (root == nullptr)
{
return false;
}
if (root->_data < Key)
{
_FindR(root->_right, Key);
}
else if (root->_data > Key)
{
_FindR(root->_left, Key);
}
else
{
return true;
}
}
(8)插入节点(递归版)
//插入(递归)
bool InsertR(const T& Key)
{
return _InsertR(_root, Key);
}
bool _InsertR(Node* root, const T& Key)
{
if (root == nullptr)
{
root = new Node(Key);
return true;
}
if(root->_data < Key)
{
return _InsertR(root->_right, Key);
}
else if (root->_data > Key)
{
return _InsertR(root->_left, Key);
}
else
{
return false;
}
}
因为递归到最后1步的时候,root只是一个局部变量,根本插入不了数据。
可以一步一步的把父亲传下来,但这里有一个关键点:加引用
//插入(递归)
bool InsertR(const T& Key)
{
return _InsertR(_root, Key);
}
bool _InsertR(Node*& root, const T& Key)
{
if (root == nullptr)
{
root = new Node(Key);
return true;
}
if(root->_data < Key)
{
return _InsertR(root->_right, Key);
}
else if (root->_data > Key)
{
return _InsertR(root->_left, Key);
}
else
{
return false;
}
}
这里的引用最后1步才起作用,它是空,但它也是上一层传下来的别名。
赋给root,就把父亲链接起来了,可以用二级指针,但传引用就很方便。
(9)二叉树的删除(递归版)
这里的递归删除和上面的递归插入一样,也用到了非常巧妙的引用:
//二叉树节点的删除(递归版)
bool EraseR(const T& key) {
return _EraseR(_root, key);
}
bool _EraseR(Node*& root, const T& Key)
{
if (root == nullptr)
{
return false;
}
if (root->_data < Key)
{
return _EraseR(root->_right, Key);
}
else if (root->_data > 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* MinNode = root->_right;
while (MinNode->_left)
{
MinNode = MinNode->_left;
}
//root->_data = MinNode->_data; //错误
swap(root->_data, MinNode->_data);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
现在,我们测试一下:
test.cpp
#include"SearchBinaryTree.h"
void test1()
{
BSTree<int> t;
int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
t.EraseR(8);
t.EraseR(3);
t.InOrder();
for (const auto& e : arr)
{
t.EraseR(e);
}
t.InOrder();
}
void test2()
{
BSTree<int> t;
int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
t.EraseR(8);
t.EraseR(3);
t.EraseR(1);
t.InOrder();
cout << t.FindR(10) << endl;
cout << t.FindR(100) << endl;
}
int main()
{
test1();
cout << "=======================" << endl;
test2();
return 0;
}
(10)析构、拷贝构造和赋值
test.cpp默认生成的:
void test3()
{
BSTree<int> t;
int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
BSTree<int> copy = t;
copy.InOrder();
}
int main()
{
test3();
return 0;
}
这里也是浅拷贝问题,指向的是同一颗树,编译器没有崩溃,只是没写析构,写一下析构函数:
//析构函数
~BSTree()
{
_Destroy(_root);
}
private:
void _Destroy(Node*& root)
{
if (root == nullptr)
{
return;
}
_Destroy(root->_left);
_Destroy(root->_right);
delete root;
root = nullptr;
}
再测试一下:
这时,我们就应该自己写拷贝构造了:
//拷贝构造
BSTree(const BSTree<K,V>& t)
{
_root = _Copy(t._root);
}
private:
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* CopyRoot = new Node(root->_data); //中序递归链接左右子树
CopyRoot->_left = _Copy(root->_left);
CopyRoot->_right = _Copy(root->_right);
return CopyRoot;
}
此时,运行就会报错:
错误(活动) E0291 类 "BSTree<int>" 不存在默认拷贝构造函数
我们再写一个默认的拷贝构造:
//拷贝构造
BSTree(const BSTree<K,V>& t)
{
_root = _Copy(t._root);
}
//BSTree() //这样写也可以,但下面是C++11的用法
//{ }
BSTree() = default; //C++11关键字,强制编译器生成默认的构造
private:
Node* _Copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* CopyRoot = new Node(root->_data); //中序递归链接左右子树
CopyRoot->_left = _Copy(root->_left);
CopyRoot->_right = _Copy(root->_right);
return CopyRoot;
}
运行程序:
写了拷贝构造,我们就可以直接用现代写法写一个赋值:
//赋值重载
BSTree<k> operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
测试一下:
void test4()
{
BSTree<int> t;
int arr[] = { 8,3,1,10,2,2,3,6,4,7,14,13 };
for (const auto& e : arr)
{
t.InsertR(e);
}
t.InOrder();
BSTree<int> copy = t;
copy.InOrder();
BSTree<int> copy = t;
copy.InOrder();
BSTree<int> t2;
t2.InsertR(3);
t2.InsertR(5);
t2.InsertR(4);
copy = t2;
t2.InOrder();
copy.InOrder();
}
int main()
{
test4();
return 0;
}
三、搜索二叉树的应用
3.1 K 模型
K模型,即只有 key 作为关键码,我们上面写的就是 K 模型,
结构中只需存储 key 即可,关键码就是需要搜索到的值。
举个例子:对于单词 word,我们需要判断该单词是否拼写正确
以单词集合中的每个单词作为key,构建一个搜索二叉树。
在二叉树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
3.2 KV 模型
KV模型,每一个关键码 key,都有与之相对应的值 Value,即 <Key, Value> 的键值对。
这就像 python 中的 dict 字典类型一样,key 和 value 对应。
这在生活中也是非常常见的,比如英汉词典就是英文和中文的对应关系,通过英文可以快速检索到对应的中文,英文单词也可以与其对应的中文构建出一种键值对:
<string, string> 即 <word, chinese>
再比如统计水果次数,就构建出了一种键值对:
<string, int> 即 <水果, count>
直接放用于测试的代码:
BinarySearchTree.h
//二叉搜索树的KV结构
namespace keyValue
{
//定义2个类模板参数K、V
template<class K,class V>
struct BSTreeNode
{
K _key; //存放了2个类型的数据,相比较于K类型
V _value;
BSTreeNode<K, V>* _left;
BSTreeNode<K, V>* _right;
BSTreeNode(const K& key,const V& value)
:_key(key)
,_value(value)
,_left(nullptr)
,_right(nullptr)
{ }
};
//同样的,定义2个类模板参数K、V
//搜索二叉树依旧是按照K的数据进行排序,与V无关
template<class K,class V>
class BSTree
{
typedef BSTreeNode<K, V> Node;
public:
//查找只和数据_key有关,与数据_value无关
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;
}
//插入新节点和原来的代码逻辑相似
//只是多加了1个value数据
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;
}
//删除数据只和数据_key有关,与数据_value无关
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
{
//被删除的当前节点有2个孩子
//右子树的最小节点作为替代节点
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left)
{
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_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);
}
Node* _root = nullptr;
};
}
test.cpp
#include"BinarySearchTree.h"
void test2()
{
keyValue::BSTree<string, string> dict; //字典树,如果所有单词都在里面就能很准确查找
dict.insert("up", "上边");
dict.insert("down", "下边");
dict.insert("left", "左边");
dict.insert("right", "右边");
string str;
while (cin >> str) //Ctrl+z+换行结束或者Ctrl+c结束
{
keyValue::BSTreeNode<string, string>* ret = dict.find(str);
if (ret)
{
cout << "对应的中文: " << ret->_value << endl;
}
else
{
cout << "无此单词,请重新输入!" << endl;
}
}
}
int main()
{
test2();
return 0;
}
再来一个例子:
#include"BinarySearchTree.h"
void test3() //统计水果出现的次数
{
string arr[] = { "苹果","西瓜","葡萄","香蕉","西瓜",
"西瓜","苹果","葡萄","苹果","西瓜" };
keyValue::BSTree<string, int> CountTree;
for (auto& str : arr)
{
//先查找水果在不在搜索树中
//1、不在,说明水果第1次出现,则插入<水果,1>
//2、在,则查找到的节点中水果对应的次数++
//keyValue::BSTreeNode<string, int>* ret = CountTree.find(str);
auto ret = CountTree.find(str);
if (ret == nullptr)
{
CountTree.insert(str, 1);
}
else
{
ret->_value++;
}
}
CountTree.InOrder();
}
int main()
{
test3();
return 0;
}
片尾
今天我们学习了二叉搜索树,这是学习map和set的必备知识,希望看完这篇文章对友友们有所帮助!!!
求点赞收藏加关注!!!
谢谢大家!!!