【LeetCode & 剑指offer刷题】树题2:二叉查找树的查找、插入、删除
【LeetCode & 剑指offer 刷题笔记】目录(持续更新中...)
二叉查找树的查找、插入、删除
1 查找结点
最佳情况是 O(log
2
n),而最坏情况是 O(n)
-
BST 的查找是从根结点开始,若二叉树非空,将给定值与根结点的关键字比较,
-
若相等,则查找成功;
-
若不等,则比较查找结点值与当前结点值大小,当给定值小于当前结点值时,在当前结点左子树中查找,否则在右子树中查找
typedef
struct
Node
{
int
key
;
Node
*
left
;
Node
*
right
;
Node
*
parent
;
}
*
BSTree
;
/**
*
递归查找:返回指向包含关键字
k
的结点的指针
*/
Node
*
BST_Search
(
BSTree T
,
int
k
)
{
if
(
T
==
NULL
||
k
==
T
->
key
)
return
T
;
if
(
k
<
T
->
key
)
return
BST_Search
(
T
->
left
,
k
);
else
return
BST_Search
(
T
->
right
,
k
);
2 插入结点
最佳情况是 O(log
2
n),而最坏情况是 O(n)
当向树中插入一个新的结点时,该结点将总是作为叶子结点
。所以,最困难的地方就是
如何找到该结点的父结点
。
类似于查找算法中的描述,我们
将这个新的结点称为结点 n,而遍历的当前结点称为结点 c
。开始时,结点 c 为 BST 的根结点。则定位结点 n 父结点的步骤如下:
-
在二叉树中搜寻( 若插入值小于当前结点值,则在左子树中递归插入,否则在右子树中递归插入,直到遍历到空结点,插入到此位置 ) (联系在有序数组中二分插入一值)
-
如果结点 c 为空 ,将结点 插入 到c位置。
-
如果结点c不为空,比较结点 c 与结点 n 的值。
-
如果结点 c 的值与结点 n 的值相等,则说明用户在试图插入一个重复的结点。解决办法可以是直接丢弃结点 n,或者可以抛出异常。
-
如果结点 n 的值小于结点 c 的值,则说明结点 n 一定是在结点 c 的左子树中。则将父结点设置为结点 c,并将结点 c 设置为结点 c 的左孩子,然后返回至第 1 步。
-
如果结点 n 的值大于结点 c 的值,则说明结点 n 一定是在结点 c 的右子树中。则将父结点设置为结点 c,并将结点 c 设置为结点 c 的右孩子,然后返回至第 1 步
当合适的结点找到时,该算法结束。从而使新结点被放入 BST 中成为某一父结点合适的孩子结点。
/**
*
插入:将关键字
k
插入到二叉查找树
*/
int
BST_Insert
(
BSTree
&
T
,
int
k
,
Node
*
parent
)
{
if
(
T
==
NULL
)
{
T
=
(
BSTree
)
malloc
(
sizeof
(
Node
));
T
->
key
=
k
;
T
->
left
=
NULL
;
T
->
right
=
NULL
;
T
->
parent
=
parent
;
return
1
;
//
返回
1
表示成功
}
else
if
(
k
==
T
->
key
)
return
0
;
//
树中存在相同关键字
else
if
(
k
<
T
->
key
)
return
BST_Insert
(
T
->
left
,
k
,
T
);
else
return
BST_Insert
(
T
->
right
,
k
,
T
);
}
3 删除结点
最佳情况是 O(log2n),而最坏情况是 O(n)
从 BST 中删除节点比插入节点难度更大。因为删除一个非叶子节点,就必须选择其他节点来填补因删除节点所造成的树的断裂。如果不选择节点来填补这个断裂,那么就违背了 BST 的性质要求。
删除节点算法的第一步是定位要被删除的节点
,这可以使用前面介绍的查找算法,因此运行时间为 O(log
2
n)。
接着应该选择合适的节点来代替删除节点的位置
,它共有三种情况需要考虑
-
情况 1: 如果删除的节点没有右孩子,那么就选择它的左孩子来代替原来的节点 。二叉查找树的性质保证了被删除节点的左子树必然符合二叉查找树的性质。因此左子树的值要么都大于,要么都小于被删除节点的父节点的值,这取决于被删除节点是左孩子还是右孩子。因此用被删除节点的左子树来替代被删除节点,是完全符合二叉搜索树的性质的。
-
情况 2: 如果被删除节点的右孩子没有左孩子 ,那么这个右孩子被用来替换被删除节点。因为被删除节点的右孩子都大于被删除节点左子树的所有节点,同时也大于或小于被删除节点的父节点,这同样取决于被删除节点是左孩子还是右孩子。因此,用右孩子来替换被删除节点,符合二叉查找树的性质。
-
情况 3: 如果被删除节点的右孩子有左孩子 ,就需要用被删除节点右孩子的左子树中的最下面的节点来替换它,就是说,我们 用被删除节点的右子树中最小值的节点来替换 。
或者用代码中的思路
/*
二叉查找树的结点删除
(1)
若被删除结点z
是叶子结点,则直接删除,不会破坏二叉排序树的性质;
(2)
若结点z
只有左子树或只有右子树,则让
z
的子树成为
z
父结点的子树,替代
z
的位置;
(3)
若结点z
既有左子树,又有右子树,则
用二叉树中序遍历z的后继(Successor)代替z(由于是二叉查找树,这个后继是子树中最小的值)
,然后从二叉查找树中删除这个后继,这样就转换成了第一或第二种情况。
*/
void
BST_Delete
(
BSTree
&
T
,
Node
*
z
)
{
if
(
z
->
left
==
NULL
&&
z
->
right
==
NULL
)
//被删除的是叶子结点,直接删除
{
if
(
z
->
parent
!=
NULL
)
{
if
(
z
->
parent
->
left
==
z
)
z
->
parent
->
left
=
NULL
;
else
z
->
parent
->
right
=
NULL
;
}
else
{
T
=
NULL
;
//
只剩一个结点的情况
}
free
(
z
);
}
else
if
(
z
->
left
!=
NULL
&&
z
->
right
==
NULL
)
//只有右子树,则让子树替代z的位置
{
z
->
left
->
parent
=
z
->
parent
;
if
(
z
->
parent
!=
NULL
)
{
if
(
z
->
parent
->
left
==
z
)
z
->
parent
->
left
=
z
->
left
;
else
z
->
parent
->
right
=
z
->
left
;
}
else
{
T
=
z
->
left
;
//
删除左斜单支树的根结点
}
free
(
z
);
}
else
if
(
z
->
left
==
NULL
&&
z
->
right
!=
NULL
)
//只有左子树
{
z
->
right
->
parent
=
z
->
parent
;
if
(
z
->
parent
!=
NULL
)
{
if
(
z
->
parent
->
left
==
z
)
z
->
parent
->
left
=
z
->
right
;
else
z
->
parent
->
right
=
z
->
right
;
}
else
{
T
=
z
->
right
;
//
删除右斜单支树的根结点
}
free
(
z
);
}
else
//既有左子树又有右子树,让z的后继代替z,然后删除这个后继
{
Node
*
s
=
BST_Successor
(
z
);
z
->
key
=
s
->
key
;
// s
的关键字替换
z
的关键字
BST_Delete
(
T
,
s
);
//
转换为第一或第二种情况
}
}
/**
* 后继:求二叉树中序遍历的下一个结点
方法:
(1)
有右子树的,那么下个结点就是右子树最左边的点;
(2)
没有右子树的,是父结点的左孩子,返回父结点;
(3)
没有右子树,是父结点的右孩子,从结点
x
开始向上查找,直到遍历到的结点是其父结点的左孩子位置,返回该父结点
*/
Node
*
BST_Successor
(
Node
*
node
)
{
if
(
node
->
right
)
//
有右子树时,下个结点为右子树最左结点
{
Node
*
t
=
node
->
right
;
//
右子树
while
(
t
->
left
)
t
=
t
->
left
;
return
t
;
}
else
//
无右子树时,返回父结点
(
向上查找,结点为左孩子的父结点
)
{
Node
*
par
=
node
->
parent
;
//
父结点
while
(
par
&&
node != par->left
)
//
如果当前结点是父结点的右孩子,向上遍历,直到当前结点为左结点
{
node
=
par
;
par
=
par
->
parent
;
}
//
退出时,当前结点为父结点的左孩子
return
par
;
//
返回父结点
}
}