二叉搜索树

来源:互联网 发布:网络推广工作安排 编辑:程序博客网 时间:2024/06/14 05:35

前言

搜索树数据结构支持许多动态集合操作,包括SEARCH, MINIMUM, MAXIMUM, PREDECESSOR, SUCCESSOR, INSERT和DELETE等。因此,我们使用一棵搜索树既可以作为一个字典又可以作为一个优先队列。

二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于有n个结点的一棵完全二叉树来说,这些操作的最坏运行时间为Θ(lgn)。然而,如果这棵树是一条n个结点组成的线性链,那么同样的操作就要花费Θ(n)的最坏运行时间。在12.4节中,我们冼看到一棵随机构造的二叉搜索树的期望高度为O(lgn),因此这样一棵树上的动态集合的基本操作的平均运行的时间是Θ(lgn)。

实际上,我们并不能总是保证随机地构造二叉搜索树,然而可以设计二叉搜索树的变体,来保证基本操作具有好的最坏情况性能。第13章给出了一个这样的变形,即红黑树,它的树高为O(lgn)。第18章将介绍B树,它特别适用于二级(磁盘)存储器上的数据库维护。

在给出二叉搜索树的基本性质之后,随后几节介绍如何遍历一棵二叉搜索树来按序输出各个值,如何在一棵二叉搜索树上查找一个值,如何查找最小或最大元素,如何查找一个元素的前驱和后继,以级如何对一棵二叉搜索树进行插入和删除。树的这些基本数学性质见附录B。

什么是二叉搜索树

顾名思义,一棵二叉搜索树是以一棵二叉树来组织的,如图12-1所示。这样一棵树可以使用一个链表数据结构来表示,其中每个结点就是一个对象。除了key和卫星数据之外,每个结点还饮食属性left, right 和 p, 它们分别指向结点的左孩子,右孩子和双新。如果某个孩子结点和父结点不存在,则相应属性的值为NIL。根结点是树中唯一父指针为NIL的结点
二叉树

二叉搜索树中的关键字总是以满足二叉搜索树性质的方式来存储:
设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key<=x.key。如果y是x右子树中的一个结点,那么y.key >= x.key

因此,在图12-1(a)中,树根的关键字为6,在其左子树中有关键字2, 5 和 5,它们均不大于6;而在其右子树中有关键字7和8,它们均不小于6。这个性质对树中的每个结点都成立。例如,树根的左孩子为关键字5,不小于其左子树中的关键字2并且不大于其右子树中的关键字5。

二叉搜索树性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序遍历(inorder tree walk)算法。这样命名的原因是输出的子树根的关键字位于其左子树的关键字和右子树的关键字值之间。(类似地,先序遍历(preorder tree walk)中输出的根的关键字在其左右子树的关键字值之前,而后序遍历(postorder tree walk)输出的根的关键字在其左右子树的关键字值之后。)调用下面的过程INORDER-TREE-WALK(T.root),就可以输出一棵二叉搜索树T中的所有无素。

INORDER-TREE-WALK(x)if x≠ NIL    INORDER-TREE-WALK(x.left)    print x.key    INORDER-TREE-WALK(x.right)

作为一个例子,对于图12-1中的两棵二叉搜索树,中序遍历输出的关键字次序均为2, 5, 5, 6, 7, 8。根据二叉搜索树性质,可以直接应用归纳法证明该算法的正确性。
遍历一棵有n个结点的二叉搜索树需要耗费Θ(n)的时间,因为初次调用之后,对于树中的每个结点这个过程恰好要自己调用两次:一次是它的左孩子,另一次是它的右孩子。下面的定理给出了执行一次中序遍历耗费线性时间的一个证明。下面的定理给出了执行一次中序遍历耗费线性时间的一个证明。

