文章目录
本文参考:《算法导论》P286~P307
注:每种操作下列举了一些leetcode上的相关习题,可以用作练习。
二叉搜索树
二叉搜索树由一棵二叉树组织。这样的一棵树可以使用一个链表的数据结构来表示,每个节点就是一个对象。节点的属性值包含
l
e
f
t
left
left(左孩子)、
r
i
g
h
t
right
right(右孩子)、
p
a
r
e
n
t
parent
parent(双亲)、
k
e
y
key
key(关键字/节点值) ,如果二叉搜索树的某个节点的孩子节点或双亲节点不存在,其对应的属性值就为
N
o
n
e
None
None。(根节点是二叉搜索树中唯一父指针为
N
o
n
e
None
None的节点)
二叉搜索树支持的操作有:
S
E
A
R
C
H
SEARCH
SEARCH(查找)、
M
I
N
I
M
U
M
MINIMUM
MINIMUM(最小值)、
M
A
X
I
M
U
M
MAXIMUM
MAXIMUM(最大值)、
S
U
C
C
E
S
S
O
R
SUCCESSOR
SUCCESSOR(后继)、
P
R
E
D
E
C
E
S
S
O
R
PREDECESSOR
PREDECESSOR(前驱)等。
1 二叉搜索树的性质
性质:对于任何节点
x
x
x,其左子树中的关键字最大不超过
x
.
k
e
y
x.key
x.key,其右子树中的关键字最小不小于
x
.
k
e
y
x.key
x.key。不同的二叉搜索树可以代表同一组值的集合。大部分搜索树的操作的最坏运行时间与树的高度成正比。
二叉搜索树的关键字
k
e
y
key
key总是以满足二叉搜索树性质的方式来存储,设
x
x
x是二叉搜索树的一个节点,则:
- 如果 y y y是 x x x左子树中的一个节点,那么 y . k e y ≤ x . k e y y.key\le x.key y.key≤x.key。
- 如果 y y y是 x x x右子树中的一个节点,那么 y . k e y ≥ x . k e y y.key\ge x.key y.key≥x.key。
二叉搜索树的性质允许我们通过中序遍历( i n o r d e r t r e e w a l k inorder\ tree\ walk inorder tree walk)来输出二叉搜索树中的所有关键字。leetcode 94.二叉树的中序遍历可以用作练习。下面是中序遍历的递归版伪代码描述。(非递归需要借助栈的数据结构)
I N O R D E R − T R E E − W A L K ( T . r o o t ) INORDER-TREE-WALK(T.root) INORDER−TREE−WALK(T.root):中序遍历二叉树 T T T。
INORDER-TREE-WALK(x):
if x != None:
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)
end if
与中序遍历有关的二叉搜索树基本性质的练习还有:
- Leetcode 98.验证二叉搜索树
- Leetcode 99.恢复二叉搜索树
- Leetcode 173.二叉搜索树迭代器
- Leetcode 230.二叉搜索树中第K小的元素
- Leetcode 530.二叉搜索树的最小绝对差
- Leetcode 538.把二叉搜索树转换为累加树
- Leetcode 783.二叉搜索树结点最小距离
- Leetcode 938.二叉搜索树的范围和
2 查询二叉搜索树
我们经常需要查找一个存储在二叉搜索树中的关键字。这些查询操作在高度为 h h h的二叉搜索树上,都可以在 O ( h ) O(h) O(h)的时间内执行完毕。
2.1 查找( S E A R C H SEARCH SEARCH)
输入一个指向树根的指针和一个关键字 k k k,如果这个节点存在, T R E E − S E A R C H TREE-SEARCH TREE−SEARCH返回一个指向关键字为 k k k的节点的指针,否则返回 N o n e None None。算法伪代码如下:
T R E E − S E A R C H ( x , k ) TREE-SEARCH(x,k) TREE−SEARCH(x,k):找到关键字为 k k k的节点。(递归版本)
TREE-SEARCH(x,k):
if x == None or k == x.key:
return x
end if
if k < x.key:
return TREE-SEARCH(x.left, k)
else:
return TREE-SEARCH(x.right, k)
end if
I T E R A T I V E − T R E E − S E A R C H ( x , k ) ITERATIVE-TREE-SEARCH(x,k) ITERATIVE−TREE−SEARCH(x,k):找到关键字为 k k k的节点。(迭代版本)
ITERATIVE-TREE-SEARCH(x,k):
while x != None and k != x.key:
if k < x.key:
x = x.left
else:
x = x.right
end if
end while
return x
与查找有关的练习:
2.2 最大/最小关键字( M A X I M U M , M I N M U M MAXIMUM,MINMUM MAXIMUM,MINMUM)
根据二叉搜索树的性质,我们可以很容易地想到,整棵树中,一直向左的最后一个孩子的关键字就是最小的。相对地,一直向右的最后一个孩子的关键字就是最大的。算法的伪代码描述如下:
T R E E − M I N I M U M ( x ) TREE-MINIMUM(x) TREE−MINIMUM(x),返回二叉搜索树关键字最小的节点。
TREE-MINIMUM(x):
while x.left != None:
x = x.left
end while
return x
T R E E − M A X I M U M ( x ) TREE-MAXIMUM(x) TREE−MAXIMUM(x),返回二叉搜索树关键字最大的节点。
TREE-MAXIMUM(x):
while x.right != None:
x = x.right
end while
return x
2.3 后继和前驱( S U C C E S S O R , P R E D E C E S S O R SUCCESSOR,PREDECESSOR SUCCESSOR,PREDECESSOR)
给定二叉搜索树中的一个节点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个节点 x x x的后继是大于 x . k e y x.key x.key的最小关键字的节点。一棵二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个节点的后继。下面用伪代码来描述查找节点 x x x的后继:
T R E E − S U C C E S S O R ( x ) TREE-SUCCESSOR(x) TREE−SUCCESSOR(x),寻找节点 x x x的后继。
TREE-SUCCESSOR(x):
if x.right != None:
return TREE-MINIMUM(x.right)
end if
y = x.parent
while y != None and x == y.right:
x = y
y = y.parent
end while
return y
根据伪代码,可以看出 T R E E − S U C C E S S O R TREE-SUCCESSOR TREE−SUCCESSOR被分成了两个部分:
- 如果节点 x x x的右子树非空,那么 x x x的后继就是 x x x右子树中的最左节点 T R E E − M I N I M U M ( x . r i g h t ) TREE-MINIMUM(x.right) TREE−MINIMUM(x.right)
- 如果节点 x x x的右子树为空并且 x x x有一个后继 y y y,那么 y y y就是 x x x的最底层祖先,并且 y y y的左孩子也是 x x x的一个祖先。
要理解这两种情况只需要根据伪代码思考一下中序遍历的过程。
节点 x x x的前驱的查找与后继刚好是对称的,思考一下如何逆序输出一棵二叉搜索树,答案就是将中序遍历倒过来。书上并没有给出前驱的伪代码,这里做一下补充。伪代码如下:
T R E E − P R E D E C E S S O R ( x ) TREE-PREDECESSOR(x) TREE−PREDECESSOR(x),寻找节点 x x x的前驱。
TREE-PREDECESSOR(x):
if x.left != None:
return TREE-MAXIMUM(x.left)
end if
y = x.parent
while y != None and x == y.left:
x = y
y = y.parent
end while
return y
2.4 插入( I N S E R T INSERT INSERT)
插入操作会引起由二叉搜索树表示的动态集合的变化。一定要修改数据结构来反映这个变化,同时修改不能破坏二叉搜索树的性质。
将一个新的值
v
v
v插入到一个二叉搜索树
T
T
T中,需要调用过程
T
R
E
E
−
I
N
S
E
R
T
TREE-INSERT
TREE−INSERT。该过程以节点
z
z
z作为输入,其中
z
.
k
e
y
=
v
,
z
.
l
e
f
t
=
N
o
n
e
,
z
.
r
i
g
h
t
=
N
o
n
e
z.key=v,z.left=None,z.right=None
z.key=v,z.left=None,z.right=None。算法的伪代码描述如下:
T R E E − I N S E R T ( T , z ) TREE-INSERT(T,z) TREE−INSERT(T,z),将值为 v v v的节点 z z z插入树 T T T。
TREE-INSERT(T,z):
y = None
x = T.root
while x != None:
y = x
if z.key < x.key:
x = x.left
else:
x = x.right
end if
end while
z.p = y
if y == None:
T.root = z //树T为空树
else if z.key < y.key:
y.left = z
else:
y.right = z
end if
那么分析一下这个伪代码:
- 首先 y y y一直记录 x x x的父节点,代码的 w h i l e while while循环部分类似于 S E A R C H SEARCH SEARCH操作,找到 z z z应该插入的位置,最后 x x x为空的位置就是 z z z要插入的位置。
- 接下来判断了一下 y y y是否为空,如果为空说明是一棵空树。
- 最后判断 z z z应该是 y y y的左孩子还是右孩子。
二叉搜索树插入的练习:
2.5 删除( D E L E T E DELETE DELETE)
删除操作与插入操作同样需要修改数据结构,从一棵二叉搜索树 T T T中删除一个节点 z z z的整个策略分为三种基本情况:
- 如果 z z z没有孩子节点,那么只是简单地将它删除,并修改它的父节点,用 N o n e None None作为孩子来替换 z z z。
- 如果 z z z只有一个孩子,那么将这个孩子提升到树中 z z z的位置上,并修改 z z z的父节点,用 z z z的孩子来替换 z z z。
- 如果 z z z有两个孩子,那么找 z z z的后继 y y y(一定在 z z z的右子树中),并让 y y y占据树中 z z z的位置。 z z z的原来右子树部分称为 y y y 的新的右子树,并且 z z z的左子树成为 y y y的新的左子树。
前两种情况都很简单,第三种情况比较麻烦,因为还与 y y y是否为 z z z的右孩子相关。那么,结合图来具体分析一下上面三种情况。
(1)如果 z z z没有左孩子,那么用其右孩子来替换 z z z,这个右孩子可以是 N o n e None None,也可以不是。如果是 N o n e None None则对应于上述第一种情况,如果不是 N o n e None None则对应于上述第二种情况,如下图所示。
(2)如果
z
z
z只有一个孩子且为其左孩子,那么用左孩子来替换
z
z
z,如下图所示。
(3)如果
z
z
z有两个孩子,那么就要查找
z
z
z的后继
y
y
y,这个后继位于
z
z
z的右子树中,并且没有左孩子。需要将
y
y
y移出原来的位置进行拼接,并替换树中的
z
z
z。这时,如果
y
y
y是
z
z
z的右孩子,那么用
y
y
y替换
z
z
z,并仅留下
y
y
y的右孩子,如下图所示。
(4)接着(3),
y
y
y位于
z
z
z的右子树中但不是
z
z
z的右孩子。在这种情况下,先用
y
y
y的右孩子替换
y
y
y,然后再用
y
y
y替换
z
z
z,如下图所示。
为了在二叉搜索树中内移动子树,定义一个子过程 T R A N S P L A N T TRANSPLANT TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子节点。当 T R A N S P L A N T TRANSPLANT TRANSPLANT用一棵以 v v v为根的子树来替换一棵以 u u u为根的子树时,节点 u u u的双亲就变为节点 v v v的双亲,并且最后 v v v成为 u u u的双亲的相应孩子。算法伪代码描述如下:
T R A N S P L A N T ( T , u , v ) TRANSPLANT(T,u,v) TRANSPLANT(T,u,v),用 v v v替换 u u u。
TRANSPLANT(T,u,v):
if u.parent == None: //处理u为根节点的情况
T.root = v
else if u == u.parent.left:
u.parent.left = v
else:
u.parent.right = v
end if
if v != None: //允许v为None
v.parent = u.parent
end if
那么通过 T R A N S P L A N T TRANSPLANT TRANSPLANT以及查找节点后继的方法,我们就可以写出从二叉搜索树 T T T中删除节点 z z z的删除过程,伪代码描述如下:
T R E E − D E L E T E ( T , z ) TREE-DELETE(T, z) TREE−DELETE(T,z),从二叉搜索树 T T T中删除节点 z z z。
TREE-DELETE(T,z):
if z.left == None:
TRANSPLANT(T, z, z.right)
else if z.right = None:
TRANSPLANT(T, z, z.left)
else:
y = TREE-MINIMUM(z.right)
if y.parent != z:
TRANSPLANT(T, y, y.right)
y.right = z.right
y.right.parent = y
end if
TRANSPLANT(T, z, y)
y.left = z.left
y.left.parent = z
end if
二叉搜索树节点删除的相关练习: