目录
编辑
0.前言
1.二叉搜索树的概念
1.1 二叉树的基本性质
1.2 二叉搜索树的性质
1.3 二叉搜索树的示例
2.二叉搜索树的操作
2.1查找节点
2.2插入节点
2.3删除节点
2.4遍历节点
3.二叉搜索树的实现
4.二叉搜索树的应用
4.1K模型
4.2KV模型
5.二叉搜索树性能分析
5.1 时间复杂度
5.2 空间复杂度
5.3 平衡二叉搜索树
6.结语
(图像由AI生成)
在之前的C语言系列博客中,我们已经介绍了二叉树的基本概念。二叉树是一种常见的数据结构,其每个节点最多有两个子节点,分别称为左子节点和右子节点。今天,我们将进一步探讨一种特殊的二叉树——二叉搜索树(Binary Search Tree,简称BST)。BST在计算机科学中有着广泛的应用,尤其是在需要快速查找、插入和删除操作的场景中。
二叉搜索树(Binary Search Tree,简称BST)是一种特殊的二叉树,它在树节点的存储和操作上遵循特定的性质,使得查找、插入和删除操作更加高效。具体而言,二叉搜索树具有以下几个主要性质:
在讨论二叉搜索树之前,首先回顾一下二叉树的基本性质:
二叉搜索树在二叉树的基础上增加了以下性质:
这三条性质保证了二叉搜索树的有序性,使得在树中进行查找、插入和删除操作时可以利用二分查找的思想,从而提高操作效率。
举一个简单的例子来说明二叉搜索树的性质:
10
/ \
5 15
/ \ / \
3 7 12 18
在这个二叉搜索树中:
查找节点操作的目的是在树中找到一个与给定值匹配的节点。查找操作从根节点开始,根据节点的值与目标值的比较结果,决定在左子树或右子树中继续查找,直到找到目标节点或到达树的叶节点。
查找操作的步骤如下:
下面是查找节点操作的代码实现:
template
class BSTree
{
private:
BSTNode* root;
public:
typedef BSTNode Node;
typedef BSTNode* PNode;
// 查找节点函数,根据给定的数据查找节点,返回指向该节点的指针
PNode find(const T& data) const
{
PNode p = root; // 从根节点开始查找
while (p)
{
if (data == p->data) // 找到匹配节点
return p;
else if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
return nullptr; // 未找到匹配节点,返回 nullptr
}
};
插入节点操作的目的是在树中添加一个新节点。插入操作也从根节点开始,根据新节点的值与当前节点的值进行比较,决定将新节点插入到左子树或右子树中,直到找到一个合适的叶节点位置。
插入操作的步骤如下:
下面是插入节点操作的代码实现:
template
class BSTree
{
private:
BSTNode* root;
public:
typedef BSTNode Node;
typedef BSTNode* PNode;
// 插入节点函数,插入成功返回 true,否则返回 false
bool insert(const T& data)
{
PNode p = root; // 从根节点开始查找插入位置
PNode pp = nullptr; // 记录父节点指针
while (p)
{
pp = p;
if (data == p->data) // 数据已存在,插入失败
return false;
else if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
PNode newNode = new Node(data); // 创建新节点
if (pp == nullptr) // 树为空,新节点为根节点
root = newNode;
else if (data < pp->data) // 新节点为父节点的左子节点
pp->left = newNode;
else // 新节点为父节点的右子节点
pp->right = newNode;
return true; // 插入成功
}
};
删除节点操作的目的是从树中移除一个指定值的节点。删除操作相对复杂一些,因为需要考虑删除节点后如何保持树的有序性。删除操作包括三种情况:
删除操作的步骤如下:
下面是删除节点操作的代码实现:
template
class BSTree
{
private:
BSTNode* root;
public:
typedef BSTNode Node;
typedef BSTNode* PNode;
// 删除节点函数,删除成功返回 true,否则返回 false
bool erase(const T& data)
{
PNode p = root; // 从根节点开始查找要删除的节点
PNode pp = nullptr; // 记录父节点指针
// 查找要删除的节点
while (p && p->data != data)
{
pp = p;
if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
if (p == nullptr) // 未找到要删除的节点
return false;
// 要删除的节点有两个子节点
if (p->left && p->right)
{
PNode minP = p->right; // 找到右子树中的最小节点
PNode minPP = p; // 记录最小节点的父节点指针
while (minP->left)
{
minPP = minP;
minP = minP->left;
}
p->data = minP->data; // 用最小节点的数据替换要删除节点的数据
p = minP; // 重新标记要删除的节点
pp = minPP; // 更新父节点指针
}
// 要删除的节点是叶子节点或者仅有一个子节点
PNode child = nullptr; // 记录要删除节点的子节点指针
if (p->left)
child = p->left;
else if (p->right)
child = p->right;
if (pp == nullptr) // 要删除的节点是根节点
root = child;
else if (pp->left == p) // 要删除的节点是父节点的左子节点
pp->left = child;
else // 要删除的节点是父节点的右子节点
pp->right = child;
delete p; // 释放要删除的节点
return true; // 删除成功
}
};
遍历节点是指按照一定的顺序访问树中的所有节点。常见的遍历方法包括前序遍历、中序遍历和后序遍历。在二叉搜索树中,中序遍历尤为重要,因为它可以按从小到大的顺序输出树中的所有节点值。
下面是中序遍历操作的代码实现:
template
class BSTree
{
private:
BSTNode* root;
public:
typedef BSTNode Node;
typedef BSTNode* PNode;
// 中序遍历函数,递归遍历树的节点并输出节点数据
void inOrder(PNode p) const
{
if (p)
{
inOrder(p->left); // 递归遍历左子树
cout << p->data << " "; // 输出节点数据
inOrder(p->right); // 递归遍历右子树
}
}
};
为了遍历整棵树,可以在主函数中调用 inOrder
函数,并传入根节点:
int main() {
Key::BSTree tree;
tree.insert(5);
tree.insert(3);
tree.insert(7);
tree.insert(2);
tree.insert(4);
tree.insert(6);
tree.insert(8);
cout << "Inorder traversal: ";
tree.inOrder(tree.getRoot()); // 调用中序遍历
cout << endl;
return 0;
}
输出结果:
Inorder traversal: 2 3 4 5 6 7 8
下面是二叉搜索树(K模型)的基本实现代码:
namespace Key {
// 定义模板结构体 BSTNode,用于表示二叉搜索树的节点
template
struct BSTNode
{
T data; // 节点存储的数据
BSTNode* left; // 指向左子节点的指针
BSTNode* right; // 指向右子节点的指针
// 构造函数,初始化节点数据和左右子节点指针
BSTNode(const T& data = T()) : data(data), left(nullptr), right(nullptr) {}
};
// 定义模板类 BSTree,用于表示二叉搜索树
template
class BSTree
{
private:
BSTNode* root; // 指向树根节点的指针
public:
typedef BSTNode Node; // 定义 Node 类型为 BSTNode
typedef BSTNode* PNode; // 定义 PNode 类型为指向 BSTNode 的指针
// 构造函数,初始化树根节点为 nullptr
BSTree() :root(nullptr) {}
// 析构函数
~BSTree() {}
// 获取树根节点
PNode getRoot() const { return root; }
// 中序遍历函数,递归遍历树的节点并输出节点数据
void inOrder(PNode p) const
{
if (p)
{
inOrder(p->left); // 递归遍历左子树
cout << p->data << " "; // 输出节点数据
inOrder(p->right); // 递归遍历右子树
}
}
// 查找节点函数,根据给定的数据查找节点,返回指向该节点的指针
PNode find(const T& data) const
{
PNode p = root; // 从根节点开始查找
while (p)
{
if (data == p->data) // 找到匹配节点
return p;
else if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
return nullptr; // 未找到匹配节点,返回 nullptr
}
// 插入节点函数,插入成功返回 true,否则返回 false
bool insert(const T& data)
{
PNode p = root; // 从根节点开始查找插入位置
PNode pp = nullptr; // 记录父节点指针
while (p)
{
pp = p;
if (data == p->data) // 数据已存在,插入失败
return false;
else if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
PNode newNode = new Node(data); // 创建新节点
if (pp == nullptr) // 树为空,新节点为根节点
root = newNode;
else if (data < pp->data) // 新节点为父节点的左子节点
pp->left = newNode;
else // 新节点为父节点的右子节点
pp->right = newNode;
return true; // 插入成功
}
// 删除节点函数,删除成功返回 true,否则返回 false
bool erase(const T& data)
{
PNode p = root; // 从根节点开始查找要删除的节点
PNode pp = nullptr; // 记录父节点指针
// 查找要删除的节点
while (p && p->data != data)
{
pp = p;
if (data < p->data) // 数据小于当前节点数据,查找左子树
p = p->left;
else // 数据大于当前节点数据,查找右子树
p = p->right;
}
if (p == nullptr) // 未找到要删除的节点
return false;
// 要删除的节点有两个子节点
if (p->left && p->right)
{
PNode minP = p->right; // 找到右子树中的最小节点
PNode minPP = p; // 记录最小节点的父节点指针
while (minP->left)
{
minPP = minP;
minP = minP->left;
}
p->data = minP->data; // 用最小节点的数据替换要删除节点的数据
p = minP; // 重新标记要删除的节点
pp = minPP; // 更新父节点指针
}
// 要删除的节点是叶子节点或者仅有一个子节点
PNode child = nullptr; // 记录要删除节点的子节点指针
if (p->left)
child = p->left;
else if (p->right)
child = p->right;
if (pp == nullptr) // 要删除的节点是根节点
root = child;
else if (pp->left == p) // 要删除的节点是父节点的左子节点
pp->left = child;
else // 要删除的节点是父节点的右子节点
pp->right = child;
delete p; // 释放要删除的节点
return true; // 删除成功
}
};
}
二叉搜索树(BST)在计算机科学中有着广泛的应用,特别是在需要快速查找、插入和删除操作的场景中。根据应用需求的不同,二叉搜索树可以有多种变体,常见的包括K模型和KV模型。
K模型是一种基于二叉搜索树的数据存储模型,在这种模型中,树的节点仅存储键(key),而不存储对应的值。K模型适用于只需要键而不需要存储对应值的场景。例如,在实现集合(Set)数据结构时,K模型是一种常见的选择。K模型的代码参考3.二叉搜索树的实现
应用场景
特点
KV模型是一种扩展的二叉搜索树模型,在这种模型中,树的节点不仅存储键(key),还存储对应的值(value)。KV模型适用于键值对数据存储的场景,例如在实现映射(Map)数据结构时,KV模型非常有用。
应用场景
特点
下面是KV模型的二叉搜索树的代码实现:
namespace KeyValue {
template
struct BSTNode
{
K key; // 节点存储的键
V value; // 节点存储的值
BSTNode* left; // 指向左子节点的指针
BSTNode* right; // 指向右子节点的指针
// 构造函数,初始化节点键和值以及左右子节点指针
BSTNode(const K& key = K(), const V& value = V()) : key(key), value(value), left(nullptr), right(nullptr) {}
};
template
class BSTree
{
typedef BSTNode Node; // 定义 Node 类型为 BSTNode
typedef BSTNode* PNode; // 定义 PNode 类型为指向 BSTNode 的指针
private:
PNode root; // 指向树根节点的指针
public:
BSTree() :root(nullptr) {} // 构造函数,初始化树根节点为 nullptr
~BSTree() {} // 析构函数
PNode getRoot() const { return root; } // 获取树根节点
// 中序遍历函数,递归遍历树的节点并输出节点键和值
void inOrder(PNode p) const
{
if (p)
{
inOrder(p->left); // 递归遍历左子树
cout << p->key << "->" << p->value << " "; // 输出节点键和值
inOrder(p->right); // 递归遍历右子树
}
}
// 查找节点函数,根据给定的键查找节点,返回指向该节点的指针
PNode find(const K& key) const
{
PNode p = root; // 从根节点开始查找
while (p)
{
if (key == p->key) // 找到匹配节点
return p;
else if (key < p->key) // 键小于当前节点键,查找左子树
p = p->left;
else // 键大于当前节点键,查找右子树
p = p->right;
}
return nullptr; // 未找到匹配节点,返回 nullptr
}
// 插入节点函数,插入成功返回 true,否则返回 false
bool insert(const K& key, const V& value)
{
PNode p = root; // 从根节点开始查找插入位置
PNode pp = nullptr; // 记录父节点指针
while (p)
{
pp = p;
if (key == p->key) // 键已存在,插入失败
return false;
else if (key < p->key) // 键小于当前节点键,查找左子树
p = p->left;
else // 键大于当前节点键,查找右子树
p = p->right;
}
PNode newNode = new Node(key, value); // 创建新节点
if (pp == nullptr) // 树为空,新节点为根节点
root = newNode;
else if (key < pp->key) // 新节点为父节点的左子节点
pp->left = newNode;
else // 新节点为父节点的右子节点
pp->right = newNode;
return true; // 插入成功
}
// 删除节点函数,删除成功返回 true,否则返回 false
bool erase(const K& key)
{
PNode p = root; // 从根节点开始查找要删除的节点
PNode pp = nullptr; // 记录父节点指针
// 查找要删除的节点
while (p && p->key != key)
{
pp = p;
if (key < p->key) // 键小于当前节点键,查找左子树
p = p->left;
else // 键大于当前节点键,查找右子树
p = p->right;
}
if (p == nullptr) // 未找到要删除的节点
return false;
// 要删除的节点有两个子节点
if (p->left && p->right)
{
PNode minP = p->right; // 找到右子树中的最小节点
PNode minPP = p; // 记录最小节点的父节点指针
while (minP->left)
{
minPP = minP;
minP = minP->left;
}
p->key = minP->key; // 用最小节点的键替换要删除节点的键
p->value = minP->value; // 用最小节点的值替换要删除节点的值
p = minP; // 重新标记要删除的节点
pp = minPP; // 更新父节点指针
}
// 要删除的节点是叶子节点或者仅有一个子节点
PNode child = nullptr; // 记录要删除节点的子节点指针
if (p->left)
child = p->left;
else if (p->right)
child = p->right;
if (pp == nullptr) // 要删除的节点是根节点
root = child;
else if (pp->left == p) // 要删除的节点是父节点的左子节点
pp->left = child;
else // 要删除的节点是父节点的右子节点
pp->right = child;
delete p; // 释放要删除的节点
return true; // 删除成功
}
};
}
二叉搜索树(BST)的性能分析主要关注其时间复杂度和空间复杂度。BST的性能与树的结构密切相关,树的高度(深度)是影响其操作效率的关键因素。本文将从平均情况和最坏情况两个方面分析BST的性能。
BST的主要操作包括查找、插入和删除。它们的时间复杂度主要取决于树的高度h。
平均情况
在理想情况下(即树是平衡的),树的高度h与节点数n之间的关系为h = O(log n)。在这种情况下,查找、插入和删除操作的平均时间复杂度为O(log n)。这种情况通常出现在随机插入节点的情况下。
最坏情况
在最坏情况下(即树退化为一个链表),树的高度h与节点数n之间的关系为h = O(n)。在这种情况下,查找、插入和删除操作的时间复杂度为O(n)。这种情况通常出现在顺序插入节点的情况下。
各操作的时间复杂度总结
BST的空间复杂度主要取决于存储节点所需的内存。每个节点需要存储数据、左子节点指针和右子节点指针。因此,BST的空间复杂度为O(n),其中n为节点数。
为了避免BST退化为链表,常常使用平衡二叉搜索树(如红黑树和AVL树)。这些树通过在插入和删除操作后调整树的结构,保证树的高度保持在O(log n),从而确保查找、插入和删除操作的时间复杂度为O(log n)。
红黑树
红黑树是一种自平衡的二叉搜索树,每个节点包含一个额外的颜色位(红色或黑色)。通过遵循一组严格的规则,红黑树在插入和删除操作后保持树的平衡,使得其高度始终为O(log n)。
AVL树
AVL树是另一种自平衡的二叉搜索树,它通过维护每个节点的平衡因子(即左子树高度与右子树高度之差),在插入和删除操作后调整树的结构,确保树的高度保持在O(log n)。
二叉搜索树是一种功能强大且高效的数据结构,广泛应用于各种查找、插入和删除操作中。通过理解其基本概念和操作,并借助平衡二叉搜索树等优化技术,可以在实际应用中有效地提升性能。希望本文的介绍能够帮助你更好地理解和应用二叉搜索树,为你的编程实践提供有力支持。