定理12.1 如果x是一棵有n个结点子树的根,那么调用INORDER-TREE-WALK(x)需要Θ(n)时间。
证明 当INORDER-TREE-WALK作用于一棵有n个结点子树的根时,用T(n)表示需要的时间,由于INORDER-TREE-WALK要访问这棵子树的全部n个结点,所以有T(n) = Ω(n)。下面要证明T(n) = O(n)。

由于对于一棵空树,INORDER-TREE-WALK需要耗费一个小的常数时间(因为测试x≠NIL),因此对某个常数c > 0, 有 T(0) = c。
对于n > 0,假设调用INORDER-TREE-WALK作用在一个结点x上,x结点左子树有k个结点且其右子树有n-k-1个结点,则执行INORDER-TREE-WALK(x)的时间由T(n) <= T(k) + T(n - k - 1) + d限界,其中常数d > 0。此式反映了执行INORDER-TREE-WALK(x)的一个时间上界,其中不包括递归调用所花费的时间。
使用替换法,通过证明T(n) <= (c + d)n + c,可以证得T(n) = O(n)。对于n = 0, 有( c + d) * 0 + c = c = T(0)。对于n > 0,有

T(n) <= T(k) + T(n - k - 1) + d = ((c + d)k + c) + ((c + d)(n - k - 1) + c) + d = (c + d)n + c - (c + d) + c + d = (c + d)n + c

于是,便完成了定理的证明。

练习

12.1-1对于关键字集合{1, 4, 5, 10, 16, 17, 21},分别画出高度为2, 3, 4, 5和6的二叉搜索树
self
这里写图片描述
12.1-2二叉搜索树性质与最小堆性质之间有什么不同?能使用最小堆性质在O(n)时间内按序输出一棵有n个结点树的关键字吗?可以的话,请说明如何做,否则解释理由

答:最小堆:要求为完全二叉树          A[PARENT(i)] <= A[i]   二叉树:一个节点的左子树的关键字要<= 该节点的关键字,其右子树的关键字要>=该节点的关键字不能以O(n)的时间按顺序输出最小堆的关键字

12.1-3 设计一个执行中序遍历的非递归算法(提示:一种容易的方法是使用栈作为辅助数据结构;另一种较复杂但比较简洁的做法是不使用栈,但要假设能测试两个指针是否相等。)

public void inorderTreeWalk(Node<E> x){        Node<E> temp = x;        Stack<Node<E>> stack = new Stack<>();        while (temp != null || !stack.isEmpty()) {            while (temp != null){                stack.push(temp);                temp = temp.getLeft();            }            Node<E> node = stack.pop();            System.out.println(node.getItem());            temp = node.getRight();        }    }

12.1-4对于一棵有n个结点的树,请设计在Θ(n)时间内完成的先序遍历算法和后序遍历算法。

public void preorderTreeWalk(Node<E> x){        if (x != null) {            System.out.println(x.getItem());            preccderTreeWalk(x.left);            preccderTreeWalk(x.right);        }    }    public void postorderTreeWalk(Node<E> x) {        if (x != null) {            postorTreeWalk(x.left);            postorTreeWalk(x.right);            System.out.println(x.getItem());        }    }

查询二叉搜索树

我们经常需要查找一个存储在二叉搜索树中的关键字。除了SEARCH操作之外,二叉搜索树还支持如MINIMUM,MAXMUM,SUCCESSOR和PREDECESSOR的查询操作。本节将讨论这些操作,并且说明在任何高度为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)

这个过程从树根开始查找,并沿着这棵树中的一条简单路径向下进行,如图12-2所示。对于遇到的每个结点x,比较关键字k与x.key。如果两个关键字相等,查找就终止。如果k小于x.key,查找在x在左子树中的继续,因为二叉搜索树性质蕴涵了k不可能被存储在右子树中,对称地,如果k大于x.key,查找在右子树中继续。从树根开始递归期间遇到的结点就形成了一条向下的简单路径,所以TREE-SEARCH的运行时间为O(h),其中h是这棵树的高度。

我们可以采用while循环来展开递归,用一种迭代方式重写这个过程。对于大多数计算机,迭代版本的效率要高得多。

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

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

通过从树根开始沿着left孩子指针直到遇到一个NIL,我们总能在一棵二叉搜索树中找到一个元素,如图12-2所示。下面的过程返回了一个指向在以给定结点x为根的子树中的最小元素的指针,这里假设不为NIL
这里写图片描述

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

二叉搜索树性质保证了TREE-MINIMUM是正确的。如果结点x没有左子树,那么由于x右子树中的每个关键字都至少大于或等于x.key,则以x为根的子树中的最小关键字是x.key。如果结点x有左子树,那么由于其右子树中没有关键字小于x.key,且在左子树中的每个关键字不大于x.key,则以x为根的子树中的最小关键字一定在经x.left为根的子树中。

TREE-MAXIMUM为伪代码是对称的,如下:

TREE-MAXIMUM(x)    while x.right ≠ NIL        x = x.right    return x

这两个过程在一棵高度为h的树上均能在O(h)时间内执行完,因为与TREE-SEARCH一样,它们所遇到的结点均形成了一条从树根向下的简单路径。

后继和前驱

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

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右子树中的最左结点。通过第3行中的TREE-MINIMUM(x.right)调用可以找到。例如,在图12-2中,关键为15的结点的后继是关键字为17的结点。
另一方面,正如练习12.2-6所要做的,如果结点x的右子树非空并有一个后继y,那么y就是x的有左孩子的最底层祖先,并且它也是x的一个祖先。在图12-2中,关键字为13的结点的后继是关键字为15的结点。为了找到y,只需简单地从x开始没树而上直到遇到一个其双新有左孩子的结点。TREE-SUCCESSOR中的第4-8行正是处理这种情况。

在一棵高度为h的树上,TREE-SUCCESSOR的运行时间为O(h),因为该过程或者遵从一条简单路径没树向上或者遵从简单路径没树向下。过种TREE-PREDECESSOR与TREE-SUCCESSOR是对称的,其运行时间也为O(h)。

即使关键字非全不相同,我们仍然定义任何结点x的后继和前驱为分别调用TREE-SUCCESSOR(x)和TREE-PREDECESSOR(x)所返回的结点。

总之,我们已经证明了下面的定理。

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

练习

12.2-1假设一棵二叉搜索树中的结点在1到1000之间,现在想要查找数值为363的结点。下面序列中哪个不是查找过的序列

答 c

12.2-2写出TREE-MINIMUM和TREE-MAXIMUM的递归版本。

TREE-MAXIMUM(x)    if(x.right ≠ NIL)        TREE-MAXIMUM(x.right)    else        return xTREE-MINIMUM(x)    if(x.left ≠ NIL)        TREE-MAXIMUM(x.left)    else        return x

12.2-3写出过程TREE-PREDECESSOR的伪代码。

TREE-PREDECESSOR(x)    if x.left ≠ NIL        return TREE-MAXIMUM(x.right)    y = x.p    while y≠ NIL, and x == y.left        x = y        y = y.p    return y

插入和删除

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

插入

要将一个新值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    elseif z.key < y.key        y.left = z    else y.right = z

图12-3显示了TREE-INSERT是如何工作的。正如过程TREE-SEARCH和ITERATIVE-TREE-SEARCH一样,TREE-INSERT从树根开始,指针x记录了一条向下的简单路径,并查找要替换的输入项z的NIL。该过程保持遍历指针(trailing pointer)y作为x的双新。初始化后,第4-8行的while循环使得这两个指针没树向下移支,向或或右移动取决于z.key和x.key的比较,直到x变为NIL。这个NIL占据的位置就是输入项z要放置的地方。我们需要遍历指针y,这是因为找到NIL时要知道z属于哪个结点。第9-14行设置相应的指针,使得z插入其中。
与其他搜索树上的原始操作一样,过各TREE-INSERT在一棵高度为h的树上的运行时间为O(h)。
img

