目录
12.1 什么是二叉搜索树
12.1-1
12.1-2
12.1-3
12.1-4
12.1-5
12.2 查询二叉搜索树
12.2-1
12.2-2
12.2-3
12.2-4
12.2-5
12.2-6
12.2-7
12.2-8
12.2-9
12.3 插入和删除
12.3-1
12.3-2
12.3-3
12.3-4
12.3-5
12.3-6
12.4 随机构建二叉搜索树
12.4-1
12.4-2
12.4-3
12.4-4
思考题
12-1 带有相同关键字的二叉搜索树
12-2 基数树
12-3 随机构建二叉搜索树中的平均结点深度
12-4 不同二叉树的数目
对于关键字集合{1,4,5,10,16,17,21},分别画出高度为2、3、4、5和6的二叉搜索树。
最小堆性质:设x是最小堆中的一个结点。如果y是x子树中的一个结点,那么y.key≥x.key。
二叉搜索树性质:设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key。如果y是x右子树中的一个结点,那么y.key≥x.key。
不能使用最小堆性质在时间内按序输出一棵有n个结点树的关键字。
证明:假设能使用最小堆性质在时间内按序输出一棵有n个结点树的关键字,这也意味着给定一个最小堆,就能在时间内对其所有结点的关键字进行排序,但堆排序的时间复杂度是,所以假设不成立。因此,不能使用最小堆性质在时间内按序输出一棵有n个结点树的关键字。
设计执行中序遍历的非递归算法。
INORDER-TREE-WALK-ITERATION-WITH-STACK(x)
let S be a new stack
p = x
while p ≠ NIL or !STACK-EMPTY(S)
if p ≠ NIL
PUSH(S, p)
p = p.left
else p = POP(S)
print p.key
p = p.right
INORDER-TREE-WALK-ITERATION-WITHOUT-STACK(x)
cur = x
prev = NIL
while cur ≠ NIL
if cur.left == NIL
print cur.key
cur = cur.right
else prev = cur.left
while prev.right ≠ NIL and prev.right ≠ cur
prev = prev.right
if prev.right == NIL
prev.right = cur
cur = cur.left
else prev.right = NIL
print cur.key
cur = cur.right
对于一棵有n个结点的树,设计在时间内完成的先序遍历算法和后序遍历算法。
PREORDER-TREE-WALK(x)
if x ≠ NIL
print x.key
PREORDER-TREE-WALK-RECURSIVE(x.left)
PREORDER-TREE-WALK-RECURSIVE(x.right)
PREORDER-TREE-WALK-WITH-STACK(x)
let S be a new stack
p = x
while p ≠ NIL or !STACK-EMPTY(S)
if p ≠ NIL
print p.key
PUSH(S, p)
p = p.left
else p = POP(S)
p = p.right
PREORDER-TREE-WALK-WITHOUT-STACK(x)
cur = x
prev = NIL
while cur ≠ NIL
if cur.left == NIL
cur = cur.right
else prev = cur.left
while prev.right ≠ NIL and prev.right ≠ cur
prev = prev.right
if prev.right == NIL
print cur.key
prev.right = cur
cur = cur.left
else prev.right = NIL
cur = cur.right
POSTORDER-TREE-WALK(x)
if x ≠ NIL
PREORDER-TREE-WALK-RECURSIVE(x.left)
PREORDER-TREE-WALK-RECURSIVE(x.right)
print x.key
POSTORDER-TREE-WALK-WITH-STACK(x)
let S be a new stack
p = x
while p ≠ NIL or !STACK-EMPTY(S)
if p ≠ NIL
p.visited = 1
PUSH(S, p)
p = p.left
else p = POP(S)
if p.visited == 1
p.visited = p.visited + 1
PUSH(S, p)
p = p.right
elseif p.visited == 2
print p.key
p = NIL
PRINT-REVERSE(from, to)
if from != to
PRINT-REVERSE(from.right, to)
print from.key
POSTORDER-TREE-WALK-WITHOUT-STACK(x)
dump.left = x
cur = dump
prev = NIL
while cur ≠ NIL
if cur.left == NIL
cur = cur.right
else prev = cur.left
while prev.right ≠ NIL and prev.right ≠ cur
prev = prev.right
if prev.right == NIL
prev.right = cur
cur = cur.left
else PRINT-REVERSE(cur.left, prev)
prev.right = NIL
cur = cur.right
证明:考虑一棵高度为h、具有l个可达叶结点的决策树,它对应一个对n个元素所做的比较排序。因为输入数据的n!种可能的排列都是叶结点,所以有n!≤l。由于在一棵高为h的二叉树中,叶结点的数目不多于,得到:,对该式两边取对数,有。所以,任何基于比较的算法从n个元素的任意序列中构造一棵二叉搜索树,其最坏情况下需要的时间。
(c)和(e)序列不是查找过的序列。把这五个序列各插入到初始为空的二叉搜索树中,可以发现(c)和(e)序列形成的不是一条路径,(c)中的912和(e)中的299都是分支。因为查找过程是从上到下一条路径查找的,所以它们是不会被查找到的,可见(c)和(e)序列不是查找过的序列。
快速判断方法:将序列分成两部分,第一部分是小于目标数值的数值,第二部分是大于目标数值的数值。第一部分必须单调递增,第二部分必须单调递减,违反这个规定的就不是查找过的序列。
写出TREE-MINIMUM和TREE-MAXIMUM的递归版本。
RECURSIVE-TREE-MINIMUM(x)
if x.left == NIL
return x
else return RECURSIVE-TREE-MINIMUM(x.left)
RECURSIVE-TREE-MAXIMUM(x)
if x.right == NIL
return x
else return RECURSIVE-TREE-MAXIMUM(x.right)
写出过程TREE-PREDECESSOR的伪代码。
TREE-PREDECESSOR(x)
if x.left ≠ NIL
return TREE-MAXIMUM(x.left)
y = x.p
while y ≠ NIL and x == y.left
x = y
y = y.p
return y
在图12-2中的二叉搜索树中查找关键字20,A={2,3,4,6,7,9,13,17},B={15,18,20},C={},存在,但,所以该教授这个论断不成立。
证明:如果一棵二叉搜索树中的一个结点有两个孩子,那么它的后继为它的右子树中的最小值,所以它的后继没有左孩子,它的前驱为它的左子树中的最大值,所以它的前驱没有右孩子。
证明:因为T中所有的关键字互不相同,所以y是大于x.key的最小关键字的结点。
因此,如果T中一个结点x的右子树为空,且x有一个后继y,那么y一定是x的最底层祖先,并且其左孩子也是x的祖先。(注意到,每个结点都是它自己的祖先。)
证明:根据算法可知,每条边被遍历了两次,第一次向下,第二次向上。因为二叉搜索树中有n个结点,也即有n-1条边,共访问了2(n-1)次,所以该算法的运行时间为。
证明:在一棵高度为h的树上,TREE-SUCCESSOR的运行时间为,因为该过程或者遵从一条简单路径沿树向上或者遵从简单路径沿树向下。根据12.2-7,k次连续的TREE-SUCCESSOR调用的运行时间是。结合这两者可得,在一棵高度为h的二叉搜索树中,不论从哪个结点开始,k次连续的TREE-SUCCESSOR调用所需时间为。
证明:因为T是一棵二叉搜索树,其关键字互不相同,又因为x是一个叶结点,y为其父结点,所以y为x的前驱或后继。
因此,y.key或者是T树中大于x.key的最小关键字,或者是T树中小于x.key的最大关键字。
TREE-INSERT过程的一个递归版本。
RECURSIVE-TREE-INSERT(x, z)
y = x
if z.key < x.key
x = x.left
else x = x.right
if x ≠ NIL
RECURSIVE-TREE-INSERT(x, z)
else z.p = y
if y == NIL
T.root = z // tree T was empty
elseif z.key < y.key
y.left = z
else y.right = z
证明:假设通过反复向一棵树中插入互不相同的关键字来构造一棵二叉搜索树,在这棵树中查找关键字所经过的路径和先前插入这个关键字所经过的路径相同,唯一不同的就是多检查了目标结点的关键字与目标关键字是否相等。因此,在这棵树中查找关键字所检查过的结点数目等于先前插入这个关键字所检查的结点数目加1。
对于给定的n个数的集合,可以通过先构造包含这些数据的一棵二叉搜索树(反复使用TREE-INSERT逐个插入这些数),然后按中序遍历输出这些数的方法,来对它们排序。这个排序算法的最坏情况是得到一个链表,运行时间为;最好情况是得到一个满二叉树,运行时间为。
删除操作不可交换。反例如下:
假设为每个结点换一种设计,将属性x.p替换为x.succ。给出使用这种表示法的二叉搜索树T上SEARCH、INSERT和DELETE操作的伪代码。这些伪代码应在时间内执行完,其中h为树T的高度。(提示:应该设计一个返回某个结点的双亲的子过程。)
TREE-SEARCH(x, k)
if x == NIL or k == x.key
return x
if k < x.key
return TREE-SEARCH(x.left, k)
else return TREE-SEARCH(x.right, k)
TREE-INSERT(T, z)
y = NIL
x = T.root
while x ≠ NIL
y = x
if z.key < x.key
x = x.left
else x = x.right
if y == NIL
T.root = z
elseif z.key < y.key
y.left = z
z.succ = y
else y.right = z
z.succ = y.succ
y.succ = z
PARENT(x)
z = x.succ
y = z.left
while y ≠ x
z = y
y = y.right
return z
TRANSPLANT(T, u, v)
p = PARENT(u)
if p == NIL
T.root = v
elseif u == p.left
p.left = v
v.succ = p
else p.right = v
p.succ = v
TREE-DELETE(T, z)
if z.left == NIL
TRANSPLANT(T, z, z.right)
elseif z.right == NIL
TRANSPLANT(T, z, z.left)
else y = TREE-MINIMUM(z.right)
if z ≠ PARENT(y)
TRANSPLANT(T, y, y.right)
y.right = z.right
y.succ = z.right
TRANSPLANT(T, z, y)
y.left = z.left
y.left.succ = y
当TREE-DELETE中的结点z有两个孩子时,选择结点y作为它的前驱,而不是作为它的后继。如果这样做,TREE-DELETE如下所示:
TREE-DELETE(T, z)
if z.left == NIL
TRANSPLANT(T, z, z.right)
elseif z.right == NIL
TRANSPLANT(T, z, z.left)
else y = TREE-MAXIMUM(z.left)
if y.p ≠ z
TRANSPLANT(T, y, y.left)
y.left = z.left
y.left.p = y
TRANSPLANT(T, z, y)
y.right = z.right
y.rignt.p = y
一些人提出了一个公平策略,为前驱和后继赋予相等的优先级,这样得到了较好的试验性能。对TREE-DELETE进行修改来实现这样一种公平策略。
TREE-DELETE(T, z)
if z.left == NIL
TRANSPLANT(T, z, z.right)
elseif z.right == NIL
TRANSPLANT(T, z, z.left)
elseif RANDOM(1, 2) == 1
y = TREE-MINIMUM(z.right)
if y.p ≠ z
TRANSPLANT(T, y, y.right)
y.right = z.right
y.right.p = y
TRANSPLANT(T, z, y)
y.left = z.left
y.left.p = y
else y = TREE-MAXIMUM(z.left)
if y.p ≠ z
TRANSPLANT(T, y, y.left)
y.left = z.left
y.left.p = y
TRANSPLANT(T, z, y)
y.right = z.right
y.rignt.p = y
证明:假设有n+3个球,编号为1~n+3,选择其中4个球有种不同的方案。设4个球中编号最大的球的号码为i,则另外3个球有种不同的选择方案。因为i的取值范围是4~n+3,所以这4个球共有种不同的选择方案。因此,。
在一棵n个结点的二叉搜索树中,先让其中的个结点组成一棵满二叉树,然后把剩下的个结点在某个叶结点后连成一条单链。这棵树中结点的平均深度为,这棵树的高度是。
一棵有n个结点的二叉搜索树中结点的平均深度为,设这棵树高度为h。存在一条从根结点到深度为h的叶结点的路径,这条路径上的结点的深度为0,1,...,h。令S为这条路径上结点的集合,T为所有其他结点的集合,则这棵树中结点的平均深度为。所以,这棵树高度的一个渐进上界为。
下图中6棵二叉搜索树即是随机构建二叉搜索树的结果,可以发现{2,1,3}和{2,3,1}得到的二叉搜索树是一样的。所以在随机选择二叉搜索树中,每种二叉搜索树被选择的可能都是1/5,但在随机构建二叉搜索树中,{2,1,3}和{2,3,1}得到的二叉搜索树被选择的可能是2/6,其余每种二叉搜索树被选择的可能是1/6。
证明:因为对于所有的实数x都大于0,所以函数是凸的。
a.当用TREE-INSERT将n个其中带有相同关键字的数据插入到一棵初始为空的二叉搜索树中时,其渐进性能是。
建议通过在第5行之前测试z.key=x.key和在第11行之前测试z.key=y.key的方法,来对TREE-INSERT进行改进。如果相等,根据下面的策略之一来实现。对于每个策略,得到将n个其中带有相同关键字的数据插入到一棵初始为空的二叉搜索树中的渐进性能。(对第5行描述的策略是比较z和x的关键字,用于第11行的策略是用y代替x。)
b.在结点x设置一个布尔标志x.b,并根据x.b的值,置x为x.left或x.right。当插入一个与x关键字相同的结点时,每次访问x时交替地置x.b为FALSE或TRUE。
TREE-INSERT(T, z)
y = NIL
x = T.root
while x ≠ NIL
y = x
if z.key < x.key
x = x.left
elseif z.key == x.key
if x.b
x = x.left
else x = x.right
x.b = !x.b
else x = x.right
z.p = y
if y == NIL
T.root = z // tree T was empty
elseif z.key < y.key
y.left = z
elseif z.key == y.key
if y.b
y.left = z
else y.right = z
y.b = !y.b
else y.right = z
c.在x处设置一个与x关键字相同的结点列表,并将z插入到该列表中。
TREE-INSERT(T, z)
y = NIL
x = T.root
while x ≠ NIL
y = x
if z.key < x.key
x = x.left
elseif z.key == x.key
z.next = x.next
x.next = z
return
else x = x.right
z.p = y
if y == NIL
T.root = z // tree T was empty
elseif z.key < y.key
y.left = z
else y.right = z
d.随机地置x为x.left或x.right。
TREE-INSERT(T, z)
y = NIL
x = T.root
while x ≠ NIL
y = x
if z.key < x.key
x = x.left
elseif z.key == x.key
if RANDOM(1, 2) == 1
x = x.left
else x = x.right
else x = x.right
z.p = y
if y == NIL
T.root = z // tree T was empty
elseif z.key < y.key
y.left = z
elseif z.key == y.key
if RANDOM(1, 2) == 1
y.left = z
else y.right = z
else y.right = z
使用一棵基数树在时间内按字典序对S进行排序。
RADIX-TREE-INSERT(T, s)
x = T.root
for i = 0 to s.length-1
if s[i] == 0
if x.left == NIL
p = new node
p.p = x
x.left = p
x = x.left
else if x.right == NIL
p = new node
p.p = x
x.right = p
x = x.right
x.str = s
BUILD-RADIX-TREE(S)
let T be a new radix tree
for i = 1 to S.length
RADIX-TREE-INSERT(T, S[i])
PRINT(x)
if x ≠ NIL
if x.str
print x.str
PRINT(x.left)
PRINT(x.right)
a.证明:根据定义,一棵二叉树T的路径总长度为T中所有结点x的深度之和,对每个结点x的深度表示为d(x,T),所以。因此,T中的一个结点平均深度是。
b.证明:因为树T的左子树中的每个结点在中的深度比在T中少1,树T的右子树中也是如此。如果T有n个结点,则和共有n-1个结点,因为根结点的深度为0,所以。
c.证明:设T表示有n个结点的随机构建二叉搜索树,和分别表示树T的左子树和右子树。假设中有i个结点,则中有n-i-1个结点,因为i的取值范围是0~n-1,P(n)表示有n个结点的随机构建二叉搜索树的平均路径长度,所以。
e.证明:令,对于某个恰当选出的正常数a,假定对于所有正数i
f.请给出快速排序的一种实现,使快速排序中对一组元素的比较与将这些元素插入一棵二叉搜索树中所需的比较恰好相同。(这些比较的次序可以不同,但出现的比较一定要一样。)
PARTITION(A, p, r)
x = A[p]
i = p
for j = p+1 to r
if A[j] ≤ x
i = i + 1
exchange A[i] with A[j]
exchange A[i] with A[p]
return i
QUICKSORT(A, p, r)
if p < r
q = PARTITION(A, p, r)
QUICKSORT(A, p, q-1)
QUICKSORT(A, q+1, r)
a.证明:设T表示含有n个结点的二叉树,和分别表示树T的左子树和右子树。假设中有k个结点,则中有n-1-k个结点。因为k的取值范围是0~n-1,表示含有n个结点的不同二叉树的数目,所以。