1、搜索树数据结构 支持 许多动态集合操作,包括 SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT 和 DELETE 等。使用搜索树 既可以作为一个字典 又可以作为一个优先队列
2、二叉搜索树上的基本操作 所花费的时间 与这棵树的高度成正比。对于有n个结点的 一棵完全的二叉搜索树来说,这些操作的最坏运行时间为 Θ(lgn),如果 这棵树是一条n个结点组成的线性链,那么 同样的操作 就要花费 Θ(n) 的最坏运行时间
1、什么是二叉搜索树
1、除了key和卫星数据之外,每个结点 还包含属性left、night和p,它们分别指向结点的左孩子、右孩子和双亲。根结点是树中唯一父指针为NIL的结点
对任何结点,其左子树中的关键字最大 不超过 x.key,其右子树中的关键字最小 不低于x.key。不同的二叉搜索树 可以代表同一组值的集合
2、中序遍历 就可以 从大到小输出 一棵二叉树T中的 所有元素
INORDER-TREE-WALK(x)
if x != NIL
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.night)
3、如果x是一棵 有n个结点子树的根,那么调用INORDER-TREE-WALK(x) 需要 Θ(n) 时间
证明:要访问 这棵子树的全部n个结点,所以有 T(n) = Ω(n),下面要证明 T(n) = O(n)
首先有 T(0) = c
当调用 INORDER-TREE-WALK作用在 一个结点x上,x结点左子树 有k个结点 且其右子树有 n - k - 1个结点,时间由 T(n) <= T(k) + T(n - k - 1) +d 的限界其中 常数d > 0。反映了执行的时间的上界,
使用 替换法,通过 证明 T(n) <= (c + d)n + c,可以证得 T(n) = O(n)。对于 n = 0,有 (c + d)*0 + c = c = T(0)。对于 n > 0,有
4、二叉搜索树性质与最小堆性质之间 有什么不同?能使用最小堆性质在 O(n) 时间内 按序输出一棵有n个结点树的关键字吗
最小堆性质:设x是最小堆中的一个结点。如果y是x子树中的一个结点,那么y.key≥x.key
二叉搜索树性质:设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key。如果y是x右子树中的一个结点,那么y.key≥x.key
不能使用最小堆性质在时间 O(n) 内按序输出一棵有n个结点树的关键字
证明:假设能使用最小堆性质在时间 O(n) 内按序输出一棵有n个结点树的关键字,这也意味着给定一个最小堆,就能在时间 O(n) 内对其所有结点的关键字进行排序,但堆排序的时间复杂度是 O(lgn) ,所以假设不成立
5、执行 中序追历的非递归算法
一种容易的方法是 使用栈作为辅助数据结构;另一种较复杂但比较简洁的做法是不使用栈,但要假设能测试两个指针是否相等
使用栈
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
1)不使用栈 中序遍历
如果当前节点的左孩子为空,则输出当前节点并将当前节点的右孩子作为“新的当前节点”
如果当前节点的左孩子不为空,在当前节点的左子树中 找到当前节点在中序遍历下的前驱节点
a) 如果前驱节点的右孩子为空,那么将 当前节点设置为前驱节点的右孩子。把当前节点的左孩子设置为“新的当前节点”
b) 如果前驱节点的右孩子为当前节点,将前驱节点的右孩子重新设为空(即恢复树的形状)。然后输出当前节点,并将当前节点的右孩子设置为“新的当前节点”
void inorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL)
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
// find predecessor
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL)
{
prev->right = cur;
cur = cur->left;
}
else
{
prev->right = NULL;
printf("%d ", cur->val);
cur = cur->right;
}
}
}
}
要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候 怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了 线索二叉树的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱 和 后继节点,只需要利用叶子节点中的左右空指针 指向某种顺序遍历下的前驱节点 或 后继节点 就可以了
线索二叉树是一种特殊的二叉树,它通过利用节点的空指针域存储线索,以便在不需要递归或使用栈的情况下实现对二叉树的快速遍历。线索二叉树包括前序线索二叉树、中序线索二叉树、后序线索二叉树等
2)不使用栈 前序遍历(线索二叉树)
与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序
如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点
如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点 在中序遍历下的前驱节点
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子
void preorderMorrisTraversal(TreeNode *root) {
TreeNode *cur = root, *prev = NULL;
while (cur != NULL)
{
if (cur->left == NULL)
{
printf("%d ", cur->val);
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL)
{
printf("%d ", cur->val); // the only difference with inorder-traversal
prev->right = cur;
cur = cur->left;
}
else
{
prev->right = NULL;
cur = cur->right;
}
}
}
}
3)不使用栈 后序遍历
Morris 后序遍历的步骤(适配后序遍历)
建立临时节点:建立一个临时节点 temp,令 temp.left 指向根节点
遍历节点:初始化 cur 指向 temp
左子树处理:
如果 cur.left 不为空,找到 cur 的左子树中的最右节点(即左子树的前驱节点)
如果前驱节点的右指针为空(即 prev.right == NULL),令 prev.right = cur,然后 cur = cur.left 继续处理左子树
如果前驱节点的右指针指向 cur(形成了一个环路,说明左子树已处理完成),则需要断开环路(prev.right = NULL),处理右子树之前,按逆序打印从 cur.left 到这个前驱之间的所有节点,再令 cur = cur.right
右子树处理:
当 cur.left 为空时,直接移动到 cur.right。
结束遍历:当 cur 为空时,结束遍历
逆序打印
逆序打印是后序遍历中使用 Morris 遍历时特有的一个步骤。从当前节点的左子节点出发到最右子节点的路径上的所有节点需要逆序打印,这模拟了后序遍历中“左右根”的顺序
这种方法避免了使用栈,但通过修改树的结构(临时修改右指针)来保存必要的信息。遍历完成后,树的结构被还原到其原始状态
void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
{
if (from == to)
return;
TreeNode *x = from, *y = from->right, *z;
while (true)
{
z = y->right;
y->right = x;
x = y;
y = z;
if (x == to)
break;
}
}
void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
{
reverse(from, to);
TreeNode *p = to;
while (true)
{
printf("%d ", p->val);
if (p == from)
break;
p = p->right;
}
reverse(to, from);
}
void postorderMorrisTraversal(TreeNode *root) {
TreeNode dump(0);
dump.left = root;
TreeNode *cur = &dump, *prev = NULL;
while (cur)
{
if (cur->left == NULL)
{
cur = cur->right;
}
else
{
prev = cur->left;
while (prev->right != NULL && prev->right != cur)
prev = prev->right;
if (prev->right == NULL)
{
prev->right = cur;
cur = cur->left;
}
else
{
printReverse(cur->left, prev); // call print
prev->right = NULL;
cur = cur->right;
}
}
}
}
对于一棵有个结点的树,请设计在 O(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
6、证明任何基于比较的算法从n个元素的任意序列中构造一棵二叉搜索树,其最坏情况下需要Ω(n lgn)
证明:
2、查询二叉搜索树
1、查找:输入一个指向树根的指针 和 一个关键字k,如果 这个结点存在,TREE-SEARCH 返回一个指向关键字为k的 结点指针;否则返回NIL
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-SEARCH 的运行时间为 O(h),其中 h 是这棵树的高度
采用 while 循环 来展开递归,用一种 迭代方式重写这个过程,效率更高
ITERATIVE-TREE-SEARCH(x.k)
while x != NIL and k != x.key
if k < x.key
x = x.left
else x = x.right
return x
2、最大关键字元素和最小关键字元素
TREE-MINIMUM(x)
while x.left != NIL
x = x.left
return x
TREE-MAXIMUM的伪代码是对称的
TREE-MAXIMUM(x)
while x.right != NIL
x = x.right
return x
这个过程 在一棵高度为h的树上 均能在 O(h) 时间内执行完
3、后继和前驱
按中序顺序的次序 查找他的后继,则 一个结点x的后继 是大于 x.key的最小关键字的结点
TREE-SUCCESSOR(x)
if x.right != NIL
return TREE-MINIMUM(x.right)
y = x.p
while y != NIL and x == y.right // 还是右子树的关系,要找到左子树就停了
x = y
y = y.p
return y
如果 结点x的右子树非空,那么x的后继结点 恰是x右子树中的 最左节点
如果结点x(13)的右子树为空 并有一个后继y(15),那么 y就是x的有左孩子的 最底层祖先,而且 它也是x的一个祖先。为了找到y,只需简单地从x开始 沿树而上 直到 遇到一个双亲有左孩子的结点
该过程 或者遵从一条简单路径 沿树而上 或者 遵从简单路径 沿树而下
求前驱跟后继代码对称
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
4、在一棵高度为h的二叉搜索树上,动态集合上的操作 SEARCH、MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR可以在 O(h) 时间内完成
5、考虑一棵二叉搜索树T,其关键字 互不相同。证明:如果T中 一个结点x的右子树为空,且x有一个后继y,那么 y一定的最底层祖先,并且其左孩子 也是x的祖先
(注意到,每个结点都是它自己的祖先)
证明:因为T中所有的关键字互不相同,所以y是大于x.key的最小关键字的结点。
- 如果y不是x的最底层祖先,则y的左孩子的关键字大于x.key小于y.key,此时y不是大于x.key的最小关键字的结点,与条件矛盾,所以y是x的最底层祖先。
- 如果y的左孩子不是x的祖先,因为每个结点都是它自己的祖先,则x是y的右子树中的一个结点,此时y的关键字小于x.key,与条件矛盾,所以y的左孩子也是x的祖先。
6、用另一种方法 实现n个结点的二叉搜索树的中序遍历:TREE-MINIMUM找到这棵树中的最小元素,然后再调用 n-1 次的TREE-SUCCESSOR 证明 该算法的运行时间为 Θ(n)
证明:根据算法可知,每条边被遍历了两次,第一次向下,第二次向上。因为二叉搜索树中有 n 个结点,也即有 n-1 条边,共访问了 2(n-1) 次,所以该算法的运行时间为 Θ(n)
7、在一棵高度为h的二叉搜索树中,不论从哪个结点开始,k次连续的TREE-SUCCESSOR调用所需时间为 O(k + h)
证明:在一棵高度为h的树上,TREE-SUCCESSOR的运行时间为 O(h),因为该过程 或者遵从一条简单路径沿树向上 或者遵从简单路径沿树向下。根据6,k次连续的TREE-SUCCESSOR调用的运行时间是 O(k)。结合这两者可得,在一棵高度为h的二叉搜索树中,不论从哪个结点开始,k次连续的TREE-SUCCESSOR调用所需时间为 O(k + h)
8、设T是一棵二叉搜索树,其关键字 互不相同,设x是一个叶结点,y为其父结点。证明:y.key 或者是 T树中大于x.key的最小关键字,或者 是T树中小于x.key的最大关键字
证明:因为T是一棵二叉搜索树,其关键字互不相同,又因为x是一个叶结点(没有左右孩子的),y为其父结点,所以y为x的前驱或后继
- 当x是y的左孩子时,y是x的后继,所以y.key是T树中大于x.key的最小关键字
- 当x是y的右孩子时,y是x的前驱,所以y.key是T树中小于x.key的最大关键字
因此,y.key或者是T树中大于x.key的最小关键字,或者是T树中小于x.key的最大关键字
3、插入和删除
1、插入一个新结点带来的树修改 要相对简单些,删除的处理 有些复杂
插入:将一个新值v插入到 一棵二叉搜索树中,以结点z作为输入,其中 z.key = v,z.left = NIL,z.right = NIL,只在叶子结点插入
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
z.p = y
if y == NIL
T.root = z // 树T为空
elseif z.key < y.key
y.left = z
else y.right = z
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
2、删除:
- 如果 z没有孩子结点,那么 只是简单地将他删除,并修改它的父结点,用 NIL 作为孩子来替换z
- 如果 z只有一个孩子,那么 将这个孩子 提升到 树中z的位置上,并修改z的父结点,用z的孩子 来替换z
- 如果 z有两个孩子,那么找z的后继y(一定在z的右子树中),并 让y占据树中z的位置。z的原来右子树部分 变为 y的新的右子树,并且 z的左子树 成为y的新的左子树。还与 y是否为z的右孩子相关
如果 z有两个孩子:
如果y是z的右孩子,用y替换z,并仅留下 y的右孩子(如c)
否则,位于z的右子树中 但并不是z的右孩子。先用 y的右孩子替换y,然后 再用y替换z(看起来做了旋转)(如d)
定义 一个子过程 TRANSPLANT,它是用 另一棵子树 替换 一棵子树并成为 其双亲的孩子结点
TRANSPLANT(T, u, v)
if u.p == NIL
T.root = v
elseif u == u.p.left
u.p.left = v
else u.p.right = v
if v != NIL
v.p = u.p
u是其双亲的左孩子 或 右孩子。TRANSPLANT 并没有处理 v.left 和 v.right 的更新;这些更新 由TRANSPLANT的调用者来完成
TREE-DELETE(T, x)
if z.left == NIL
TRANSPLANT(T, z, z.right)
elseif z.right == NIL
TRANSPLANT(T, z, z.left)
else y = TREE-MINIMUM(z.right) // y是后继,第5行
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
第5行查找结点y,它是z的后继。因为z的右子树非空,这样后继 一定是这个子树中 具有最小关键字的结点
因为y已经是最小的,所以 不会有左孩子。将y移出它的原来位置 进行拼接,并替换树中的z
如果 y是z的右孩子,那么就用 y替换z并成为z的双亲的一个孩子,用z的左孩子 替换y的右孩子
如果 y不是z的右孩子,用y的右孩子 替换y并成为y的双亲的一个孩子,然后 将z的左孩子 替换为 y的左孩子(旋转过程,把没有左孩子的y提上去),最后三行:用y替换z 并成为z的双亲的一个孩子,再用z的左孩子 替换为y的左孩子
除了 第5行调用 TREE-MINIMUM 之外,TREE-DELETE 的每一行,包括调用 TRANSPLANT ,都只花费 常数时间。因此,在一棵高度为h的树上,TREE-DELETE 的运行时间为 O(h)
3、在一棵高度为h的二叉搜索树上,实现动态集合操作INSERT 和 DELETE的运行时间 均为 O(h)
4、删除操作不可交换(可交换的含义是,先删除x再删除y留下的结果树 与 先删除y再删除x 留下的结果树完全一样)。反例如下:
也证明了 同样的序列(不同顺序删除后 最后的序列都是一样的)可以对应 不同的二叉搜索树
5、假设 为每个结点换一种设计,将属性x.p(双亲)替换为x.succ(后继)。给出使用这种表示法的二叉搜索树T上SEARCH、INSERT和DELETE操作的伪代码。这些伪代码应在O(h)时间内执行完,其中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 // 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 // z是y的父结点
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
6、当 TREE-DELETE 中的结点z有两个孩子时,选择结点y为它的前驱,而不是 后继。如果这样做,对TREE-DELETE应该做些什么必要的修改?一些人提出了一个公平策略,为前驱和后继赋予相等的优先级,这样得到了较好的实验性能。如何对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) // else才有区别
if y.p ≠ z
TRANSPLANT(T, y, y.left) // left和right反的
y.left = z.left
y.left.p = y
TRANSPLANT(T, z, y)
y.right = z.right
y.right.p = y
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
4、随机构建二叉搜索树
1、随着 元素的插入和删除,二叉搜索树的高度 是变化的。例如,如果个关键字 按严格递增的次序被插入,则这棵树 一定是高度为 n-1 的一条链
定义n个关键字 的一棵初始的空树中 而生成的树,这里输入关键字的 n! 个排列中的每个 都是等可能地出现
2、一棵 有n个不同关键字的 随机构建二叉搜索树的期望高度为 O(lg n)
证明:从定义三个随机变量开始,用 Xn 表示一棵 有n个不同关键字的 随即构建二叉树的高度,并定义 指数高度 Yn = 2Xn
选择 一个关键字作为 树根,并设 Rn为一个随机变量,表示 这个关键字在n个关键字排好序后 应占据的位置
如果 Rn = i,那么 根的左子树是一棵 有i - 1个关键字的 随机构建二叉搜索树,右子树 是一棵有 n - i 个关键字的 随机构建二叉搜索树
因为 二叉树的高度 比根的两棵子树较高的 那棵子树大1,因此 二叉树的指数高度是 根的两棵子树较高的 那棵子树的2倍。如果 Rn = i,则有
设 Yi = 1,1个结点的树的指数高度是 20=1,定义 Y0 = 0
定义 指示器随机变量 Zn,1,Zn,2,…,Zn,n,其中 Zn,i = I{Rn = i}。因为 Rn 对集合 {1, 2, …,n} 中的任何元素 都是等可能的(选择单个元素的位置,而不是整个序列)
12.3证明
有了 E[Yn] 界,但 最终的目标是 要得到 E[Xn] 的界,因为 f(x) = 2x是凸的,使用 Jensen不等式
得到 E[Xn] = O(lgn)
1)Jensen不等式
2)凸函数
3)判断凸函数的方法