删除

从一棵二叉搜索树T中删除一个结点z的整个策略分为三种基本情况(如下所述),但只有一种情况有点棘手。

  • 如果z没有孩子结点,那么只是简单地将它删除,并修改它的父结点,用NIL作为孩子来替换z
  • 如果z只有一个孩子,那么将这个孩子提升到树中z的位置上,并修改z的父结点,用的孩子来替换z。
  • 如果z有两个孩子,那么找z的后继y(一定在z的右子树中),并让y占据树中z的位置。z的原来右子树部分成为y的新的右子树,并且z的左子树成为y的新的左子树。这种情况稍显麻烦(如下所述),因为还与y是否为z的右孩子相关。

从一棵二叉搜索树T中删除一个给定的结点z,这个过程取指向T和z的指针作为输入参数。考虑在图12-4中显示的4种情况,它与前面概括出的三种情况有些不同。

  • 如果z没有左孩子(图12-4(a)),那么用其右孩子来替换z,这个右孩子可以是NIL,也可以不是。当z的右孩子是NIL时,此时这种情况归为z没有孩子结点的情形。当z的右孩子非NIL时,这种情况就是z仅有一个孩子结点的情形,该孩子是其右孩子。
  • 如果z仅有一个孩子且为其左孩子(图12-4(b)),那么用其左孩子来替换z。
  • 否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子(见练习12.2-5)。现在需要将y移出原来的位置进行拼接,并替换树中的z。
  • 如果y是z的右孩子(图12-4(c)),那么用y替换z,并仅留下y的右孩子。
  • 否则,y位于z的右子树中但并不是z的右孩子(图12-4(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    else u.p.right = v    if v ≠ NIL        v.p = u.p

第2~3行处理u是T的树根的情况。否则,u是其双亲的左孩子或右孩子。如果u是一个左孩子,第4-5行负责u.p.left的更新;如果u是一个右孩子,第6行更新u.p.right。我们允许v为NIL,如果v为非NIL时,第7-8行更新v.p。注意到v.left和v.right的更新;这些更新都由TRANSPLANT的调用者来负责。
利用现成的TRANSPLANT过程,下面是从二叉搜索树T中删除结点z的删除过程:

TREE-DELETE(T, z)if z.left == NIL    TRANSPLANT(T, z, z.right);elseif z.right == NIL    TRANSPLANT(T, z, z.left);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

TREE-DELETE过程处理4种情况如下。第2-3行处理结点z没有左孩子的情况,第4~5行处理z有一个左孩子但没有右孩子的情况。第6-13行处理剩下的两种情况,也就是z有两个孩子的情形。第6行查找结点y,它是z的后继。因为z的右子树非空,这样后继一定是这个子树中具有最小关键字的结点,因此就调用TREE-MINIMUM(z.right)。如前所述,y没有左孩子。将y移出它的原来位置进行拼接,并替换树中的z。如果y是z的右孩子,那么11-13行用y替换z并成为z的双亲的一个孩子,用z的左孩子替换y的左孩子。如果y不是z的左孩子,第8-10行用y的右孩子替换y并成为y的双亲的一个孩子,然后将z的右孩子转变为y的右孩子,最后第11-13行用y替换z并成为z的双亲的一个孩子,再用z的左孩子替换为y的左孩子。
除了第5行调用TREE-MINIMUM之外,TREE-DELETE的每一行,包括调用TRANSPLANT,都只花费常数时间。因此,在一棵高度为h的树上,TREE-DELETE的运行时间为O(h)。
总之,我们证明了下面的定理。
定理12.3 在一棵高度为h的二叉搜索上,实现动态集合操作INSERT和DELETE的运行时间均为O(h)。

参考

算法导论二叉树

0 0
原创粉丝点击