二叉搜索树
⼆叉搜索树的概念
⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:
- 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值
- 若它的右⼦树不为空,则右⼦树上所有结点的值都⼤于等于根结点的值
- 它的左右⼦树也分别为⼆叉搜索树
- ⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义,后续map/set/multimap/multiset系列容器底层就是⼆叉搜索树,其中map/set不⽀持插⼊相等值multimap/multiset⽀持插⼊相等值
⼆叉搜索树的性能分析
- 最优情况下,⼆叉搜索树为完全⼆叉树(或者接近完全⼆叉树),其⾼度为:O(log2 N)
- 最差情况下,⼆叉搜索树退化为单⽀树(或者类似单⽀),其⾼度为:O(N/2 )
二叉搜索树(BST)的性能主要依赖于树的形状。以下是其性能的详细分析:
平均性能
查找:O(log N)
插入:O(log N)
删除:O(log N)
这些性能假设树是平衡的,即每个节点的左右子树高度差不超过1。在这种情况下,树的高度大约为 log N,因此操作的时间复杂度为 O(log N)。
最坏性能
查找:O(N)
插入:O(N)
删除:O(N)
在最坏情况下,例如树退化成链表(所有节点只有一个子节点),树的高度变为 N,操作的时间复杂度变为 O(N)。
平衡性对性能的影响
完全二叉树:在这种树中,每一层都被完全填充,性能接近于 O(log N)。
AVL 树:一种自平衡的二叉搜索树,任何时刻保持平衡,因此所有操作都能在 O(log N) 时间内完成。
红黑树:另一种自平衡的二叉搜索树,虽然略逊色于 AVL 树的严格平衡,但仍保证 O(log N) 的操作时间。
操作详解
查找:从根节点开始,按键值与当前节点键值比较决定向左或向右子树移动。这个过程在平衡树中是对数级别的时间复杂度。
插入:查找适当的位置后插入新节点,并可能需要调整树结构(例如,通过旋转)以保持树的平衡。这在自平衡树中通常是对数级别的时间复杂度。
删除:查找并删除节点后,需要调整树结构以保持平衡。删除操作可能涉及节点替换(例如,使用右子树的最小节点替代),并可能需要进行树结构的调整。
二叉搜索树的插入
在二叉搜索树(BST)中插入一个新节点的过程包括以下步骤:
从根节点开始:.
首先,将新节点的键值与当前节点的键值进行比较。
决定插入方向:
如果新节点的键值小于当前节点的键值,则移动到当前节点的左子树。
如果新节点的键值大于当前节点的键值,则移动到当前节点的右子树。
递归或迭代插入:
如果左子树或右子树为空(即到达一个叶节点的位置),则在该位置插入新节点。
如果左子树或右子树不为空,则递归或迭代执行插入过程,直到找到合适的位置。
更新树结构:插入节点后,树的结构可能需要更新。如果树是自平衡的(如 AVL 树或红黑树),可能需要调整树的平衡性,例如通过旋转操作来维持树的平衡。
示例代码(伪代码)
void insert(Node*& root, int key) {
if (root == nullptr) {
root = new Node(key); // 插入新节点
} else if (key < root->key) {
insert(root->left, key); // 递归插入到左子树
} else {
insert(root->right, key); // 递归插入到右子树
}
}
关键点
查找插入位置:通过比较键值确定插入位置。
保持 BST 属性:插入新节点后,树仍应保持二叉搜索树的性质。
自平衡树处理:如果使用自平衡树,如 AVL 树或红黑树,插入后需要额外步骤来调整树的平衡。
二叉搜索树的查找
在二叉搜索树(BST)中,查找某个节点的步骤非常简单,因为 BST 的特性保证了每个节点的左子树中的所有节点值都小于该节点的值,右子树中的所有节点值都大于该节点的值。基于这一特性,查找操作可以高效地通过比较来进行。
查找步骤
从根节点开始:
如果树为空,则返回 null 或相应的错误提示,因为节点不存在。
比较节点值:
如果查找的值等于当前节点的值,说明找到了目标节点,返回该节点。
如果查找的值小于当前节点的值,则在当前节点的左子树中继续查找。
如果查找的值大于当前节点的值,则在当前节点的右子树中继续查找。
递归或迭代:
递归方法会调用自身来继续查找,直到找到目标节点或到达树的末端。
迭代方法使用循环来进行查找,直到找到目标节点或遍历完整棵树。
示例代码(C++)
下面是一个使用 C++ 实现的二叉搜索树查找操作的示例代码,包括递归和迭代两种方式:
#include <iostream>
class TreeNode {
public:
int key;
TreeNode* left;
TreeNode* right;
TreeNode(int val) : key(val), left(nullptr), right(nullptr) {
}
};
class BinarySearchTree {
public:
BinarySearchTree() : root(nullptr) {
}
void insert(int key) {
TreeNode* newNode = new TreeNode(key);
if (root == nullptr) {
root = newNode;
return;
}
TreeNode* current = root;
TreeNode* parent = nullptr;
while (true) {
parent = current;
if (key < current->key) {
current = current->left;
if (current == nullptr) {
parent->left = newNode;
return;
}
} else {
current = current->right;
if (current == nullptr) {
parent->right = newNode;
return;
}
}
}
}
TreeNode* search(int key) {
return searchRec(root, key);
}
TreeNode* searchIter(int key) {
TreeNode* current = root;
while (current != nullptr)