二叉搜索树
搜索树数据结构支持许多动态集合操作,包括SEARCH/MINIMUM/MAXIMUM/PREDECESSOR/SUCCESSOR/INSERT/DELETE等。因此,我们是用一棵搜索树既可以作为一个字典又可以作为一个优先队列。
二叉搜索树的基本操作所花费的时间与这棵树的高度成正比(所以相同的数据,组织成二叉搜索树后,其高度越低,则用于搜索时效率越高)。一棵随机构造的二叉搜索树的期望高度为O(lgn),因此这样一棵树上的动态集合的基本操作的平均运行时间为O(lgn)。
实际上,我们并不能总是保证随机的构造二叉搜索树,然而可以设计二叉搜索树的变体,来保证基本操作具有好的最坏搜索情况。红黑树就是这样的一个变体,它的树高为O(lgn),B树也是一个变体,它特别适用于二次(磁盘)存储器上的数据库维护。
12.1 什么是二叉搜索树
一棵二叉搜索树是以一棵二叉树来组织的,这样一棵树可以使用一个链表数据结构来表示,其中每个结点就是一个对象。除了key和卫星数据以外,每个结点还包含属性left/right/parent。
二叉搜索树中的关键字总是以满足二叉搜索树性质的方式来存储:
设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key<=x.key,如果y是x右子树中的一个结点,那么y.key>=x.key。
二叉搜索树的性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序算法(inorder tree walk)。这样命名的原因是输出的子树根的关键字位于其左子树的关键字值和右子树的关键字值之间。(类似的,先序遍历(preorder tree walk)中输出的根的关键字在其左右子树的关键字之前,二后序遍历(postorder tree walk)输出的根的关键字在其左右子树的关键字值之后).
INORDER-TREE-WALK(x)
if x!=null
INORDER-TREE-WALK(x.left)
print x.key
INORDER-TREE-WALK(x.right)
12.2 查询二叉搜索树
查找
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-SEARCH的运行时间是O(h),其中h是这棵树的高度。
我们可以使用while循环来展开递归,用一种迭代方式重写这个过程,对于大多数计算机,迭代版本的效率要高很多。
ITERATIVE-TREE-SEARCH(x, k)
while x!=null and k!=x.key
if k<x.key
x=x.left
else
x=x.right
return x
(看到这里的迭代版本,知道函数式编程里面要用递归的一个原因大概和迭代的话肯定要有变量是var类型而不是val类型的,瞎说一点)。
最大关键字元素和最小关键字元素
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)
后继和前驱
给定一棵二叉搜索树中的一个结点,有时候需要按照中序遍历的次序查找它的后继,如果所有的关键字互不相同,则一个结点x的后继是大于x.key的最小关键字的结点。一个二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。
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
上面伪代码的意思是如果结点x的右子树非空,则x的后继恰是x的柚子树中的最左结点。如果结点x的右子树为空并有一个后继y,那么y就是x的最底层祖先(指的是最靠上的祖先),并且y的左孩子也是x的一个祖先。为了找到y,只需要从x开始沿树向上直到遇到这样一个结点:这个结点是它的parent的左结点。
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
12.3 插入和删除
插入和删除操作会引起由二叉搜索树表示的动态集合的变化,一定要修改数据结构来反映这个变化,但修改要保持二叉搜索树的性质的成立。
插入
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
指针x记录了一条向下的简单路径,并查找输入项z要进行替换的null(这里隐含的意味也就是z一定是添加作为一个叶结点的)。该过程保持遍历指针(trailing pointer)y作为x的parent。while循环是的y和x这两个指针沿树向下移动,向左或向右取决于z.key和x.key的比较,直到x变为null。这个null占据的位置就是输入项z要放置的地方。
删除
从一棵二叉搜索树T中删除一个结点z的整个策略分为三种基本情况,但只有一种比较棘手:
如果z没有孩子结点,那么只是简单的将它删除,并修改它的父结点,用null作为孩子来替换z;
如果z只有一个孩子,那么将这个孩子提升到树的z的位置上,并修改z的父结点,用z的孩子来替换z;
如果z有两个孩子,那么找z的后继y(一定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况稍显麻烦,因为还与y是否为z的右孩子相关。
为了在二叉搜索树内移动子树,定义一个子过程TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子结点。
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
利用线程的TRANSPLANT过程,下面是从二叉搜索树T中删除结点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