搜索树数据结构支持许多动态集合操作,包括SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT和DELETE等。因此,我们使用一棵搜索树既可以作为一个字典又可以作为一个优先队列。
二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有n个结点的完全二叉树来说,这些操作的最坏运行时间为O(lgn)。然而,如果这棵树是一条n个结点组成的线性链,那么同样的操作就要花费O(n)的最坏运行时间。随机构造一棵二叉搜索树的期望高度为O(lgn),因此这样一棵树上的动态集合的基本操作的平均运行时间是O(lgn)。
1. 什么是二叉搜索树
顾名思义,一棵二叉搜索树是以一棵二叉树来组织的。这样一棵树可以使用一个链表数据结构来表示,其中每个结点就是一个对象。除了key和卫星数据之外,每个结点还包含属性left、right和p,它们分别指向结点的左孩子、右孩子和双亲。如果某个孩子结点和父结点不存在,则相应属性值为NIL。根结点是树中唯一父指针为NIL的结点。
二叉搜索树中的关键字总是以满足二叉搜索树性质的方式来存储:
设x是二叉搜索树中的一个结点。如果y是x的左子树中的一个结点,那么y.key <= x.key。如果y是x的右子树中的一个结点,那么y.key >= x.key。
二叉搜索树的性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序遍历(inorder tree walk)算法。这样命名的原因是输出的子树根的关键字位于左子树的关键字值和右子树关键字值之间。调用下面的过程INORDER-TREE-WALK(T.root),就可以输出一棵二叉搜索树T中的所有元素:
INORDER-TREE-WALK(x)
1. if x != NIL
2. INORDER-TREE-WALK(x.left)
3. print x.key
4. INORDER-TREE-WALK(x.right)
定理:如果x是一棵有n个结点子树的根,那么调用INORDER-TREE-WALK(x)需要O(n)时间。
2. 查询二叉搜索树
查找
我们使用下面的过程在一棵二叉搜索树中查找一个具有给定关键字的结点。输入一个指向树根的指针和一个关键字k,如果这个结点存在,TREE-SEARCH返回一个指向关键字为k的结点的指针;否则返回NIL。
TREE-SEARCH(x, k)
1. if x == NIL or k = x.key
2. return x
3. if k < x.key
4. return TREE-SEARCH(x.left, k)
5. else return TREE-SEARCH(x.right, k)
我们可以采用while循环来展开递归,用一种迭代的方式重写这个过程。对于大多数计算机,迭代版本的效率要高得多。
ITERATIVE-TREE-SEARCH(x, k)
1. while x != NIL and k != x.key
2. if k < x.key
3. x = x.left
4. else x = x.right
5. return x
最大关键字元素和最小关键字元素
TREE-MINIMUM(x)
1. while x.left != NIL
2. x = x.left
3. return x
TREE-MAXIMUM(x)
1. while x.right != NIL
2. x = x.right
3. return x
后继和前驱
给定一棵二叉搜索树中的一个结点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个结点x的后继是大于x.key的最小关键字结点。一棵二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。如果后继存在,下面的过程将返回一棵二叉搜索树中的结点x的后继;如果x是这棵树中的最大关键字,则返回NIL。
TREE-SUCCESSOR(x)
1. if x.right != NIL
2. return TREE-MINIMUM(x.right)
3. y = x.p
4. while y != NIL and x == y.right
5. x = y
6. y = y.p
7. return y
同理,下面的程序返回前驱:
TREE-PREDECESSOR(x)
1. if x.left != NIL
2. return TREE-MAXIMUM(x.left)
3. y = x.p
4. while y != NIL and x == y.left
5. x = y
6. y = y.p
7. return y
定理:在一棵高度为h的二叉搜索树上,动态集合上的操作SEARCH、MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR可以在O(h)时间内完成。
3. 插入和删除
插入
要将一个新值v插入到一棵二叉搜索树T中,需要调用过程TREE-INSERT。该过程以结点z作为输入,其中z.key=v,z.left=NIL,z.right=NIL。这个过程要修改T和z的某些属性,来把z插入到树中的相应位置上。
TREE-INSERT(T, z)
1. y = NIL
2. x = T.root
3. while x != NIL
4. y = x
5. if z.key < x.key
6. x = x.left
7. else x = x.right
8. z.p = y
9. if y == NIL
10. T.root = z // tree T was empty
11. else if z.key < y.key
12. y.left = z
13. else y.right = z
删除从一棵二叉搜索树T中删除一个结点z的整个策略分为三种基本情况(如下所述),但只有一种情况有点棘手。
如果z没有孩子结点,那么只是简单地将它删除,并修改它的父结点,用NIL作为孩子来替换z。
如果z只有一个孩子,那么将这个孩子提升到z的位置上,并修改z的父结点,用z的孩子来替换z。
如果z有两个孩子,那么找z的后继y(一定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况稍显麻烦,因为还与y是否为z的右孩子有关。
从一棵二叉搜索树T中删除一个结点z,这个过程取指向T和z的指针作为输入参数,它与前面概括的三种情况略有不同:
如果z没有左孩子,那么直接用其右孩子来替换z,这个右孩子可以是NIL,也可以不是。当z的右孩子是NIL时,此时这种情况归为z没有孩子结点的情形。当z的右孩子非NIL时,这种情况就是z仅有一个孩子结点的情形,该孩子是其右孩子。
如果z仅有一个孩子且为其左孩子,那么用其左孩子来替换z。
否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子。现在需要将y移出原来的位置进行拼接,并替换树中的z。
如果y是z的右孩子,那么用y替换z,并仅留下y的右孩子。
为了在二叉搜索树内移动子树,定义一个子过程TRANSPLANT,它是用另一棵树替换一棵子树并成为其双亲的孩子结点。当TRANSPLANT用一棵以v为根的子树来替换一棵以u为根的子树时,结点u的双亲就变为结点v的双亲,并且最后v成为u的双亲的相应孩子。
TRANSPLANT(T, u, v)
1. if u.p == NIL
2. T.root = v
3. else if u == u.p.left
4. u.p.left = v
5. else u.p.right = v
6. if v != NIL
7. v.p = u.p
利用现成的TRANSPLANT过程,下面从二叉搜索树T中删除结点z的删除过程:
TREE-DELETE(T, z)
1. if z.left = NIL
2. TRANSPLANT(T, z, z.right)
3. else if z.right == NIL
4. TRANSPLANT(T, z, z.left)
5. else y = TREE-MINIMUM(z.right)
7. if y.p != z
8. TRANSPLANT(T, y, y.right)
9. y.right = z.right
9. y.right.p = y
10. TRANSPLANT(T, z, y)
11. y.left = z.left
12. y.left.p = y
定理:在一棵高为h的二叉搜索树上,实现动态集合操作INSERT和DELETE的运行时间均为O(h)。