目录
1.二叉搜索树的概念
2.二叉搜索树的性能分析
3.二叉搜索树的结构和中序遍历
3.1二叉搜索树中节点的结构
3.2二叉搜索树的结构
3.3中序遍历
4.二叉搜索树的插入
5.二叉搜索树的查找
6.二叉树搜索树的删除
7. 二叉搜索树的默认成员函数
8.参考代码
9.二叉搜索树key和key/value使用场景
9.1key搜索场景
9.2key/value搜索场景
1.二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
(1)若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
(2)若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
(3)它的左右子树也分别为二叉搜索树。
(4)二叉搜索树可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义。如下图,左边是不支持插入相等的值,右边支持插入相等的值。
2.二叉搜索树的性能分析
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:。最差情况下,二叉树退化为单支树(或者类似单支),其高度为:。所有综合而言二叉搜索树增删查的时间复杂度为:。
另外需要说明的是,二分查找也可以实现级别的查找效率,但是二分查找有两个大的缺陷:
(1)需要存储在支持下标随机访问的结构中,并且有序。
(2)插入和删除数据效率很低,因为存储在下标随机访问的结构(例如数组)中,插入和删除数据一般需要挪动数据。
所以二叉搜索树就能体现出价值所在,即通过二叉树中序遍历就能有序的访问数据,并且插入和删除都是节点的操作,不需要挪动数据。二叉搜索树的查找效率为,但是这个缺陷在后续的平衡二叉树AVL树已经红黑树中可以解决,将效率提高到。
3.二叉搜索树的结构和中序遍历
3.1二叉搜索树中节点的结构
_key为节点中的值,_left和_right分别是指向左节点和右节点的指针。
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
3.2二叉搜索树的结构
二叉树的结构中只用一个_root成员。
template<class K>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K>;
private:
Node* _root = nullptr;
};
3.3中序遍历
因为二叉搜索树左边比根节点小,右边比根节点大,中序遍历的顺序是:左子树,根节点,右子树,所以通过中序遍历遍历二叉搜索树,天然就是有序的。
中序遍历需要传一个根节点,在类外面不能直接访问私有成员,所以这里的中序遍历实现在private里面,然后外面再用InOrder()函数进行封装,这里在类外面直接调用InOrder()函数就可以实习中序遍历了,不需要传入根节点。
template<class K>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K>;
public:
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;
};
4.二叉搜索树的插入
插入的具体过程如下:
(1)树为空,则直接新增节点,赋值给root。
(2)树不为空,按二叉搜索树的性质,插入值比当前节点大往右走,插入值比当前节点小往左走,找到空位置插入新节点。
(3)如果支持插入相等的值,插入值跟当前节点相等的值可以往右走,也可以往左走,找到空位置插入新节点。(要注意保持逻辑的一致性,插入相等的值不要一会往右走,一会往左走)
这里以插入下列这棵树为例子:
1.不允许相同的值插入
//不允许相同的值插入
bool Insert(const K& key) //K是插入的值的类型
{
if (_root == nullptr) //当为空树时,直接赋值给root
{
_root = new Node(key);
return true;
}
Node* parent = nullptr; //parent指向cur的父亲节点
Node* cur = _root;
while (cur)
{
if (cur->_key < key) //当插入值key大于cur节点的值,往右走
{
parent = cur;
cur = cur->_right;
}
else if (cur->_key > key) //当插入值key小于cur节点的值,往左走
{
parent = cur;
cur = cur->_left;
}
else //插入相等的值时,插入失败
{
return false;
}
}
//此时找到空位置了,cur == nullptr, parent指向cur的父亲节点,进行插入
cur = new Node(key);
if (parent->_key < key) //如果插入值大于parent节点的值,插入在parent的右边
{
parent->_right = cur;
}
else //如果插入值小于parent节点的值,插入在parent的左边
{
parent->_left = cur;
}
return true;
}
2.允许相同的值插入
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
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;
}
}
cur = new Node(key);
if (parent->_key <= key) //相同的值插入到parent的右边
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
实现上述二叉树的插入(不允许相同的值插入):
int main()
{
key::BSTree<int> t;
int a[] = { 8,3,1,10,1,6,4,7,14,13 };
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
t.Insert(16);
t.InOrder();
return 0;
}
5.二叉搜索树的查找
(1)从根节点开始,查找x,x比根的值大则往右边走继续查找,反之,往左边走继续查找。
(2)最多查找高度次,走到空还没找到,这个值不存在。
(3)如果不支持插入相等的值,找到x即可返回。
(4)如果支持插入相等的值,意味着有多个值为x的节点存在,一般要求查找中序的第一个x。如下图查找3,要找到1的右孩子的那个3进行返回。
查找的实现其实在插入中就已经实现了,插入中的第一个步骤就是要先进行查找,看是否有相同的值。 这里实现的是没用相同的值的查找,找到了直接返回true。
bool 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 true;
}
}
return false;
}
6.二叉树搜索树的删除
首先查找元素是否在二叉树中,如果不存在,则返回false。如果元素存在则存在以下四种 情况(假设要删除的节点为N):
(1)N的左右孩子均为空。
(2)N的左孩子为空,右孩子不为空。
(3)N的右孩子为空,左孩子不为空。
(4)N的左右孩子均不为空。
上述四种情况,对应进行分别处理:
(1)把N节点的父亲节点指向N节点的指针置为nullptr,然后直接删除N节点(情况1可以当成2或者3处理)
(2)把N节点的父亲节点指向N节点的指针指向N的右孩子,直接删除N节点。
(3)把N节点的父亲节点指向N节点的指针指向N的左孩子,直接删除N节点。
(4)无法直接删除N节点,因为直接删除N节点之后,左右孩子与这棵树就断掉了,只能用替换法删除。找到N左子树中值最大的节点R(即左子树的最右节点)或者N右子树中值最小的节点R(即右子树的最左节点)替代N,因为这两个节点替代N之后,二叉树左子树小于根右子树大于根的性质没有被破坏。替代N的意思是交换N和R两个节点的值,转而删除R节点,这样R节点就是上述情况1,2,3中的一种,直接删除即可。
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
{
//删除
//1. 左为空
if (cur->_left == nullptr)
{
//处理删除的节点是_root的情况
if (cur == _root)
{
_root = cur->_right;
}
else
{
//如果cur是parent的左节点,让parent的左节点指向cur的右节点
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr) //2. 右为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else //3. 左右都不为空
{
//replace可以是右子树的最左节点或者左子树的最右节点
//这里的replaceParent直接给cur是为了处理删除cur时cur右子树的根就是右
//子树的最左节点,此时就不会进入下列循环,replaceParent就没有更新
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
//这里如果右子树的根节点就是右子树的最左节点时,这种情况下对于
//replaceParent来说,replace就不是replaceParent的左孩子了
//replace就变成了上述左为空的情况了,这时就需要判断replace是
//replaceParent的左孩子还是右孩子了
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
return true;
}
}
return false;
}
int main()
{
key::BSTree<int> t;
int a[] = { 8,3,1,10,1,6,4,7,14,13 };
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
for (auto e : a)
{
t.Erase(e);
t.InOrder();
}
return 0;
}
7. 二叉搜索树的默认成员函数
这里的拷贝构造和析构都实现在private里面,在public里面进行封装,和中序遍历的实现一样。拷贝构造的实现就是通过递归的方式前序遍历原二叉搜索树,然后每遍历一个节点就复制一个节点。赋值运算符的实现和之前STL容器的赋值运算符实现类似。析构函数的实现是通过一个后序遍历对二叉树进行一个节点一个节点的释放。
template<class K>
class BSTree
{
public:
BSTree() {}
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
BSTree& operator=(BSTree tmp)
{
swap(_root, tmp._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
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;
}
};
8.参考代码
//BinarySearchTree.h
#pragma once
namespace key
{
template<class K>
struct BSTNode
{
K _key;
BSTNode<K>* _left;
BSTNode<K>* _right;
BSTNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K>;
public:
BSTree() {}
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
//传值传参是一种拷贝,会调用拷贝构造,构造出一个临时对象
BSTree& operator=(BSTree tmp)
{
swap(_root, tmp._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
//不允许相同的值插入
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
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);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
//允许相等的值插入,下面写的逻辑是相等的值插入root的右边
//bool Insert(const K& key)
//{
// if (_root == nullptr)
// {
// _root = new Node(key);
// 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;
// }
// }
// cur = new Node(key);
// if (parent->_key <= key)
// {
// parent->_right = cur;
// }
// else
// {
// parent->_left = cur;
// }
// return true;
//}
bool 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 true;
}
}
return false;
}
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
{
//删除
//1. 左为空
if (cur->_left == nullptr)
{
//处理删除的节点是_root的情况
if (cur == _root)
{
_root = cur->_right;
}
else
{
//如果cur是parent的左节点,让parent的左节点指向cur的右节点
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr) //2. 右为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else //3. 左右都不为空
{
//replace可以用右子树的最左节点或者左子树的最右节点
//这里的replaceParent直接给cur是为了处理删除cur时cur右子树的根就是右子树的最左节点,此时就不会进入下列循环,replaceParent就没有更新
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
//这里如果右子树的根节点就是右子树的最左节点时,这种情况下对于replaceParent来说,replace就不是replaceParent的左孩子了
//replace就变成了上述左为空的情况了,这时就需要判断replace是replaceParent的左孩子还是右孩子了
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
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;
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
Node* _root = nullptr;
};
}
9.二叉搜索树key和key/value使用场景
9.1key搜索场景
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的⼆叉树搜索树⽀持增删查,但是不⽀持修改,修改key破坏搜索树结构了。
场景1:⼩区⽆⼈值守⻋库,⼩区⻋库买了⻋位的业主⻋才能进⼩区,那么物业会把买了⻋位的业主的⻋牌号录⼊后台系统,⻋辆进⼊时扫描⻋牌在不在系统中,在则抬杆,不在则提⽰⾮本⼩区⻋辆,⽆法进⼊。
场景2:检查⼀篇英⽂⽂章单词拼写是否正确,将词库中所有单词放⼊⼆叉搜索树,读取⽂章中的单词,查找是否在⼆叉搜索树中,不在则波浪线标红提⽰。
9.2key/value搜索场景
每⼀个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字⾛⼆叉搜索树的规则进⾏⽐较,可以快速查找到key对应的value。key/value的搜索场景实现的⼆叉树搜索树⽀持修改,但是不⽀持修改key,修改key破坏搜索树结构了,可以修改value。
场景1:简单中英互译字典,树的结构中(结点)存储key(英⽂)和vlaue(中⽂),搜索时输⼊英⽂,则同时查找到了英⽂对应的中⽂。
场景2:商场⽆⼈值守⻋库,⼊⼝进场时扫描⻋牌,记录⻋牌和⼊场时间,出⼝离场时,扫描⻋牌,查找⼊场时间,⽤当前时间-⼊场时间计算出停⻋时⻓,计算出停⻋费⽤,缴费后抬杆,⻋辆离场。
场景3:统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。
这里给出一个key/value二叉搜索树的代码仅供参考,与key场景的代码相似,就是多存储了一个value值:
namespace key_value
{
template<class K, class V>
struct BSTNode
{
K _key;
V _value;
BSTNode<K, V>* _left;
BSTNode<K, V>* _right;
BSTNode(const K& key, const V& value)
:_key(key)
,_value(value)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K, class V>
class BSTree
{
//typedef BSTNode<K> Node;
using Node = BSTNode<K, V>;
public:
BSTree() {}
BSTree(const BSTree& t)
{
_root = Copy(t._root);
}
BSTree& operator=(BSTree tmp)
{
swap(_root, tmp._root);
return *this;
}
~BSTree()
{
Destroy(_root);
_root = nullptr;
}
//不允许相同的值插入
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;
}
//允许相等的值插入,下面写的逻辑是相等的值插入root的右边
/*bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
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;
}
}
cur = new Node(key);
if (parent->_key <= key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}*/
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;
}
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
{
//删除
//1. 左为空
if (cur->_left == nullptr)
{
//处理删除的节点是_root的情况
if (cur == _root)
{
_root = cur->_right;
}
else
{
//如果cur是parent的左节点,让parent的左节点指向cur的右节点
if (parent->_left == cur)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
}
else if (cur->_right == nullptr) //2. 右为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_left == cur)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
}
else //3. 左右都不为空
{
//replace可以用右子树的最左节点或者左子树的最右节点
//这里的replaceParent直接给cur是为了处理删除cur时cur右子树的根就是右子树的最左节点,此时就不会进入下列循环,replaceParent就没有更新
Node* replaceParent = cur;
Node* replace = cur->_right;
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
cur->_key = replace->_key;
//这里如果右子树的根节点就是右子树的最左节点时,这种情况下对于replaceParent来说,replace就不是replaceParent的左孩子了
//replace就变成了上述左为空的情况了,这时就需要判断replace是replaceParent的左孩子还是右孩子了
if (replaceParent->_left == replace)
replaceParent->_left = replace->_right;
else
replaceParent->_right = replace->_right;
delete replace;
}
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newRoot = new Node(root->_key, root->_value);
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;
}
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
Node* _root = nullptr;
};
}