二叉查找树的定义、遍历与查找
定义
二叉查找树( B i n a r y Binary Binary S e a r c h Search Search T r e e Tree Tree)或者是一棵空树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉查找树。
上述性质被称为
B
S
T
BST
BST性质
可以看出,二叉查找树是递归定义的
如图
1
1
1是一个二叉查找树。
下述代码给出了一个
B
S
T
BST
BST节点的定义。
struct BST_Node {
BST_Node *left, *right;//节点的左右子树的指针
int value; //节点的值
};
遍历
对于一个已知的二叉查找树,从小到大输出其节点的值,只需对其进行二叉树的中序遍历,即递归地先输出其左子树,再输出其本身,然后输出其右子树
遍历的时间复杂度为
Θ
(
N
)
Θ(N)
Θ(N)
下述代码为把一个以
r
o
o
t
root
root为根的
B
S
T
BST
BST所有节点的值从小到大输出到标准输出流。
BST_Node *root;
void BST_Print(BST_Node *P) {
if (P) {
BST_Print(P->left);
printf(“%d\n”, P->value);
BST_Print(P->right);
}
}
int main() {
BST_Print(root);
return 0;
}
查找
对于一个已知的二叉查找树,在其中查找特定的值,方法如下。
- 从根节点开始查找;
- 如果当前节点的值就是要查找的值,查找成功;
- 如果要查找的值小于当前节点的值,在当前节点的左子树中查找该值;
- 如果要查找的值大于当前节点的值,在当前节点的右子树中查找该值;
- 如果当前节点为空节点,查找失败,二叉查找树中没有要查找的值。
查找的期望时间复杂度为
Θ
(
l
o
g
N
)
Θ(logN)
Θ(logN)
下述代码为在一个已知的二叉查找树中查找值
5
5
5。
BST_Node *root;
BST_Node *BST_Find(BST_Node *P, int value) {
if (!P) return 0; //查找失败
if (P->value == value) return P; //查找成功
else if (value < P->value) return BST_Find(P->left, value); //在左子树中查找
else return BST_Find(P->right, value); //在右子树中查找
}
int main() {
BST_Node *result;
result = BST_Find(root, 5);//在BST中查找值为5的节点
if (result) printf("Found!");
else printf("Not Found!");
return 0;
}
二叉查找树的插入与删除
插入
在二叉查找树中插入元素,要建立在查找的基础上
基本方法是类似于线性表中的二分查找,不断地在树中缩小范围定位,最终找到一个合适的位置插入
具体方法如下所述。
- 从根节点开始插入;
- 如果要插入的值小于等于当前节点的值,在当前节点的左子树中插入;
- 如果要插入的值大于当前节点的值,在当前节点的右子树中插入;
- 如果当前节点为空节点,在此建立新的节点,该节点的值为要插入的值,左右子树为空,插入成功。
对于相同的元素,一种方法我们规定把它插入左边,另一种方法是我们在节点上再加一个域,记录重复节点的个数
上述方法为前者
插入的期望时间复杂度为
Θ
(
l
o
g
N
)
Θ(logN)
Θ(logN)。
下述代码为在一个二叉查找树中插入值
5
5
5。
BST_Node *root;
BST_Node *BST_Insert(BST_Node *&P,int value) {//节点指针要传递引用
if (!P) {
P = new BST_Node; //插入成功
P->value = value;
}
else if (value <= value) return BST_Insert(P->left, value); //在左子树中插入
else return BST_Insert(P->right, value); //在右子树中插入
}
int main() {
BST_Insert(root, 5);//在 BST 中插入值 5
return 0;
}
删除
二叉查找树的删除稍有些复杂,要分三种情况分别讨论
基本方法是要先在二叉查找树中找到要删除的节点的位置,然后根据节点分以下情况:
- 情况一,该节点是叶节点(没有非空子节点的节点),直接把节点删除即可。
- 情况二,该节点是链节点(只有一个非空子节点的节点),为了删除这个节点而不影响它的子树,需要把它的子节点代替它的位置,然后把它删除。如图
2
2
2所示,删除节点
2
2
2时,需要把它的左子节点代替它的位置。
- 情况三,该节点有两个非空子节点。由于情况比较复杂,一般的策略是用它右子树的最小值来代替它,然后把它删除。如图
3
3
3所示,删除节点
2
2
2时,在它的右子树中找到最小的节点
3
3
3,该节点一定为待删除节点的后继。删除节点
3
3
3(它可能是叶节点或者链节点),然后把节点
2
2
2的值改为
3
3
3。
实际上在实现的时候,情况一和情况二是可以一样对待的,因为用叶节点的子节点代替它本身,就是用空节点代替了它,等同于删除
对待情况三除了可以用后继代替本身以外,我们还可以使用它的前驱(左子树的最大值)代替它本身
这就牵涉到了查找最小值的问题,基本方法就是从子树的根节点开始,如果左子节点不为空,那么就访问左子节点,直到左子节点为空,当前节点就是该子树的最小值节点,删除它只需用它的右子节点代替它本身。
下述代码是在一个给定的二叉查找树中删除值
2
2
2。
BST_Node *root;
int Find_DeleteMin(BST_Node *&P) {//返回子树的最小值并删除 节点指针要传递引用
if (!P->left) {//如果已经没有左子节点,删除它
BST_Node *t = P;
int r = P->value;
P = P->right;
delete t;
return r;
}
else return Find_DeleteMin(P->left); //如果还有左子节点,访问左子节点
}
void BST_Delete(BST_Node *&P, int value)//节点指针要传递引用
{
if (!P) return ; //查找失败,BST 中不存在要删除的节点
if (P->value == value) {//查找成功,对其删除
if (P->left and P->right) P->value = Find_DeleteMin(P->right);
else {
BST_Node *t = P;
if (P->left) P = P->left;
else P = P->right;
delete t;
}
}
else if (value < P->value) BST_Delete(P->left, value); //在左子树中查找
else BST_Delete(P->right, value); //在右子树中查找
}
int main() {
BST_Delete(root, 2);//在 BST 中删除值 2
return 0;
}
二叉查找树的平衡性问题讨论
二叉查找树的效果究竟如何?
事实上,随机的进行
N
2
(
N
>
=
1000
)
N^2(N>=1000)
N2(N>=1000)次插入和删除之后,二叉查找树会趋向于向左偏沉
为什么会出现这种情况,原因是在于删除时,我们总是选择将待删除节点的后继代替它本身
这样就会造成总是右边的节点数目减少,以至于树向左偏沉
已经被证明,随机插入或删除
N
2
N^2
N2次以后,树的期望深度为
Θ
(
N
1
2
)
Θ(N^{\frac{1}{2}})
Θ(N21)
我们可以在删除时随机地选择用前驱还是后继代替本身,以消除向一边偏沉的问题
这种神奇地做法消除了偏沉的不平衡,效果十分明显
对待随机的数据二叉查找树已经做得很不错了,但是如果有像这样
6
,
5
,
4
,
3
,
2
,
1
6,5,4,3,2,1
6,5,4,3,2,1有序的数据插入树中时,会有什么后果出现?
如图
4
4
4
二叉查找树退化成为了一条链
这个时候,无论是查找、插入还是删除,都退化成了
Θ
(
N
)
Θ(N)
Θ(N)的时间
事实上,在实际的应用中,这种情况会经常出现,而简单的二叉查找树却没有办法变得更快
我们需要使二叉查找树变得尽量平衡,才能保证各种操作在
Θ
(
l
o
g
N
)
Θ(logN)
Θ(logN)的期望时间内完成,于是各种自动平衡二叉查找树(
S
e
l
f
−
B
a
l
a
n
c
i
n
g
Self-Balancing
Self−Balancing
B
i
n
a
r
y
Binary
Binary
S
e
a
r
c
h
Search
Search
T
r
e
e
Tree
Tree)因而诞生
在常见的自动平衡二叉查找树(以下简称平衡树)中,有近乎完美平衡的
A
V
L
AVL
AVL树(
A
d
e
l
s
o
n
−
V
e
l
s
k
i
i
Adelson-Velskii
Adelson−Velskii &
L
a
n
d
i
s
Landis
Landis
T
r
e
e
Tree
Tree),红黑树(
R
e
d
Red
Red
B
l
a
c
k
Black
Black
T
r
e
e
Tree
Tree)和
S
B
T
SBT
SBT(
S
i
z
e
Size
Size
B
a
l
a
n
c
e
d
Balanced
Balanced
T
r
e
e
Tree
Tree)等,以及期望平衡的伸展树(
S
p
l
a
y
Splay
Splay
T
r
e
e
Tree
Tree)和
T
r
e
a
p
Treap
Treap(
T
r
e
e
Tree
Tree &
H
e
a
p
Heap
Heap)等数据结构。
T
r
e
a
p
Treap
Treap具有可观的平衡性,且最易于编码调试等特点,因此在信息学竞赛中被广泛地使用。
相应的,这里还有关于
T
r
e
a
p
Treap
Treap的讲解,有兴趣的同学可以来这里看一下——传送门