前言
为什么要学习二叉搜索树呢?因为set 和 map的底层实际上就是一颗二叉搜索树,只不过是被进行了一些特殊的处理,所有了解二叉搜索树的底层实现有利于我们更好的理解的map和set的原理。二叉搜索树又叫二叉排序树,它或者是一颗空树,或者具有以下的性质:1.若它的左子树不为空,则左子树上所有的节点都小于根,若它的右子树不为空则它的右子树上的所有节点都大于根。它的左右子树也是二叉搜索树。让我们一起来看看吧!
目录
Binary Search Tree的实现代码
1.二叉搜索树的操作
1.1二叉搜索树的查找
1.2二叉搜索树的插入
1.3二叉搜索树的删除
1.4Inorder
3.二叉搜索树的应用
3.1K模型
3.2KV模型
4.二叉搜索树的缺点
它的实现比较简单,我相信你看一看这篇博客就能学会
二叉搜索树图:
Binary Search Tree的实现代码
namespace qyy
{
template<class T>
struct BSTNode//二叉搜索树的节点
{
typedef BSTNode<T> Node;
BSTNode(const T&key = T())//构造函数初始化二叉搜索树节点,
:_left(nullptr)
,_right(nullptr)
,_key(key)
{ }
public:
Node* _left;
Node* _right;
T _key;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
public:
BSTree(const T& key = T())//构造函数只需要将根节点初始化为空就好了
{
_root = nullptr;
}
//无需析构函数
bool Insert(const T&key)//插入key值
{
if (_root == nullptr)//如果为空树
{
_root = new Node(key);//申请新的节点
_root->_left = _root->_right = nullptr;//初始化
return true;
}
Node* cur = _root;
Node* parent = cur;//保存cur的父亲节点
//这里通过双指法进行插入
while (cur)//遍历搜索树找要插入的位置
{
if (cur->_key > key)//key的值小于当前节点的值,去节点的左边子树找位置
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)//key的值大于当前节点的值,去节点的右边子树找位置
{
parent = cur;
cur = cur->_right;
}
else//key的值与当前节点的值相同无法插入,结束插入,因为二叉搜索树额key值不能是相同的,这是由它的性质决定的
{
return false;
}
}
Node* tmp = new Node(key);//申请新的节点
if (parent->_key > key)//通过比较节点的值和插入的key值来确认插入位置
parent->_left = tmp;
else
parent->_right = tmp;
return true;
}
bool Find(const T& key)
{
if (_root == nullptr)//如果根节点为空,找到key值
return false;
Node* cur = _root;
while (cur)//在二叉搜索树中对应的节点的值
{
if (cur->_key > key)//key小于当前节点的值,到左子树中找
cur = cur->_left;
else if (cur->_key < key)//key大于当前节点的值,到右子树中找
cur = cur->_right;
else
return true;//key的值等于当前节点的值,找到了返回true
}
return false;
}
void _Inorder(Node*root)//二叉搜索树的中序遍历
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key<<" ";
_Inorder(root->_right);
}
void Inorder()
{
_Inorder(_root);//调用中序遍历,进行遍历因为如果直接写在类的外面获取不到根节点的值就无法进行传参,所以这要写一个中序遍历的子函数,传参调用进行中序遍历
cout << endl;
}
bool Erase(const T&key)//查找并删除key值
{
if (_root == nullptr)//如果根节点为空不需要查找
return false;
Node* cur = _root;
Node* parent = cur;//用来保存cur的父亲节点的值
if (cur->_key == key && cur->_left == nullptr && cur->_right == nullptr)//左右子树为空才相等,且要key的值等于当前节点的值。
{
delete cur;//删除根节点
_root = nullptr;
return true;
}
//查找并删除采用双指针的方法
while (cur)//遍历查找并删除val值的节点
{
if (cur->_key > key)//在左子树中找
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)//在右子树中查找
{
parent = cur;
cur = cur->_right;
}
else//找到含key值的节点
{
//删除节点
if (cur->_left == nullptr )//如果左子树为空
{
if (parent->_key > cur->_key)//判断父亲节点和要删除的节点的关系,
parent->_left = cur->_right;//断开节点的链接
else
parent->_right = cur->_right;
delete cur;//删除节点
cur = nullptr;
return true;
}
else if (cur->_right == nullptr )//如果右子树都为空
{
if (parent->_key > cur->_key)//判断父亲节点和要删除的节点的关系,
parent->_left = cur->_left;//断开节点的链接
else
parent->_right = cur->_left;
delete cur;//删除节点
cur = nullptr;
return true;
}
else //左右孩子都存在
{
Node* tmp = cur->_right;//如果要删除的节点的左右孩子都存在的话,就要考虑,找到这个节点的左子树中val值做大的节点
//或者右子树中val值最小的节点,交换它们的值,然后删除左子树中这个最大的值的节点,或者右子树中最小的值的这个节点
//采用双指针法来保存右子树中的最小值节点的父节点
Node* tmpParent = nullptr;
while (tmp->_left)//在节点的右子树中找最左节点,也就是右子树中节点值最小的节点
{
tmpParent = tmp;//保存父亲节点
tmp = tmp->_left;//右子树中的最左孩子
}
cur->_key = tmp->_key;//用右子树中节点的最小值来替换要删除的节点
if (tmpParent == nullptr)//说明要删除的节点的右节点就是要替换的节点
{
cur->_right = tmp->_right;//最小值所在的节点的右节点不为空链接cur和最小节点的右节点
}
else
{
if (tmpParent->_key > tmp->_key)//判断最小节点和它父节点的关系,它在父节点的左边还是右边
tmpParent->_left = nullptr;
else
tmpParent->_right = nullptr;
}
delete tmp;//删除这个替换的节点,也就是右子树中最小的节点
tmp = nullptr;
return true;
}
}
}
return false;
}
private:
Node* _root;
};
}
测试代码:
#include<iostream>
using namespace std;
#include"BSTree.hpp"
void Test1()
{
qyy::BSTree<int> b1;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto& e : a)
b1.Insert(e);
b1.Inorder();
cout<< b1.Find(20);
}
void Test2()
{
qyy::BSTree<int> b2;
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
for (auto& e : a)
b2.Insert(e);
for (auto& e : a)
b2.Erase(e);
b2.Inorder();
}
int main()
{
//Test1();
Test2();
return 0;
}
1.二叉搜索树的操作
1.1二叉搜索树的查找
从根节点开始查找,如果比根节点的值小往左子树中查找,如果比根节点的值大往右子树中查找 。最多查找高度次,走到空还没有找到这个值为空。
代码:
bool Find(const T& key)
{
if (_root == nullptr)//如果根节点为空,找到key值
return false;
Node* cur = _root;
while (cur)//在二叉搜索树中对应的节点的值
{
if (cur->_key > key)//key小于当前节点的值,到左子树中找
cur = cur->_left;
else if (cur->_key < key)//key大于当前节点的值,到右子树中找
cur = cur->_right;
else
return true;//key的值等于当前节点的值,找到了返回true
}
return false;//没有找到
}
1.2二叉搜索树的插入
插入的过程:如果树为空,则直接新增节点,赋值给root指针
树不为空按二叉搜索树的性质查找到插入的位置,插入新节点,如图:
注意只能插入二叉搜索树中没有的key值,这是由二叉搜索树的性质决定的。插入树中的地方都是空的位置。
代码:
bool Insert(const T&key)//插入key值
{
if (_root == nullptr)//如果为空树
{
_root = new Node(key);//申请新的节点
_root->_left = _root->_right = nullptr;//初始化
return true;
}
Node* cur = _root;
Node* parent = cur;//保存cur的父亲节点
//这里通过双指法进行插入
while (cur)//遍历搜索树找要插入的位置
{
if (cur->_key > key)//key的值小于当前节点的值,去节点的左边子树找位置
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)//key的值大于当前节点的值,去节点的右边子树找位置
{
parent = cur;
cur = cur->_right;
}
else//key的值与当前节点的值相同无法插入,结束插入,因为二叉搜索树额key值不能是相同的,这是由它的性质决定的
{
return false;
}
}
Node* tmp = new Node(key);//申请新的节点
if (parent->_key > key)//通过比较节点的值和插入的key值来确认插入位置
parent->_left = tmp;
else
parent->_right = tmp;
return true;
}
这里采用双指针法来插入,用parent指针来保存cur的父节点,最后通过判断插入key值。
1.3二叉搜索树的删除
二叉搜索树的删除要分几种情况进行,如果只剩下根节点,删除根节点,然后将根节点置空就好了。如图:
如果删除的是叶子节点,判断它在父节点的左边还是右边,将父节点的左边或者右边置空就行了。然后进行删除。
如果删除的节点它的左子树为空,只需要判断父节点和它的关系然后将它的右子树链接到父节点的左边或者右边,再删除它就行了。
如果删除的节点它的右子树为空,只需要判断父节点和它的关系然后将它的左子树链接到父节点的左边或者右边,再删除它就行了。(同上面一样这里就不做过多解释)
如果左右节点都不为空,就需要找它的右边子树的最小节点替换它的值然后进行删除就行了,但是需要注意如果最小节点左子树如果为空,就需要判断一下,然后将最小节点的父节点与最小节点的父亲节点链接起来,再删除最小节点。
代码:
bool Erase(const T& key)//查找并删除key值
{
if (_root == nullptr)//如果根节点为空不需要查找
return false;
Node* cur = _root;
Node* parent = cur;//用来保存cur的父亲节点的值
if (cur->_key == key && cur->_left == nullptr && cur->_right == nullptr)//左右子树为空才相等,且要key的值等于当前节点的值。
{
delete cur;//删除根节点
_root = nullptr;
return true;
}
//查找并删除采用双指针的方法
while (cur)//遍历查找并删除val值的节点
{
if (cur->_key > key)//在左子树中找
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)//在右子树中查找
{
parent = cur;
cur = cur->_right;
}
else//找到含key值的节点
{
//删除节点
if (cur->_left == nullptr)//如果左子树为空
{
if (parent->_key > cur->_key)//判断父亲节点和要删除的节点的关系,
parent->_left = cur->_right;//断开节点的链接
else
parent->_right = cur->_right;
delete cur;//删除节点
cur = nullptr;
return true;
}
else if (cur->_right == nullptr)//如果右子树都为空
{
if (parent->_key > cur->_key)//判断父亲节点和要删除的节点的关系,
parent->_left = cur->_left;//断开节点的链接
else
parent->_right = cur->_left;
delete cur;//删除节点
cur = nullptr;
return true;
}
else //左右孩子都存在
{
Node* tmp = cur->_right;//如果要删除的节点的左右孩子都存在的话,就要考虑,找到这个节点的左子树中val值做大的节点
//或者右子树中val值最小的节点,交换它们的值,然后删除左子树中这个最大的值的节点,或者右子树中最小的值的这个节点
//采用双指针法来保存右子树中的最小值节点的父节点
Node* tmpParent = nullptr;
while (tmp->_left)//在节点的右子树中找最左节点,也就是右子树中节点值最小的节点
{
tmpParent = tmp;//保存父亲节点
tmp = tmp->_left;//右子树中的最左孩子
}
cur->_key = tmp->_key;//用右子树中节点的最小值来替换要删除的节点
if (tmpParent == nullptr)//说明要删除的节点的右节点就是要替换的节点
{
cur->_right = tmp->_right;//最小值所在的节点的右节点不为空链接cur和最小节点的右节点
}
else
{
if (tmpParent->_key > tmp->_key)//判断最小节点和它父节点的关系,它在父节点的左边还是右边
tmpParent->_left = nullptr;
else
tmpParent->_right = nullptr;
}
delete tmp;//删除这个替换的节点,也就是右子树中最小的节点
tmp = nullptr;
return true;
}
}
}
return false;
}
1.4Inorder
二叉搜索树的中序遍历不能直接实现,需要借助,一个子函数来调用实现,因为在类的外面是无法将根节点传给函数的(根节点是类中的私有成员)。
代码:
void _Inorder(Node*root)//二叉搜索树的中序遍历
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_key<<" ";
_Inorder(root->_right);
}
void Inorder()
{
_Inorder(_root);//调用中序遍历,进行遍历因为如果直接写在类的外面获取不到根节点的值就无法进行传参,所以这要写一个中序遍历的子函数,传参调用进行中序遍历
cout << endl;
}
3.二叉搜索树的应用
3.1K模型
K模型即只有Key作为关键码,结构中只需要存储K就行了,关键码即为要搜索的值。
比如火车站进站时刷身份证就是判断你是不是今天在这个车站乘车。
具体方法如下:通过某天车站的购票身份证号建立一颗二叉搜索树。
在二叉搜索树中检索身份证号是否存在来判断当日是否在这里乘车。
3.2KV模型
每个关键码K都有其对应的值Value ,即<Key,Value>的键值对。该种方法在现实中非常常见。比如:在图书馆中通过图书编号检索图书的信息。通过图书的编号就可以快速找到所需要的的图书的信息了。图书检索中图书编号和对应的图书信息就构成键值对。
在比如英汉词典通过英文去查询汉语意思,英文单词和对应的中文就构成一种键值对。
例如:
//改造的二叉搜索树 KV模型
#include<string>
#include<vector>
#include<iostream>
using namespace std;
template<class K,class V>
struct BSTNode//节点的定义
{
BSTNode(const K& key = K(),const V&value =V())//构造函数初始化节点
:_left(nullptr)
, _right(nullptr)
, _key(key)
,_value(value)
{ }
BSTNode<K,V>* _left;
BSTNode<K,V>* _right;//
K _key;
V _value;
};
template<class K,class V>
class BSTree
{
public:
typedef BSTNode<K,V> node;
BSTree()//构造函数初始化根节点为空
:_root(nullptr)
{ }
bool Insert(const K& key,const V&value)//插入节点
{
if (_root == nullptr)//树为空
{
//初始化根节点
_root = new node(key,value);
return true;
}
//树不为空,插入节点,按照左小右大
node* cur = _root;
node* parent = cur;//用来保存上一层的节点,便于插入
while (cur)
{
if (cur->_key > key)//走左边
{
parent = cur;
cur = cur->_left;
}
else if (cur->_key < key)//走右边
{
parent = cur;
cur = cur->_right;
}
else//key值重复插入失败直接退出
{
return false;
}
}
//判断key值应该插在左边还是右边
node* newnode = new node(key,value);
if (parent->_key > key)//插在左边
parent->_left = newnode;
else
parent->_right = newnode;
return true;
}
//删除节点与节点的位置有很大的关系
//如果是删除末尾的节点直接删除就好
//如果删除的节点缺少左子树或者右子树,只需要记录另外一个子树的连接然后删除节点之后重新连接就好了
//如果删除的节点左右子树都存在,就要找到一个能够替换它的节点,然后交换数据删除替换的节点就好了
bool erase(const K& key)//删除节点
{
if (_root == nullptr)//树为空
return false;
node* cur = _root;
node* parent = cur;
if (cur->_left == nullptr && cur->_right == nullptr)//左右子树为空才相等
{
delete cur;//删除根节点
_root = nullptr;
return true;
}
while (cur)//查找并删除key节点的值
{
if (cur->_key > key)//走左边
{
parent = cur;//记录cur的上一层节点
cur = cur->_left;
}
else if (cur->_key < key)//走右边找
{
parent = cur;
cur = cur->_right;
}
else//找到节点
{
//删除节点
if (cur->_left == nullptr)//左节点为空
{
if (cur->_key > parent->_key)
parent->_right = cur->_right;//cur是parent的右子树
else
parent->_left = cur->_right;;//链接cur的右子树
delete cur;
}
else if (cur->_right == nullptr)//右子树为空
{
if (cur->_key > parent->_key)//cur是parent的右子树
parent->_right = cur->_left;
else
parent->_left = cur->_left;
delete cur;
}
else//左右子树都不为空
{
//找到右子树的最左节点然后交换 key 值
node* tmp = cur->_right;
node* parentTmp = cur;
while (tmp->_left)
{
parentTmp = tmp;//保存tmp的上一层
tmp = tmp->_left;
}
cur->_key = tmp->_key;
if (cur->_right == tmp && tmp->_right == nullptr)//说明cur的右子树仅仅只有一个节点
{
cur->_right = nullptr;
}
else
{
if (tmp->_key >= parentTmp->_key)//parentTmp的右节点置空
parentTmp->_right = tmp->_right;
else
parentTmp->_left = tmp->_right;//左节点置空
}
delete tmp;//删除tmp
}
return true;
}
}
return false;
}
node* Find(const K& key)
{
if (_root == nullptr)//树为空
return nullptr;
node* cur = _root;
while (cur)
{
if (cur->_key > key)//去左边找
cur = cur->_left;
else if (cur->_key < key)//去右边找
cur = cur->_right;
else//找到了
return cur;
}
return nullptr;
}
void _InOrder(node* root)//中序遍历
{
//node* root = _root;
if (root == nullptr)
return;//递归结束
_InOrder(root->_left);//左子树
cout << root->_key << " ";
_InOrder(root->_right);//右子树
}
void InOrder()//中序遍历无法直接遍历节点,因为this指针是隐含的无法显示调用必须借助于其他子函数
{
_InOrder(_root);
cout << endl;
}
private:
node* _root;//根节点
};
void Test()//英汉词典模型
{
BSTree<string, string> Bt;
Bt.Insert("sort", "排序");
Bt.Insert("function", "函数");
Bt.Insert("English", "英文");
Bt.Insert("china", "中国");
BSTNode<string,string>*p=Bt.Find("sort");
if (p)//指针不为空
cout<< p->_value<<endl;
//Bt.InOrder();
}
void Test1()//统计物品出现的次数
{
BSTree<string,int> Bt;
string s1[] = { "西瓜" ,"黄瓜" , "西瓜" , "黄瓜" , "西瓜" , "香蕉" , "西瓜" , "苹果" , "草莓" , "西瓜" , "冬瓜" , "西瓜" , "西瓜" , "冬瓜" };
for (auto& e : s1)
{
BSTNode<string, int>* p =Bt.Find(e);
if (p==nullptr)
{
Bt.Insert(e,1);//如果第一次插入Value值等于1
}
else
{
p->_value++;//不是第一次插入Value值++
}
}
BSTNode<string, int>* p = Bt.Find("西瓜");
if (p)
cout <<"西瓜:"<< p->_value;
}
4.二叉搜索树的缺点
如果插入的数据是有序或者接近有序的,那么二叉搜索树就是一条直线或者近似是直线,它就只有右子树或者左子树,这时候它的查找效率就会很慢是O(N),如果是无序的数据插入到二叉搜索树中,它查找的效率就会很高,只需要做多它的高度次,也就是O(logN);所以它也是有一定的缺陷的,而map和set就是通过解决它的缺陷从而实现高效率的查找的。
写的不好的地方希望指正。