搜索树数据结构支持许多动态及和操作,包括 SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT 和 DELETE 等。因此,我们使用一棵搜索树既可以作为一个字典,又可以作为一个优先队列。
二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势,所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
二叉搜索树(Binary Search Tree),也叫二叉查找树和二叉排序树(Binary Sort Tree)。
定义一
二叉搜索树或者是空树,或者是满足下列性质的二叉树:
定义二
二叉搜索树或者是空树,或者是满足下列性质的二叉树:
定义三
二叉搜索树或者是空树,或者是满足下列性质的二叉树:
以上的三种定义在不同的数据结构教材中均有不同的定义方式,但是都是正确的,在开发时需要根据不同的需求进行选择。
下面使用定义一来对二叉搜索树进行介绍。
(1)按中序遍历二叉搜索树所得的中序序列是一个递增的有序序列,因此,二叉查找树可以把无序序列变为有序序列。
如果 x x x 是一棵有 n n n 个结点子树的根,那么对其进行中序遍历得到递增的有序序列的时间复杂度为 O ( n ) O(n) O(n)。也就是说可以在 O ( n ) O(n) O(n) 的时间内按序输出树中的所有关键字。
Q: 二叉搜索树与最小堆有什么区别?能使用最小堆在 O ( n ) O(n) O(n) 时间内按序输出一棵有 n n n 个结点树的关键字吗?
二叉搜索树:根的左子树的所有节点的值都小于根,右子树的所有节点的值都大于根;最小堆:根的左右子树均大于根。
不能,最小堆所有根的左右子树根节点的值是无序的,所以不能按照树的前中后序遍历在 O ( n ) O(n) O(n) 时间内来有序地输出;而二叉查找树,按照中序遍历正好是 左 < 根 < 右,是有序的。
(2)同一个数据集合,可按关键字表示成不同的二叉搜索树,即同一数据集合的二叉搜索树不唯一,但中序序列相同。
如关键字集合 1,4,5,10,16,17,21
,可以构建出多种二叉搜索树,其中两种如下所示:
我们经常需要查找一个存储在二叉搜索树中的关键字。除了 SEARCH 操作之外,二叉搜索树还能支持诸如 MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR 查询操作。
二叉搜索树的高度为 h h h,这里介绍如何在 O ( h ) O(h) O(h) 的时间内执行完每个操作。这里按照《算法导论》中的介绍,每个树结点包含属性 l e f t , r i g h t , p left,right,p left,right,p,分别指向结点的左孩子、右孩子和双亲。
在一棵二叉搜索树中查找一个具有给定关键字的节点。这个过程从树根开始查找,并沿着这棵树中的一条简单路径向下进行,对于遇到的每个结点 x x x,比较关键字 k k k 与 x . k e y x.key x.key:
从树根开始递归期间遇到的结点就形成了一条向下的简单路径,所以该算法的时间复杂度为 O ( h ) O(h) O(h),其中 h h h 是这棵树的树高。
// 递归
TREE_SEARCH(x, k)
if x == NULL 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 != NULL and k != x.key
if k < x.key
x = x.left
else x = x.right
return x
通过从树根开始沿着 l e f t left left 孩子指针直到遇到一个 N U L L NULL NULL,我们总能在一棵二叉搜索树中找到一个最小元素。
// 迭代
TREE_MINIMUM(x)
while x.left != NULL
x = x.left
return x
// 递归
RECURSIVE_TREE_MINIMUM(x)
if x.left == NULL
return x
return RECURSIVE_TREE_MINIMUM(x.left)
二叉搜索树的性质保证了 T R E E _ M I N I M U M TREE\_MINIMUM TREE_MINIMUM 是正确的。如果结点 x x x 没有左子树,那么由于 x x x 右子树的每个关键字都至少大于或等于 x . k e y x.key x.key,则以 x x x 为根的子树中的最小关键字是 x . k e y x.key x.key;如果结点 x x x 有左子树,那么由于其右子树中没有关键字小于 x . k e y x.key x.key,且在左子树中的每个关键字不大于 x . k e y x.key x.key,则以 x x x 为根的子树中的最小关键字一定在以 x . l e f t x.left x.left 为根的子树中。
T R E E M A X I M U M TREE_MAXIMUM TREEMAXIMUM 的伪代码是对称的:
// 迭代
TREE_MAXIMUM(x)
while x.right != NULL
x = x.right
return x
// 递归
RECURSIVE_TREE_MAXIMUM(x)
if x.right == NULL
return x
return RECURSIVE_TREE_MINIMUM(x.right)
这两个过程在一棵高度为 h h h 的二叉搜索树上均能在 O ( h ) O(h) O(h) 的时间内执行完,因为它们所遇到的节点构成了一条从树根向下的简单路径。
这里就需要用到结点中存储的其父节点的指针。
假定树中的所有关键字都不同,而且树中节点中包括一个指向其父节点的指针,其树中某节点的后继结点的查询过程:
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
如上图,关键字为 15 的结点的后继是关键字为 17 的结点,也就是 15 右子树的最左结点;关键字为 13 的节点的后继是关键字为 15 的结点。
在一棵高度为 h h h 的树上, T R E E − S U C C E S S O R TREE-SUCCESSOR TREE−SUCCESSOR的运行时间为 O ( h ) O(h) O(h),因为该过程或者遵从一条简单路径沿树向上或者遵从简单路径沿树向下。
过程 T R E E − P R E D E C E S S O R TREE-PREDECESSOR TREE−PREDECESSOR 与 T R E E − S U C C E S S O R TREE-SUCCESSOR TREE−SUCCESSOR 是对称点,运行时间也是 O ( h ) O(h) O(h):
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
再如上图,关键字为 15 的结点的前驱是关键字为 13 的结点,也就是 15 左子树的最右结点;关键字为 9 的结点的前驱是关键字为 7 的结点。
结论:在一棵高度为 h h h 的二叉搜索树上,动态集合上的查找、最小(大)元素、前驱和后继操作可以在 O ( h ) O(h) O(h) 时间内完成。
二叉查找树的插入操作:
TREE_INSERT(T,z) // 将结点 z 插入到二叉搜索树 T 中, z.left = z.right = NULL
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 // tree T was empty
T.root = z
elseif z.key < y.key
y.left = z
elseif z.key > y.key
y.right = z
递归实现
TREE_INSERT(T,z) // 将结点 z 插入到以 x 为根的子树上
x = T.root
if x == NULL
T.root = z
elseif z.key < x.key
TREE_INSERT(x.left, z)
elseif z.key > x.key
TREE_INSERT(x.right, z)
与其他搜索树上的原始操作一样,过程 T R E E − I N S E R T TREE-INSERT TREE−INSERT 在一棵高度为 h 的树上的运行时间为 O ( h ) O(h) O(h),只需要从根开始沿树向下搜索,直到找到新节点要插入的位置(某一个 N U L L NULL NULL 的位置)。
删除某结点,并保持二叉排序树特性,分三种情况处理:
1)如果删除的是叶结点,则直接删除;
2)如果删除的结点只有一株左子树或右子树,则直接继承:将该子树移到被删结点位置;
3)如果删除的结点有两株子树,则用继承结点(后继)代替被删结点,这相当于删除继承结点——按 1) 或 2) 处理继承结点(因为继承节点肯定没有左子树,不然它就不会是继承节点)。
二叉查找树的删除操作的实现步骤(按《算法导论》中每个节点存储了父节点指针来实现的):
查找结点 z z z 的右子树上的最左下结点 y y y( y y y 肯定没有左孩子);
将结点 y y y 数据域替换到被删结点 z z z 的数据域;
如果 y y y 是 z z z 的右孩子,那么用 y y y 替换 z z z 成为 z z z 的双亲的一个孩子,然后用 z z z 的左孩子替换 y y y 的左孩子;
如果 y y y 不是 z z z 的左孩子,那么用 y y y 的右孩子替换 y y y 并成为 y y y 的双亲的一个孩子,然后再将 z z z 的右孩子转变为 y y y 的右孩子,然后执行和上面一样的操作,用 y y y 替换 z z z 成为 z z z 的双亲的一个孩子,然后用 z z z 的左孩子替换 y y y 的左孩子;
为了在二叉搜索树内移动子树,定义一个子过程 T R A N S P L A N T TRANSPLANT TRANSPLANT,它是用一棵子树替换一棵子树并成为其双亲的孩子节点。当 T R A N S P L A N T TRANSPLANT TRANSPLANT 用一棵以 v v v 为根的子树来替换一棵以 u u u 为根的子树时,结点 u u u 的双亲就变为了节点 v v v 的双亲,并且最后 v v v 成为 u u u 的双亲的相应孩子。
TRANSPLANT(T,u,v)
if u.p == NULL
T.root = v
elseif u == u.p.left
u.p.left = v
elseif u == u.p.right
u.p.right = v
if v != NULL
v.p = u.p
利用现成的 T R A N S P L A N T TRANSPLANT TRANSPLANT 过程,下面是从二叉搜索树 T T T 中删除节点 z z z 的过程:
TREE_DELETE(T,z)
if z.left == NULL
TRANSPLANT(T,z,z.left)
elseif z.right == NULL
TRANSPLANT(T,z,z.right)
else
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
除了调用 T R E E − M I N I M U M TREE-MINIMUM TREE−MINIMUM 之外, T R E E − D E L E T E TREE-DELETE TREE−DELETE 的每一行,包括调用 T R A N S P L A N T TRANSPLANT TRANSPLANT 都只花费常数时间,因此在一棵高度为 h h h 的二叉搜索树上, T R E E − D E L E T E TREE-DELETE TREE−DELETE 的运行时间为 O ( h ) O(h) O(h)。
结论:在一棵高度为 h h h 的二叉搜索树上,动态集合上的查找、最小(大)元素、前驱和后继、插入和删除操作,均可以在 O ( h ) O(h) O(h) 时间内完成。
我们已经证明了高度为 h h h 的二叉搜索树上的每个基本操作都可以在 O ( h ) O(h) O(h) 时间内完成,然而随着元素的插入和删除,二叉搜索树的高度是变化的。例如,如果 n n n 个关键字按照严格递增的次序被插入,则这棵树一定是高度为 n n n 的一条链,在这样的树上进行操作的时间性能是比较低的。也就是说,如果搜索树的高度较低时,这些集合操作会执行得较快;然而如果树的高度较高时,这些集合操作可能并不比在链表上执行的快。也就有了各种各样的 “平衡”搜索树。