今天终于静下心来去看《算法导论》,今天看的是第12章–二叉搜索树,下面是晚上回来后的总结。
多年以后在某个地方我会想念此时,那个孤独,奋斗与充满激情的自己
什么是二叉搜索树
一棵二叉搜索树是以一棵二叉树来组织的。一棵二叉搜索树可以使用一个链表数据结构来表示,其中每个节点就是一个对象。除了key和卫星数据之外,每个结点还包含属性left、right和p,它们分别指向结点的左孩子、右孩子和双亲,结点的结构类似于下面的结构:
node {
key,
卫星数据,
left,
right,
p
}
如果某个孩子结点和父节点不存在,则相应属性的值为NIL,根节点是树中唯一父指针为NIL的结点。
对于一棵二叉搜索树的任何结点x,满足这样的性质:
设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key<=x.key
。如果y是x的右子树中的一个结点,那么y.key>=x.key
。
这个性质对树中的每个结点都成立。
二叉搜索树的遍历
二叉搜索树性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字。根据输出的子树根的关键字位于其左子树的关键字值和右子树关键字值的位置,可以将遍历分为前序遍历,中序遍历,后序遍历。
以中序遍历为例:
INORDER-TREE-WALK(x)
if x ≠ NIL
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)
遍历一棵有n个结点的二叉搜索树需要耗费θ(n)的时间。
结论:
如果x是一棵有n个结点子树的根,那么调用
INORDER-TREE-WALK(x)
需要θ(n)时间
查询二叉搜索树
我们经常需要查找一个存储在二叉搜索树中的关键字。除了SEARCH操作之外,二叉搜索树还能支持诸如MINIMUM、MAXUIMUM、SUCCESSOR和PREDECESSOR的查询操作。
查找
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)
迭代版本:
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
最大关键字元素和最小关键字元素
TREE-MINIMUM(x) while x.left ≠ NIL x = x.left return x
返回以给定结点x为根的子树中的最小元素的指针,这里假设x不为NIL。
二叉搜索树的性质保证了
TREE-MINIMUM
是正确的。如果结点x没有左子树,那么由于x右子树中的每个关键字都大于或等于x.key,则以x为根的子树中的最小关键字是x.key。最大关键字元素与最小关键字元素是对称的:
TREE-MAXIMUM(x) while x.right ≠ NIL x = x.right return x
结论:
在一棵高度为h的二叉搜索树上,动态集合上的操作SSEARCH、MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR可以在O(h)时间内完成
前驱和后继
后继
给定一个二叉搜索树中的一个结点,有时候需要按中序遍历的次序查找它的后继,如果所有的关键字互不相同,则一个结点的后继是大于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
把TREE-SUCCESSOR(x)
的伪代码分为两种情况:
如果结点x的右子树非空,那么x的后继恰是x右子树的最左结点
另一方面,如果x的右子树为空并有一个后继y,那么y就是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
插入与删除
插入与删除操作会引起由二叉树表示的动态集合的变化。一定要修改数据结构来反映这个变化,但修改要保持二叉搜索树性质的成立。
插入
TREE-INSERT(T,z)
y = NIL
x = T.root
while x ≠ NIL
y = x // 保存每次循环的x,退出循环时,y保存的是z的父结点
if z.key < x.key
x = x.left
else x = x.right
z.p = y
if y == NIL
T.root = z
elseif z.key < y.key
y.left = z
else y.right = z
在二叉搜索树中插入元素每次都是把该元素插入到叶子结点的位置,所以while x ≠ NIL
会一直到x == NIL
时退出,此时x的位置就是插入元素要放置的位置。
删除
总的来说,从一颗二叉树搜索树T中删除一个结点z的整个策略分为三种基本情况:
如果z没有孩子结点,那么只是简单的将它删除,并修改它的父节点,用NIL作为孩子来替换z
如果z只有一个孩子,那么将这个孩子提升到树中z的位置上,并修改z的父节点,用z的孩子来替换z
如果z有两个孩子,那么找z的后继y(一定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分成为y的新的右子树。并且z的左子树成为y的新的左子树
从一棵二叉搜索树T中删除一个给定的结点z,这个过程取指向T
和z的指针
作为输入参数:
如果z没有左孩子结点(a图),那么用其右孩子结点来替换z,这个右孩子可以是NIL,也可以不是。当右孩子是NIL时,此时这种情况归为z没有孩子结点的情形。当z的右孩子非NIL时,这种情况就是z仅有一个孩子结点的情形,该孩子是其右孩子。
如果z仅有一个孩子且为其左孩子(b图),那么用其左孩子来替换z。
否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子。现在需要将y移出原来的位置进行拼接,并替换树中的z。
如果y是z的右孩子(c图),那么用y替换z,并仅留下y的右孩子。
否则,y位于z的右子树中但不是z的右孩子(d图)。在这种情况下,先用y的右孩子替换y,然后再用y替换z。
为了在二叉搜索树内移动子树,定义一个过程TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子结点。当TRANSPLANT用一棵以v为根的子树来替换一棵以u为根的子树时,结点u的双亲就变为结点v的双亲,并且最后v成为u的双亲的相应孩子。
TRANSPLANT(T,u,v)
if u.p == NIL
T.root = v
elseif u == u.p.left
u.p.left = v
elseif u.p.right = v
if v ≠ NIL
v.p = u.p
利用现成的TRANSPLANT
过程,下面是从二叉搜索树T中删除结点z的删除过程:
TREE-DELETE(T,z)
if z.left == NIL
TRANAPLANT(T,z,z.right)
elseif z.right == NIL
TRANSPLANT(T,z,z.left)
else y = TREE-MINIMUM(z.right)
if y ≠ z.right
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
代码(Java语言实现)
package algorithm;
/**
* 结点元素
*/
class Item {
int key;
Item p;
Item left;
Item right;
public Item(int key) {
this.key = key;
}
}
/**
* 定义一个二叉搜索树
*/
class Tree {
// 树的根部
Item root;
// 树的其他属性
// ...
// ...
}
/**
* 二叉搜索树的插入,删除
*/
public class Tree_insert {
private Tree tree = new Tree();
public static void main(String[] args) {
// 针对不同的元素顺序,生成的二叉搜索树结构不同
int[] items = {6, 2, 5, 5, 8, 7};
Tree_insert ti = new Tree_insert();
Item item;
// 插入元素
for (int i = 0; i < items.length; i++) {
item = new Item(items[i]);
ti.TREE_INSERT(ti.tree, item);
}
// 中序遍历
ti.Inorder_tree(ti.tree.root);
// 删除根元素,第二个参数:必须是指向二叉搜索树中某一个结点的指针...,这里以根元素为例
ti.TREE_DELETE(ti.tree, ti.tree.root);
ti.Inorder_tree(ti.tree.root);
}
/**
* 中序遍历,其他的遍历只需调整if中的语句的顺序即可
*
* @param root
*/
private void Inorder_tree(Item root) {
if (root != null) {
Inorder_tree(root.left);
System.out.println(root.key);
Inorder_tree(root.right);
}
}
/**
* 插入结点元素
*
* @param tree
* @param item
*/
private void TREE_INSERT(Tree tree, Item item) {
Item x, y = null;
x = tree.root;
while (x != null) {
y = x;
if (x.key > item.key) {
x = x.left;
} else {
x = x.right;
}
}
item.p = y;
if (y == null) {
tree.root = item;
} else {
/*这里的判断要与前面while中的判断相同*/
if (y.key > item.key) {
y.left = item;
} else {
y.right = item;
}
}
}
/**
* 树中的最小值所在的结点的索引
*
* @param root
* @return
*/
private Item MINIMUM(Item root) {
if (root.left != null) {
root = root.left;
}
return root;
}
/**
* 删除元素
*
* @param tree
* @param item
*/
private void TREE_DELETE(Tree tree, Item item) {
if (item.left == null) {
TRANSPLANT(tree, item, item.right);
} else if (item.right == null) {
TRANSPLANT(tree, item, item.left);
} else {
Item y = MINIMUM(item.right);
if (y != item.right) {
TRANSPLANT(tree, y, y.right);
y.right = item.right;
y.right.p = y;
}
TRANSPLANT(tree, item, y);
y.left = item.left;
y.left.p = y;
}
}
/**
* 辅助函数,用v替换u
*
* @param tree
* @param u
* @param v
*/
private void TRANSPLANT(Tree tree, Item u, Item v) {
if (u.p == null) {
tree.root = v;
} else {
if (u == u.p.left) {
u.p.left = v;
} else {
u.p.right = v;
}
}
// 在这里对p已经做了处理
if (v != null) {
v.p = u.p;
}
}
}
课后习题补充
12.3-1 给出TREE-INSERT的过程的一个递归版本
private void REVER_TREE_INSERT(Item root, Item item){
if (root.key > item.key && root.left != null){
REVER_TREE_INSERT(root.left, item);
} else if (root.key < item.key && root.right != null){
REVER_TREE_INSERT(root.right, item);
} else { // 此处应该放到else块中,因为插入操作只需要插入一次
item.p = root;
if (item.key < root.key) {
root.left = item;
} else {
root.right = item;
}
}
}
程序与答案有出入,测试时,将后面的处理的部分放到else{}
才能正常工作。
获得的tips
对于一棵有n个结点的二叉搜索树,使用另一种方法来实现中序遍历:
先调用
TREE-MINIMUM
找到这棵树中的最小元素,然后调用n-1
次的TREE-SUCCESSOR
。