二叉搜索树
二叉搜索树概念
二叉搜索树 (Binary Search Tree,BST)是一种二叉树的特殊形式,它具有以下关键性质:
- 有序性:每个节点都包含一个值,且左子树中的所有节点的值都小于等于该节点的值,右子树中的所有节点的值都大于等于该节点的值。这意味着对于任何节点,它的左子树都包含比它小的值,右子树都包含比它大的值。
- 递归性质:整棵二叉搜索树本身也是一个二叉搜索树,因为它的左子树和右子树也满足二叉搜索树的性质。这意味着可以递归地应用相同的规则来构建和操作树。
- 无重复值 :通常,
BST
中不允许包含重复的值。每个值在树中只能出现一次。
由于这些性质,二叉搜索树在很多应用中都非常有用,尤其是在需要高效地进行搜索、插入和删除操作的情况下。
以下是一些关于二叉搜索树的详细解释:
- 插入操作 :
- 插入操作从根节点开始,根据节点值的大小逐步向下搜索树,直到找到一个合适的位置插入新节点。
- 如果要插入的值小于当前节点的值,就继续在左子树中搜索,否则在右子树中搜索,直到找到一个空位置插入新节点。
- 搜索操作 :
- 搜索操作也从根节点开始,根据节点值的大小逐步向下搜索树。
- 如果要搜索的值等于当前节点的值,就找到了目标节点。
- 如果要搜索的值小于当前节点的值,就在左子树中搜索;如果大于当前节点的值,就在右子树中搜索。
- 如果一直搜索到叶子节点仍然没有找到目标值,则说明目标值不在树中。
- 删除操作 :
- 删除操作通常比较复杂,因为需要考虑多种情况。删除节点时有以下几种情况:
- 被删除节点是叶子节点(没有子节点):直接删除即可。
- 被删除节点有一个子节点:将子节点替换为被删除节点的位置。
- 被删除节点有两个子节点:找到被删除节点的中序遍历前驱或后继节点,用其值替换被删除节点的值,然后递归删除前驱或后继节点。
- 删除操作通常比较复杂,因为需要考虑多种情况。删除节点时有以下几种情况:
- 遍历操作 :
- 二叉搜索树可以通过不同的遍历方式进行访问,包括中序遍历、前序遍历和后序遍历。
- 中序遍历可以按照升序顺序遍历树中的所有节点,因为它首先遍历左子树,然后当前节点,最后右子树。
- 平衡性 :
- 当二叉搜索树的高度不平衡时,搜索、插入和删除操作的时间复杂度可能会退化为
O(n)
,其中n
是节点数。为了避免这种情况,通常使用平衡二叉搜索树(如AVL树或红黑树)来确保树的高度保持在较小的范围内。
- 当二叉搜索树的高度不平衡时,搜索、插入和删除操作的时间复杂度可能会退化为
总之,二叉搜索树是一种基于节点值大小有序排列的二叉树,它支持高效的搜索、插入和删除操作,但需要注意平衡性问题以避免性能退化。
二叉搜索树操作
cpp
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
1.二叉搜索树的查找
在二叉查找树b中查找x的过程为:
- 若b是空树,则搜索失败,否则:
- 若x等于b的根节点的数据域之值,则查找成功;否则:
- 若x小于b的根节点的数据域之值,则搜索左子树;否则:
- 查找右子树
2.二叉搜索树的插入
向一个二叉搜索树b中插入一个节点s的算法,过程为:
- 若b是空树,则将s所指节点作为根节点插入,否则:
- 若
s->data
等于b的根节点的数据域之值,则返回,否则: - 若
s->data
小于b的根节点的数据域之值,则把s所指节点插入到左子树中,否则: - 把s所指节点插入到右子树中。(新插入节点总是叶子节点)
3.二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除
4.二叉搜索树的遍历
二叉搜索树的遍历最常用的是中序遍历,因为二叉搜索树(BST)的中序遍历结果是升序的。中序遍历是一种遍历二叉树的方式,按照以下规则进行:
- 首先遍历左子树。
- 然后遍历当前节点。
- 最后遍历右子树。
由于BST的性质,即左子树的值都小于当前节点的值,右子树的值都大于当前节点的值,所以中序遍历的过程中,首先访问左子树,然后访问当前节点,最后访问右子树。这导致中序遍历结果是按照升序排列的。
因此,中序遍历是一种有助于获取BST中所有节点按升序排列的有效方法。这个性质也是BST在进行搜索操作时能够快速定位目标值的关键之一。
二叉搜索树的实现
1.二叉搜索树节点结构
cpp
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
_left
:指向左子节点的指针。_right
:指向右子节点的指针。_key
:存储节点的关键字或值。
每个节点都有一个关键字 _key
,用来表示节点在二叉搜索树中的位置。这个关键字通常是用来比较节点的大小,以便将节点插入到正确的位置,以满足二叉搜索树的性质。根据节点关键字的大小,节点将被放置在左子树或右子树中。
这是一个模板类,它可以用于创建不同类型的二叉搜索树,只需指定不同的关键字类型 K
即可。这使得代码更加通用,可以适用于不同类型的数据。
2.二叉搜索树类
cpp
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
private:
Node* _root = nullptr;
}
二叉搜索树 (Binary Search Tree,BST )类模板 BSTree
的定义。这个类包含一个私有成员 _root
,它是指向树的根节点的指针。
template<class K>
:这是一个类模板声明,使用了模板参数K
,它表示关键字或值的数据类型。这允许你在创建BSTree
实例时指定不同的数据类型。typedef BSTreeNode<K> Node;
:这行代码用于定义一个别名Node
,它是BSTreeNode<K>
类的别名。这个别名使得在类中使用节点类型更加方便。Node* _root = nullptr;
:这是一个指向根节点的指针_root
,它初始化为nullptr
,表示树开始为空树。
3.二叉搜索树的构造及析构
构造函数
cpp
BSTree() = default;
BSTree() = default;
是C++中的默认构造函数的声明方式。这行代码表示你正在声明一个默认构造函数(无参数的构造函数),并使用 = default
表示使用默认的构造函数实现。
默认构造函数是一个特殊的构造函数,它在对象创建时被自动调用,不接受任何参数。默认构造函数的主要作用是初始化对象的成员变量,确保对象在创建后处于一个合法的初始状态。
使用 = default
表示你希望编译器生成默认的构造函数实现,而不需要自己编写构造函数的定义。这通常用于以下情况:
- 你的类不需要特殊的构造逻辑,只需要成员变量的默认初始化。
- 你想确保编译器生成默认构造函数,以便可以在不提供自定义构造函数的情况下创建对象。
这里需要注意的是在C++中,如果你提供了自定义的拷贝构造函数,编译器通常不会再自动生成默认的拷贝构造函数,所以我们需要再将默认构造自定义提供
析构函数
cpp
~BSTree()
{
_Destory(_root);
}
private:
void _Destory(Node*& root)
{
if (root == nullptr)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = nullptr;
}
~BSTree()
- 这是BST类的析构函数。析构函数在对象被销毁时自动调用,用于执行清理和释放资源的操作。
- 在这个析构函数中,它调用了私有辅助函数
_Destory(_root)
,传递根节点_root
作为参数,开始递归地销毁整棵树。
_Destory(Node*& root)
- 这是一个递归辅助函数,用于销毁二叉搜索树中的节点。
- 函数接受一个指向节点指针的引用作为参数,以便可以修改节点指针,将其设置为
nullptr
,以避免悬挂指针问题。 - 函数首先检查节点是否为空。如果节点为空,表示已经到达树的底部,函数直接返回,不执行任何操作。
- 如果节点不为空,函数递归地调用自己来销毁左子树和右子树。
- 然后,函数删除当前节点,释放节点的内存。
- 最后,将当前节点指针设置为
nullptr
,以确保不会再访问已释放的节点。
这样,当你销毁BST对象时,析构函数会从根节点开始递归地销毁整棵树,确保释放所有节点的内存,防止内存泄漏。这是一种正确释放资源的方式,以确保程序的健壮性。
4.二叉搜索树的拷贝构造及赋值重载
拷贝构造
cpp
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
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;
}
BSTree(const BSTree<K>& t)
- 这是拷贝构造函数,它用于创建一个新的BST对象,该对象是根据另一个BST对象
t
进行拷贝构造的。 - 在构造函数内部,它调用了私有的辅助函数
_Copy(t._root)
,传递t
的根节点_root
作为参数,从而创建了一个新的BST。
- 这是拷贝构造函数,它用于创建一个新的BST对象,该对象是根据另一个BST对象
_Copy(Node* root)
- 这是一个递归辅助函数,用于复制一棵二叉树。
- 如果传入的根节点
root
为空(nullptr
),则返回nullptr
,表示空树。 - 如果根节点不为空,函数首先创建一个新的节点
copyRoot
,并将其关键字设置为root
节点的关键字。 - 然后,函数递归地调用自己来复制左子树和右子树,并将它们分别赋值给
copyRoot->_left
和copyRoot->_right
。 - 最后,返回
copyRoot
,这样递归过程会构建出一棵与原始树相同结构和内容的树。
这样,拷贝构造函数 _Copy
会递归地复制整棵树,确保你创建的新BST对象与原始对象具有相同的结构和内容。这对于在不破坏原始树的情况下创建一个新树非常有用。
5.二叉搜索树插入
迭代写法
cpp
public:
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
是否为nullptr
)。如果树为空,它会创建一个新的根节点,该节点的关键字为key
,然后返回true
表示插入成功。 - 如果树不为空,函数会进入一个循环,用来搜索插入位置。循环中的代码执行以下操作:
- 如果当前节点的关键字小于
key
,则继续向右子树移动,将parent
设置为当前节点,以便稍后插入新节点到右子树。 - 如果当前节点的关键字大于
key
,则继续向左子树移动,同样将parent
设置为当前节点,以便稍后插入新节点到左子树。 - 如果当前节点的关键字等于
key
,则返回false
,表示不允许重复关键字的节点插入。
- 如果当前节点的关键字小于
- 一旦找到插入位置,函数创建一个新的节点
cur
,该节点的关键字为key
。 - 接着,根据
parent
节点的关键字与key
的比较结果,将新节点cur
插入到正确的位置。如果parent->_key < key
,则将cur
插入为parent
的右子节点,否则插入为parent
的左子节点。 - 最后,函数返回
true
,表示插入成功。
这个 Insert
函数实现了BST的插入操作,它确保新节点按照BST的有序性质被插入到正确的位置。如果关键字已经存在于树中,函数会返回 false
,表示插入失败(因为BST不允许重复关键字)。
递归写法
cpp
bool InsertR(const K& key)
{
return _InsertR(_root, key);
}
private:
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 InsertR(const K& key)
- 这是公有函数,用于在二叉搜索树中插入新节点。它首先调用私有递归函数
_InsertR
,并将根节点_root
和要插入的关键字key
作为参数传递给它。
- 这是公有函数,用于在二叉搜索树中插入新节点。它首先调用私有递归函数
bool _InsertR(Node*& root, const K& key)
- 这是私有递归辅助函数,用于在给定的二叉搜索树中插入新节点。
- 函数首先检查当前根节点
root
是否为空。如果为空,表示找到了插入位置,它会创建一个新节点并将其关键字设置为key
,然后将root
指向新节点,最后返回true
表示插入成功。 - 如果当前根节点
root
不为空,它会比较当前节点的关键字root->_key
与要插入的关键字key
:- 如果
root->_key < key
,则递归地在右子树中插入新节点,调用_InsertR(root->_right, key)
。 - 如果
root->_key > key
,则递归地在左子树中插入新节点,调用_InsertR(root->_left, key)
。 - 如果
root->_key == key
,表示已经存在相同关键字的节点,直接返回false
表示插入失败。
- 如果
6.二叉搜索树查找
迭代写法
cpp
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 Find(const K& key)
- 这是一个公有函数,用于查找二叉搜索树中是否存在指定关键字
key
。 - 函数首先初始化一个指针
cur
为根节点_root
,然后进入一个循环。 - 在循环中,它会不断根据当前节点的关键字与目标关键字
key
的比较结果来决定向左子树还是右子树移动:- 如果当前节点的关键字小于
key
,则说明要查找的关键字应该在当前节点的右子树中,所以将cur
指向右子节点cur->_right
。 - 如果当前节点的关键字大于
key
,则说明要查找的关键字应该在当前节点的左子树中,所以将cur
指向左子节点cur->_left
。 - 如果当前节点的关键字等于
key
,则表示找到了目标关键字,函数返回true
表示查找成功。
- 如果当前节点的关键字小于
- 循环会一直进行,直到
cur
指向空(nullptr
),这意味着整棵树都被搜索过了,但没有找到目标关键字,所以函数返回false
表示查找失败。
递归写法
cpp
bool FindR(const K& key)
{
return _FindR(_root, key);
}
private:
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;
}
bool FindR(const K& key)
- 这是公有函数,用于在二叉搜索树中查找指定关键字
key
是否存在。它首先调用私有的递归函数_FindR
,并将根节点_root
和要查找的关键字key
作为参数传递给它。
- 这是公有函数,用于在二叉搜索树中查找指定关键字
bool _FindR(Node* root, const K& key)
- 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归查找指定关键字
key
是否存在。 - 函数首先检查当前根节点
root
是否为空(nullptr
)。如果为空,表示已经搜索到了树的底部,关键字key
不存在于树中,所以返回false
表示查找失败。 - 如果当前根节点
root
不为空,函数会比较当前节点的关键字root->_key
与要查找的关键字key
:- 如果
root->_key < key
,则说明要查找的关键字应该在当前节点的右子树中,所以递归调用_FindR(root->_right, key)
在右子树中查找。 - 如果
root->_key > key
,则说明要查找的关键字应该在当前节点的左子树中,所以递归调用_FindR(root->_left, key)
在左子树中查找。 - 如果
root->_key == key
,表示找到了目标关键字,函数返回true
表示查找成功。
- 如果
- 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归查找指定关键字
这个递归查找操作会在树的节点之间不断递归地进行,直到找到目标关键字或确认目标不存在。如果目标关键字存在于树中,函数返回 true
;否则,返回 false
。
7.二叉搜索树删除
迭代写法
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
{
// 1、左为空
// 2、右为空
// 3、左右都不为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
// 找到右子树最小节点进行替换
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
- 首先,函数初始化两个指针
parent
和cur
分别为nullptr
和根节点_root
,然后进入一个循环。这个循环用于在BST中找到待删除节点,并进行删除操作。 - 在循环中,函数会不断根据当前节点的关键字与目标关键字
key
的比较结果来决定向左子树还是右子树移动,同时更新parent
和cur
指针。 - 如果找到了目标关键字
key
,表示要开始删除操作。删除操作分为以下几种情况:- 情况1:当前节点的左子树为空。在这种情况下,直接用当前节点的右子树替换当前节点即可。
- 情况2:当前节点的右子树为空。在这种情况下,直接用当前节点的左子树替换当前节点即可。
- 情况3:当前节点的左右子树都不为空。在这种情况下,需要找到右子树中的最小节点(通常是右子树中最左边的节点),将其关键字与当前节点的关键字进行交换,然后删除最小节点。
- 删除操作执行完毕后,函数返回
true
表示删除成功。 - 如果循环结束时仍然没有找到目标关键字
key
,说明关键字不存在于树中,函数返回false
表示删除失败。
递归写法
cpp
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;
if (root->_left == nullptr)
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
// 找右树的最左节点替换删除
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
bool EraseR(const K& key)
- 这是公有函数,用于在二叉搜索树中删除指定关键字
key
的节点。它首先调用私有的递归函数_EraseR
,并将根节点_root
和要删除的关键字key
作为参数传递给它。
- 这是公有函数,用于在二叉搜索树中删除指定关键字
bool _EraseR(Node*& root, const K& key)
- 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归删除指定关键字
key
的节点。 - 函数首先检查当前根节点
root
是否为空(nullptr
)。如果为空,表示已经搜索到了树的底部,关键字key
不存在于树中,所以返回false
表示删除失败。 - 如果当前根节点
root
不为空,函数会比较当前节点的关键字root->_key
与要删除的关键字key
:- 如果
root->_key < key
,则说明要删除的关键字应该在当前节点的右子树中,所以递归调用_EraseR(root->_right, key)
在右子树中进行删除操作。 - 如果
root->_key > key
,则说明要删除的关键字应该在当前节点的左子树中,所以递归调用_EraseR(root->_left, key)
在左子树中进行删除操作。 - 如果
root->_key == key
,表示找到了要删除的目标节点,此时需要执行删除操作。
- 如果
- 这是私有的递归辅助函数,用于在给定的二叉搜索树中递归删除指定关键字
- 删除操作执行的逻辑分为以下几种情况:
- 情况1:当前节点的左子树为空。在这种情况下,可以直接用当前节点的右子树替换当前节点。
- 情况2:当前节点的右子树为空。在这种情况下,可以直接用当前节点的左子树替换当前节点。
- 情况3:当前节点的左右子树都不为空。在这种情况下,需要找到右子树中的最小节点,通常是右子树中最左边的节点,将其关键字与当前节点的关键字进行交换,然后继续递归删除右子树中的最小节点。
- 删除操作执行完毕后,函数返回
true
表示删除成功。 - 如果循环结束时仍然没有找到目标关键字
key
,说明关键字不存在于树中,函数返回false
表示删除失败。
8.二叉搜索树中序遍历
cpp
void InOrder()
{
_InOrder(_root);
cout << endl;
}
private:
void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
void InOrder()
- 这是公有函数,用于执行中序遍历二叉搜索树的操作。
- 首先,它调用了私有的递归函数
_InOrder(_root)
,并传递根节点_root
作为参数,开始进行中序遍历。 - 遍历完成后,会输出一个换行符,以便在输出时每个节点值都在一行上。
void _InOrder(Node* root)
- 这是私有的递归辅助函数,用于执行中序遍历的实际操作。
- 函数首先检查当前根节点
root
是否为空。如果为空,表示到达树的底部,直接返回,不执行任何操作。 - 如果当前节点不为空,函数按照中序遍历的顺序执行以下操作:
- 递归调用
_InOrder(root->_left)
,遍历左子树。 - 输出当前节点的关键字值
root->_key
,通常是将节点值打印到控制台。 - 递归调用
_InOrder(root->_right)
,遍历右子树。
- 递归调用
这个中序遍历函数会遍历整个二叉搜索树,并按照从小到大的顺序输出节点的关键字值。这是一种常用的方式来访问二叉搜索树的所有节点,并按照升序排列输出它们的值。
9.二叉搜索树所有代码
cpp
namespace Key
{
template<class K>
struct BSTreeNode
{
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;
BSTreeNode(const K& key)
:_left(nullptr)
, _right(nullptr)
, _key(key)
{}
};
template<class K>
class BSTree
{
typedef BSTreeNode<K> Node;
public:
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;
}
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、左为空
// 2、右为空
// 3、左右都不为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_right;
}
else
{
parent->_right = cur->_right;
}
}
delete cur;
cur = nullptr;
}
else if (cur->_right == nullptr)
{
if (_root == cur)
{
_root = cur->_left;
}
else
{
if (cur == parent->_left)
{
parent->_left = cur->_left;
}
else
{
parent->_right = cur->_left;
}
}
delete cur;
cur = nullptr;
}
else
{
// 找到右子树最小节点进行替换
Node* minParent = cur;
Node* min = cur->_right;
while (min->_left)
{
minParent = min;
min = min->_left;
}
swap(cur->_key, min->_key);
if (minParent->_left == min)
minParent->_left = min->_right;
else
minParent->_right = min->_right;
delete min;
}
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);
}
~BSTree()
{
_Destory(_root);
}
BSTree() = default;
BSTree(const BSTree<K>& t)
{
_root = _Copy(t._root);
}
// t2 = t1
BSTree<K>& operator=(BSTree<K> t)
{
swap(_root, t._root);
return *this;
}
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* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
return _EraseR(root->_right, key);
}
delete del;
return true;
}
}
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(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 == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}
private:
Node* _root = nullptr;
};
}
二叉搜索树的应用
-
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误
-
KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见
比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文
<word, chinese>
就构成一种键值对再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是
<word, count>
就构成一种键值对
例如:
cpp
namespace KeyValue
{
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:
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;
}
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)
{
//...
return true;
}
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);
}
private:
Node* _root = nullptr;
};
void TestBSTree1()
{
BSTree<string, string> dict;
dict.Insert("sort", "排序");
dict.Insert("left", "左");
dict.Insert("right", "右");
dict.Insert("vector", "向量");
dict.Insert("insert", "插入");
string str;
while (cin >> str)
{
BSTreeNode<string, string>* ret = dict.Find(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.Find(str);
if (ret)
{
ret->_value++;
}
else
{
countTree.Insert(str, 1);
}
}
countTree.InOrder();
}
}
二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为 :log_2 N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为 :frac{N}{2}
优点:
- 快速的查找操作: BST的主要优点是在查找操作上具有较快的平均时间复杂度。在平均情况下,查找一个元素的时间复杂度是O(log n),其中n是树中节点的数量。这使得BST非常适合实现高效的搜索操作。
- 有序性: BST保持节点按照键的顺序排列。这意味着你可以轻松地执行范围查询,找到最小值和最大值,或者执行有序的遍历。
- 插入和删除操作: 插入和删除元素的平均时间复杂度也是O(log n)。这使得BST对于需要频繁插入和删除元素的应用非常有效。
缺点:
- 性能不稳定: 最坏情况下,BST的性能可能会下降到O(n),其中n是树中节点的数量。这种情况发生在BST变得非常不平衡时,例如,如果将有序数据插入BST,将导致树的高度为n,从而导致O(n)的查找时间。
- 不平衡性: BST的性能高度依赖于树的平衡性。如果树不平衡,例如,变成了链表,那么查找、插入和删除的性能都会下降到O(n)。为了解决这个问题,出现了平衡二叉搜索树(AVL树、红黑树等),它们确保树的高度保持在较小的范围内,以维持O(log n)的性能。
- 内存需求: BST通常需要额外的内存来存储左子树和右子树的指针,这可能会导致内存消耗较大。
总结来说,BST在平均情况下具有较好的性能,特别适用于有序数据的查找和动态数据的插入/删除操作。然而,在最坏情况下,BST的性能可能变得不稳定,因此为了确保性能稳定,可以使用平衡二叉搜索树或其他更复杂的数据结构。选择适当的数据结构取决于应用的需求和数据的特点。