《算法导论》学习之旅-第十二章-二叉搜索树


序言

搜索树数据结构支持许多动态集合操作,包括:SEARCH, MINIMUM, MAXIMUM, INSERT, DELETE, SUCCESSOR等。因此一个搜索树既可以作为一个字典又可以作为一个优先队列。二叉搜索树的基本操作所花费的时间与这棵树的高度成正比。对于一个n个结点的二叉搜索树,他的期望时间为O(lgn)


什么是二叉搜索树

二叉搜索树顾名思义就是一颗可以用链表数据结构来表示的二叉树来组织的。
在这里插入图片描述

  • 二叉搜索树中的关键字总是满足二叉搜索树性质的方式来存储的:

设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)

定理12.1 如果x是一棵有n个结点子树的根, 那么调用INORDER-TREE-WALK(x)需要Θ(n)

查询二叉搜索树

我们经常需要查找存储在二叉搜索树内的关键字。这节将讨论这些操作,并且说明在任何高度为h的二叉搜索树上,如何在O(h)时间内执行完每个操作。

查找

查找二叉搜索树中一个给定关键字的结点。输入一个指向树根的指针一个关键字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)

我们可以看出,这个算法的运行时间为O(h),其中h就是这棵树的高度。
书中还说了一种迭代法,就是用while循环来展开递归,这种效率要高很多:

ITERACTIVE-TREE-SEARCH(x, k)
while x != NIL and k != x.key
	if k < x.key
		x = x.left
	else x = x.right
return x

最大关键字元素和最小关键字元素

通过从树根开始沿着left孩子指针知道遇到一个NIL,我们总能在一颗二叉搜索树中找到一个元素。因此下列伪代码指出可以查找到最小元素的指针,这里假设不是NIL:

TREE-MINIMUM(x)
while x.left != NIL
	x = x.left
return x

相似的,最大元素查找方法是一样的:

TREE-MIXIMUM(x)
while x.right != NIL
	x = x.right
return x

前驱和后继

给定一棵二叉搜索树中的一个结点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个结点x的后继是大于工x. key的最小关键字的结点。一棵二叉搜索树的结构允许我们通过没有任何关键字的比较来确定一个结点的后继。如果后继存在,下面的过程将返回一棵二叉搜索树中的结点x的后继;如果x是这棵树中的最大关键字,则返回NIL.

TREE-SUCCESSOR(X)
if x.right != NIL
	return TREE-MINIMUN(x.right)
y = x.p
while y != NIL and x == y.right
	x = y
	y = y.p
return y

定理12.2.
在一棵高度为h的二叉搜索树上,动态集合上的操作SEARCH、MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR可以在O(h)时间内完成。

插入和删除

插入和删除操作会引起二叉搜索树表示的动态集合的变化。一定要修改数据结构来反映这个变化,但修改要保证二叉搜索树性质的成立。正如下面将要看到的,插入一个新的结点带来的树要修改的相对简单一点,而删除的处理有些复杂。

插入

要将一个新值v插入到一颗二叉搜索树T中,需要调用过程TREE-INSERT。该过程以结点z作为输入,其中z.key = v, z.left = NIL, z.right = NIL。这个过程要修改T和z的某些属性,来吧z插入到树中的相应位置上。

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
else if z.key < y.key
	y.left = z
else y.right = z

下图诠释了插入操作的过程:
在这里插入图片描述
即向一个二叉搜索树中插入一个x结点,只需要不断比较x->key与当前结点z->key的大小,若小于,则肯定向z的左子树插入,否则向z的右子树插入,循环比较,直到遇到当前结点的左/右子树为空为止。

删除

删除z结点操作要比较麻烦一些,因为要保持二叉搜索树的性质:

  • 如果z没有孩子结点,那么只是简单的将他删除,并修改他的父结点,用NIL作为孩子来替换z。
  • 如果只有一孩子,那么将这个孩子提升到树中z的位置上,并修改z的父结点,用z的孩子来替换z
  • 如果有两个孩子
    找出要删除结点z的后继结点y,这个结点y一定位于结点z的右子树中且是z的右子树中最小的一个元素,所以结点y一定没有左孩子。
    分两种情况:
    1.结点y是结点z的右孩子:这时用y替换z的位置,结点y之前的右子树还是它的右子树,结点z之前的左子树作为y的左子树。
    在这里插入图片描述
    2.结点y不是结点x的右孩子:先用y的右子树x替换y位置,接着用y替换z的位置。
    在这里插入图片描述

总结

因为数学水平有限,对随机构建二叉搜索树没有做记录。还有删除操作还不是特别清楚,不过先记录下来,写代码实践的时候再慢慢的消化吧。然后就是二叉搜索树的时间,平均下来可以类似于折半查找,Θ(lgn)的时间,但是当搜索树完全不平衡时就可能达到O(n)的时间,因此二叉搜索树的时间复杂度为O(lgn)~O(n)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章