前言
二叉搜索树(又名二叉查找树、二叉排序树)是一种可提供良好搜寻效率的树形结构,支持动态集合操作,所谓动态集合操作,就是Search、Maximum、Minimum、Insert、Delete等操作,二叉搜索树的基本操作所花费的时间与这棵树的高度成正比。一棵随机构造的二叉搜索树的期望高度为O(lgn),当然,在最坏情况下,即所有节点形成一种链式树结构,则需要O(n)时间。
二叉查找树
设x为二叉查找树中的一个结点。如果y是x的左子树中的一个结点,则key[y]≤key[x]。如果y是x的右子树中的一个结点,则key[x]≤key[y].
二叉搜索树的性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序算法(inorder tree walk)。
INORDER-TREE-WALK(x)
if x!=null
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)
- 先序遍历(preorder tree walk)中输出的根的关键字在其左右子树的关键字之前
- 后序遍历(postorder tree walk)输出的根的关键字在其左右子树的关键字值之后
练习
1-1
(略)
1-2 二叉查找树性质与最小堆(见6.1节)之间有什么区别?能否利用最小堆性质在O(n)时间内,按序输出含有n个结点的树中的所有关键字?行的话,解释该怎么做,不行的话,说明原因。
-
二叉查找树:根的左子树小于根,根的右子树大于根。而最小堆:根的左右子树均大于根。
-
不能。原因是含有n个结点的最小堆的结点key大小是根<左<右或者根<右<左,左右子树是无序的。导致结果就是不能按照树的前中后序遍历在O(n)时间内来有序的输出他们。
1-3 给出一个非递归的中序树遍历算法。
INORDER-TREE-WALK(T)
let S be an empty stack
current = T.root
done = 0
while !done
if current != NIL
PUSH(S, current)
current = current.left
else
if !S.EMPTY()
current = POP(S)
print current
current = current.right
else done = 1
1-4 对一棵含有n个结点的树,给出能在θ(n)时间内,完成前序遍历和后序遍历的递归算法。
PREORDER-TREE-WALK(x)
if x != NIL
print x.key
PREORDER-TREE-WALK(x.left)
PREORDER-TREE-WALK(x.right)
POSTORDER-TREE-WALK(x)
if x != NIL
POSTORDER-TREE-WALK(x.left)
POSTORDER-TREE-WALK(x.right)
print x.key
1-5 论证:在比较模型中,最坏情况下排序n个元素的时间为Ω(nlgn),则为从任意的n个元素中构造出一棵二叉查找树,任何一个基于比较的算法在最坏情况下,都要花Ω(nlgn)的时间。
构造二叉查找树的同时也是对一组杂乱无章的数据排序的过程,而基于比较的排序的时间为Ω(nlgn),所以构造这棵树也要Ω(nlgn)。
查询二叉查找树
查询包括查找某一个元素,查找最大、最小关键字元素,查找前驱和后继。根据二叉搜索树的性质:左子树 < 根 < 右子树,这样的操作很容易实现。
#查找
TREE-SEARCH(x, k)
if x==null || k==x.key
return x
if k<x.key
return TREE-SEARCH(x.left, k)
else
return TREE-SEARCH(x.right, k)
#最小关键字元素
TREE-MINIMUM(x)
while x.left!=null
x=x.left
return x
TREE-MININUM(x)
if x.left==null
return x
else
return TREE-MINIMUM(x.left)
## 最大关键字元素
TREE-MAXIMUM(x)
while x.right!=null
x=x.right
return x
TREE-MAXIMUM(x)
if x.right==null
return x
else
return MAXIMUM(x.right)
#前驱
TREE-PREDECESSOR(x)
if x.left!=null
return TREE-MAXIMUM(x.left)
y=x.p
while y!=null and x=y.left
x=y
y=y.p
return y
#后继
TREE-SUCCESSOR(x)
if x.right!=null
return TREE-MINIMUM(x.right)
y=x.p
while y!=null and x==y.right
x=y
y=y.p
return y
练习
2-1
- c. 911与912不符合二叉查找树规则。
- e.347与299不符合二叉查找树规则。
2-2
TREE-MINIMUM(x)
if x.left != NIL
return TREE-MINIMUM(x.left)
else return x
TREE-MAXIMUM(x)
if x.right != NIL
return TREE-MAXIMUM(x.right)
else return x
2-3
(正文有)
2-4
(略)
2-5
- 设这个结点为x其左孩子x1右孩子x2,则有key[x1]≤key[x]≤key[x2]。
- 若其后继x2有左子女x3,则key[x3]≤key[x2],而key[x的右子树]≥key[x]
- 所以key[x2]≥key[x3]≥key[x],那么x3就为结点x的后继而非x2了
2-6
- 若x是其后继y的左孩子,那么key[x]≤key[y],所以y是x的最低祖先,y的左孩子为x的祖先也就是x本身。
- 若x是其后继y的右孩子,那么key[x]≥key[y],这明显与后继定义矛盾,y是x的前驱。所以x不可能是后继y的右子树。
- 若x是y的左孩子y1的右孩子,那么有key[y1]≤key[x],后继y的左孩子y1是x的祖先,同时y是x的最低祖先。
2-7
通过这n-1结点查后继,每条边至多2次。即O(n)
2-8
每调用一次该函数就需要O(h)时间,调用k次就需要O(kh)时间。这种想法是没有深入分析题目中函数具体调用过程。如果明白12.2-7题目核心内容。就知道,除了第一次调用该函数需要O(h)时间外,其余的连续k-1次遍历了连续的k-1个结点,这k-1个结点有k-2个边,而每条边最多遍历2次。所以总时间T=O(h)+2(k-2)=O(h+k).
2-9
(略)
插入和删除
插入和删除操作会引起由二叉搜索树表示的动态集合的变化,一定要修改数据结构来反映这个变化,但修改要保持二叉搜索树的性质的成立。
插入
指针x记录了一条向下的简单路径,并查找输入项z要进行替换的null(这里隐含的意味也就是z一定是添加作为一个叶结点的)
TREE-INSERT(T, z)
y=null
x=T.root
while x!=null
y=x
if z.key<x.key
x=x.left
else
x=x.right
z.p=y
if y==null
T.root=z
elseif z.key<y.key
y.left=z
else
y.right=z
删除
从一棵二叉搜索树T中删除一个结点z的整个策略分为三种基本情况,但只有一种比较棘手:
- 如果z没有孩子结点,那么只是简单的将它删除,并修改它的父结点,用null作为孩子来替换z;
- 如果z只有一个孩子,那么将这个孩子提升到树的z的位置上,并修改z的父结点,用z的孩子来替换z;
- 如果z有两个孩子,那么找z的后继y(一定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况稍显麻烦,因为还与y是否为z的右孩子相关。
TREE-DELETE(T, z)
if z.left==null
TRANSPLANT(T, z, z.right)
else if z.right==null
TRANSPLANT(T, z, z.left)
else
y=TREE-MINIMUM(z.right)
TRANSPLANT(T, y, y.right)
y.right=z.right
y.right.p=y
TRANSPLANT(T, x, y)
y.left=z.left
y.left.p=y
TRANSPLANT(T, u, v)
if u.p==null
T.root=v
elseif u=u.p.left
u.p.left=v
else
u.p.right=v
if v!=null
v.p=u.p
练习
3-1 给出过程TREE-INSERT的一个递归版本
INSERT(p, x, z)
if x == NIL
z.p = p
if z.key < p.key
p.left = z
else p.right = z
else if z.key < x.key
INSERT(x, x.left, z)
else INSERT(x, x.right, z)
RECURSIVE-TREE-INSERT(T, z)
if T.root == NIL
T.root = z
else INSERT(NIL, T.root, z)
3-2 假设我们通过反复插入不同的关键字的做法来构造一棵二叉查找树。论证:为在树中查找一个关键字,所检查的结点数等于插入该关键字时所检查的结点数加1.
从插入和查找函数的while循环遍历结构来看是完全一样的,区别就是查找函数遍历到关键字位置后就结束了,而插入函数遍历到待插入关键字的位置前一个位置便停止遍历转而进行插入工作,所以比查找函数少遍历一个结点。
3-3 可以这样来对n个数进行排序;先构造一棵包含这些数的二叉查找树(重复应用TREE-INSERT来逐个地插入这些数),然后按中序遍历来输出这些数。这个排序算法的最坏情况和最好情况运行时间怎么样?
- 最坏情况是树的高度为n,此时T(n)=θ(1+2+…+n)+θ(n)=θ(n²)
- 最好情况是树的高度为h,此时T(n)=θ(lg1+lg2+…lgn)+θ(n)=lgn!+θ(n)=θ(nlgn)
3-4删除操作可交换的吗?(也就是说,先删除x,再删除y的二叉查找树与先删除y再删除x的一样)说明为什么是,或者给出一个反例。
不一样。
A C C
/ \ / \ \
B D B D D
/
C
A A D
/ \ \ /
B D D C
/ /
C C
3-5假设为每个节点换一种设计,将二叉树的parent指针替换为successor指针。试给出使用这种表示法的二叉搜索树T上SEARCH,INSERT和DELETE操作的伪代码。这些伪代码应在O(h)时间内执行完,其中h为T的高度。(提示:应该设计一个返回某个结点的双亲的子过程。)
PARENT(T, x)
if x == T.root
return NIL
y = TREE-MAXIMUM(x).succ
if y == NIL
y = T.root
else
if y.left == x
return y
y = y.left
while y.right != x
y = y.right
return y
INSERT(T, z)
y = NIL
x = T.root
pred = NIL
while x != NIL
y = x
if z.key < x.key
x = x.left
else
pred = x
x = x.right
if y == NIL
T.root = z
z.succ = NIL
else if z.key < y.key
y.left = z
z.succ = y
if pred != NIL
pred.succ = z
else
y.right = z
z.succ = y.succ
y.succ = z
TRANSPLANT(T, u, v)
p = PARENT(T, u)
if p == NIL
T.root = v
else if u == p.left
p.left = v
else
p.right = v
TREE-PREDECESSOR(T, x)
if x.left != NIL
return TREE-MAXIMUM(x.left)
y = T.root
pred = NIL
while y != NIL
if y.key == x.key
break
if y.key < x.key
pred = y
y = y.right
else
y = y.left
return pred
DELETE(T, z)
pred = TREE-PREDECESSOR(T, z)
pred.succ = z.succ
if z.left == NIL
TRANSPLANT(T, z, z.right)
else if z.right == NIL
TRANSPLANT(T, z, z.left)
else
y = TREE-MIMIMUM(z.right)
if PARENT(T, y) != z
TRANSPLANT(T, y, y.right)
y.right = z.right
TRANSPLANT(T, z, y)
y.left = z.left
3-6当TREE-DELETE中的结点z有两个子结点时,可以将其前驱(而不是后继)拼接掉。有些人提出一种公平的策略,即为前驱和后继结点赋予相同的优先级,从而可以得到更好的经验性能。那么,应如何修改TREE-DELETE来实现这样一种公平策略?
(略)
随机构建二叉搜索树
。。