目录
[1. 概念](#1. 概念)
[2. 二叉搜索树操作](#2. 二叉搜索树操作)
[2.1 基础结构](#2.1 基础结构)
[2.2 非递归版](#2.2 非递归版)
[1. 查找](#1. 查找)
[2. 插入](#2. 插入)
[3. 删除](#3. 删除)
[2.3 递归版](#2.3 递归版)
[1. 查找](#1. 查找)
[2. 插入](#2. 插入)
[3. 删除](#3. 删除)
[2.4 拷贝构造函数](#2.4 拷贝构造函数)
[2.5 赋值运算符重载](#2.5 赋值运算符重载)
[2.6 析构函数](#2.6 析构函数)
[2.7 完整代码](#2.7 完整代码)
[3. 二叉搜索树的应用](#3. 二叉搜索树的应用)
[4. 二叉搜索树的性能](#4. 二叉搜索树的性能)
二叉搜索树
1. 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
由于二叉搜索树的性质,该树的中序遍历就是递增序列
2. 二叉搜索树操作
2.1 基础结构
cpp
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
BSTree()
:_root(nullptr)
{}
private:
Node* _root;
};
2.2 非递归版
1. 查找
- key小于cur->_key,则往左子树找
- key大于cur->_key,则往右子树找
cpp
bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (cur->_key > key)
{
cur = cur->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
else
{
return true;
}
}
return false;
}
2. 插入
- 插入要先找到合适的空位
- 如果已经之前已经存在,那么return false
- 找到空位之后,new出一个新节点,在链接时我们要获取parent节点,这就需要我们提前记录parent节点
- 链接时还要判断 key 与 parent->_key大小关系,因为我们找到了合适的位置,但是没有记录是在左还是在右
cpp
bool Insert(const K& key)
{
//如果树为空,直接new一个根节点
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;
}
}
//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
cur = new Node(key);
if (parent->_key < key)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
return true;
}
3. 删除
- 先查找该节点是否存在,如果存在,再看以下几点
- **删除节点情况分三种:**该节点左孩子为空、该节点右孩子为空、该节点左右孩子都有
- **左孩子为空:**将右节点链接到parent上即可,此时还要判断cur在parent的哪一边,如果cur在parent的右边,那么cur的所有孩子必然比parent的key大,所以将cur的right链接到parent的right即可
- **右孩子为空:**同理
- **左右孩子都存在:**此时我们可以找该节点左子树的最右节点,或右子树的最左节点,这两个节点都是最接近根节点key值的节点(因为二叉搜索树的性质,当节点无穷时,这两个节点的key值将从左和从右边无限趋近于根节点的key值),找到节点后与根节点交换key值,此时如果是左子树的最右节点,那么该节点是绝不可能存在右子树,所以此时将该节点的左子树链接到parent节点即可(提前记录parent,在判断cur在parent的哪一边)
- 最后不要忘了delete被删除的节点
cpp
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 (cur == _root)
{
_root = cur->_right;
}
else //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
{
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
}
else if (cur->_right == nullptr) //该节点右为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else //该节点左右都不为空
{
Node* parent = cur;
Node* leftMax = cur->_left;
while (leftMax->_right)
{
parent = leftMax;
leftMax = leftMax->_right;
}
swap(leftMax->_key, cur->_key);
if (parent->_left == leftMax)
{
parent->_left = leftMax->_left;
}
else
{
parent->_right = leftMax->_left;
}
cur = leftMax;
}
delete cur;
return true;
}
}
return false;
}
2.3 递归版
由于用户不传递root参数,所以FindR内层封装一个获取root参数的函数,其他函数同理
1. 查找
- 如果找不到返回false,找到了返回true。比key小,往右子树递归找;比key大,往左子树递归找。
cpp
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
{
return true;
}
}
2. 插入
- insert函数神之一手的地方就是参数类型 Node*&,当它递归下去时 root 其实是它的父节点的 left 或 right 的引用!它可以不用提前保存parent的信息,直接链接新节点!
cpp
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
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
{
//已经有该节点了,返回false
return false;
}
}
3. 删除
- 先找到要删除的节点,然后再提前保存该节点,为了后续的delete
- 同样的,erase的参数也是Node*&,它极大方便了节点的链接
- 找到节点后,如果该节点的左节点为空,那么直接root = root->_right;如果右节点为空,那么 root = root->_left,这就是Node* &的强大之处。那么可能有疑问,非递归版本为什么不能用引用?循环版本不能使用引用,是因为引用不能改变指向!递归可以使用引用是因为每次都是一个新的栈帧
- 如果左右节点都存在,找左子树最右节点,交换key值,再erase掉key的节点(注意,不能从root开始找,因为root此时已经被交换key值了,递归会往右边去找,这就会导致找错了
- 最后Erase()这里不能传leftMax->_left,因为leftMax是局部变量,最后子节点会链接不上真正的root
cpp
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->_right, key);
}
else if (root->_key < 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* leftMax = root->_left;
while (leftMax->_right)
{
leftMax = leftMax->_right;
}
swap(leftMax->_key, root->_key);
//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
2.4 拷贝构造函数
- 根据前序序列递归拷贝
- 该递归就是从最左边开始链接,再从最底层往上
cpp
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
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;
}
2.5 赋值运算符重载
- 现代写法
cpp
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
2.6 析构函数
- 递归式析构
cpp
~BSTree()
{
Destory(_root);
}
void Destory(Node*& root)
{
if (root == nullptr)
return;
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
2.7 完整代码
cpp
namespace key
{
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_key(key)
, _left(nullptr)
, _right(nullptr)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
BSTree()
:_root(nullptr)
{}
BSTree(const BSTree<K>& t)
{
_root = Copy(t._root);
}
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
~BSTree()
{
Destory(_root);
}
bool Insert(const K& key)
{
//如果树为空,直接new一个根节点
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;
}
}
//由于不知道插入的节点与父亲的key大小关系,所以再判断一下是插到左边还是右边
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->_left;
}
else if (cur->_key < key)
{
cur = cur->_right;
}
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 //找到节点了
{
//该节点左为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else //再看看cur在父节点parent的哪一边,在哪一边就把孤儿链接到哪一边
{
if (parent->_right == cur)
{
parent->_right = cur->_right;
}
else
{
parent->_left = cur->_right;
}
}
}
else if (cur->_right == nullptr) //该节点右为空
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (parent->_right == cur)
{
parent->_right = cur->_left;
}
else
{
parent->_left = cur->_left;
}
}
}
else //该节点左右都不为空
{
Node* parent = cur;
Node* leftMax = cur->_left;
while (leftMax->_right)
{
parent = leftMax;
leftMax = leftMax->_right;
}
swap(leftMax->_key, cur->_key);
if (parent->_left == leftMax)
{
parent->_left = leftMax->_left;
}
else
{
parent->_right = leftMax->_left;
}
cur = leftMax;
}
delete cur;
return true;
}
}
return false;
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
private:
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;
}
void Destory(Node*& root)
{
if (root == nullptr)
return;
Destory(root->_left);
Destory(root->_right);
delete root;
root = nullptr;
}
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
return false;
if (root->_key < key)
{
return _EraseR(root->_right, key);
}
else if (root->_key < 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* leftMax = root->_left;
while (leftMax->_right)
{
leftMax = leftMax->_right;
}
swap(leftMax->_key, root->_key);
//这里不能传leftMax->_left,因为是别名,leftMax是局部变量,最后子节点会链接不上真正的root
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
//Node*& root,这里的引用是神之一手,因为在连接的时候可以不用再去记录parent
//在链接的时候,那个root是引用的父亲的root->_right或root->_left !!
bool _InsertR(Node*& root, const K& key)
{
if (root == nullptr)
{
//这个root是引用的父亲的root->_right或root->_left,这一步是直接链接了!
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
{
//已经有该节点了,返回false
return false;
}
}
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
{
return true;
}
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root;
};
void TestBSTree1()
{
int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
BSTree<int> t;
for (auto e : a)
{
t.Insert(e);
}
t.InOrder();
t.EraseR(4);
t.InOrder();
t.EraseR(6);
t.InOrder();
t.EraseR(7);
t.InOrder();
t.EraseR(3);
t.InOrder();
for (auto e : a)
{
t.Erase(e);
}
t.InOrder();
}
}
3. 二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key****即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- KV模型:每一个关键码key**,都有与之对应的值Value,即****<Key, Value>的键值对。该种方式在现实生活中非常常见:比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>**就构成一种键值对。
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
- <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较
- Key查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
cpp
namespace key_value
{
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;
}
Node* FindR(const K& key)
{
return _FindR(_root, key);
}
bool InsertR(const K& key, const V& value)
{
return _InsertR(_root, key, value);
}
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->_right, key);
}
else if (root->_key > key)
{
return _EraseR(root->_left, key);
}
else
{
Node* del = root;
// 1、左为空
// 2、右为空
// 3、左右都不为空
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* leftMax = root->_left;
while (leftMax->_right)
{
leftMax = leftMax->_right;
}
swap(root->_key, leftMax->_key);
return _EraseR(root->_left, key);
}
delete del;
return true;
}
}
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 false;
}
}
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;
}
}
void _InOrder(Node* root)
{
if (root == NULL)
{
return;
}
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
private:
Node* _root;
};
void TestBSTree1()
{
//BSTree<string, Date> carTree;
BSTree<string, string> dict;
dict.InsertR("insert", "插入");
dict.InsertR("sort", "排序");
dict.InsertR("right", "右边");
dict.InsertR("date", "日期");
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.FindR(str);
if (ret)
{
cout << ret->_value << endl;
}
else
{
cout << "无此单词" << endl;
}
}
}
void TestBSTree2()
{
// 统计水果出现的次数
string arr[] = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
BSTree<string, int> countTree;
for (auto& str : arr)
{
auto ret = countTree.FindR(str);
if (ret == nullptr)
{
countTree.InsertR(str, 1);
}
else
{
ret->_value++;
}
}
countTree.InOrder();
}
}
相比只有key版本,key_value只是在结构体内多加了value而已,部分函数原理也没有变
4. 二叉搜索树的性能
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树: