让你彻底明白二叉搜索树(都给你整理好了)

不要光在脑海中构建宏伟蓝图,俯下身去做才是硬道理;

您的点赞便是对我的最大支持

如果觉得本文还算不错的话,不妨加个关注

后期会更新红黑树,B树相关的知识

二叉搜索树

前言介绍 :大家都知道红黑树和B树B+树都是面试的重点可是如果没有对二叉树有过基本的了解后面的红黑树之类的学起来也会比较吃力,所以这里我为大家整理了二叉树的基本内容,这里有一本内容是我参考了算法导论里的基本概念,但是我对这些内容做了一些整理,可以让大家学起来更方便一些。

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

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

    实际上,我们并不能总是保证随机地构造二又搜索树,然面可以设计二又搜索树的变体,来保证基本操作具有好的最坏情况性能。比如红黑树,它的树高为O(lgn),又比如B树,它特别适用于二级(磁盘)存储器上的数据库维护。

    在给出二叉搜索树的基本性质之后,随后介绍如何遍历一棵二又搜索树来按序输出各个值,如何在棵二叉搜索树上查找一个值, 如何查找最小或最大元素,如何查找一个元素的前驱和后继,以及如何对一棵二叉搜索树进行插人和删除。

在这里插入图片描述

1.1 什么是二叉搜索树

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

在这里插入图片描述

    二又搜索树。对任何结点x,其左子树中的关键字最大不超过x.key,其右子树中的关键字最小不低于x.key.不同的二叉搜索树可以代表同一组值的集合。大部分搜索树操作的最坏运行时间与树的高度成正比。(a)一 棵包含6个结点、高度为2的二叉搜索树。(b)一棵包含相同关键字、高度为4的低效二叉搜索树.

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

    设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key.如果y是x右子树中的一个结点,那么y.key≥x. key

    因此,在图中,树根的关键字为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中的所有元素。其中先序遍历输出a图 6 5 2 5 7 8,中序遍历 2 5 5 6 7 8 ,后序遍历2 5 5 8 7 6.

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

    这个伪代码表达了对一个二叉树进行中序遍历,核心也就是对一棵树不断的进行递归直到递归到这棵树的叶节点(没有孩子的节点)。

如果工是一棵有n 个结点子树的根,那么调用INORDER TREE WALK(x)需要θ(n)时间。

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

1.2 查询二叉搜索树

    我们经常需要查找一个存储在二叉搜索树中的关键字。除了SEARCH操作之外,二叉搜索树还能支持诸如MINIMUM、MAXIMUM、SUCCESSOR 和PREDECESSOR的查询操作。本节将讨论这些操作,并且说明在任何高度为h的二叉搜索树上,如何在O(h)时间内执行完每个操作。

1.2.1 查找

    我们使用下面的过程在一棵二叉搜索树中查找一个具有给定关键字的结点。输人一个指向树根的指针和一个关键字k,如果这个结点存在,TREE-SEARCH返回一个指向关键字为k的节点的指针:否则返回NIL。

//其中x树种中的每个节点,k是查找的参数关键字
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)
//从根节点开始左右子树递归的寻找

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

在这里插入图片描述
    一棵二叉搜索树 上的查询。为了查找这棵树中关键字为13的结点,从树根开始沿着15→6→7→13路径进行查找。这棵树中最小的关键字为2,它是从树根开始一直沿着left指针被找到的。最大的关键字20是从树根开始一直沿着right指针被找到的。关键字为15的结点的后继是关键字为17的结点,因为它是15的右子树中的最小关键字。关键字为13的结点没有右子树,因此它的后维是最低的祖先并且其左孩子也是一个祖先。这种情况下,关键字为15的节点就是它的后继节点

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

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

这个函数跳出循环的情况有两种,一是找到目标关键字,二是访问到了空节点(也就是没有找到)

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

    通过从树根开始沿着left孩子指针直到遇到一个NIL,我们总能在一棵二叉搜索树中找到一个元素,如图所示。下面的过程返回了一个指向在以给定结点r为根的子树中的最小元素的指针,这里假设不为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(x)
  while x.right≠NIL
    x=x.right
  return x

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

1.2.3 后继和前驱

    给定一颗二叉搜索树中的一个结点,有时候需要按中序遍历的次序查找它的后继。如果所有的关键字互不相同,则一个结点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的后继恰是右子树中的最左结点。通过第2行中的TREE-MINIMUM(x. right)调用可以找到。例如,在图中,关键字为15的结点的后继是关键字为17的结点。

    另一方面,如果结点x的右子树为空并有一个后继y,那么y就是工的最底层祖先,并且y的左孩子也是x的一个祖先。在图中,关键字为13的结点的后继是关键字为15的结点。为了找到y,只需简单地从x开始沿树而上直到遇到这样一个结点:这个结点是它的双亲的左孩子。TREE SUCCESSOR中的第3~7行正是处理这种情况。

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

在这里插入图片描述

比方说我们要找15的后继节点 很明显15的后继是17但是我们怎么用程序将它找出来那 ?

    仔细观察我们发现,我们需要找后继实际上就是找右子树的最小节点,但是这又分了两种情况我们先讨论第一种情况==>有右子树的情况,不就是我们之前刚提到的最小关键元素嘛

 if x.right≠NIL
    return TREE MINIMUM(x. right)

现在我们开始讨论第二种情况,没有右子树怎么办?我们假设要找下图中13的后继

在这里插入图片描述

    这个可能会让大家思考片刻,但是也不是太难,让我们开动充满智慧的脑袋,仔细的观察一下吧,我们发现4的后继节点是6,13的后继节点是15,但是怎么用代码的形式表示出来,我来为大家讲讲吧,首先我们发现 4 --> 3 --6 最终从右子树部分到了左子树部分,而他们的双亲节点便是我们要找的哪一个后继,再来观察13 我们发下6这个节点也是刚好到15的左子树部分,而它的双亲是15 恰好是13的后继。由此我们便可以写出代码,只要找到左子树的双亲便可。

 y=x.p
  while y≠NIL and x== y.right
    x=y
    y=y.p
return y

在这里插入图片描述

1.2.4插入和删除

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

     插入:要将一个新值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

将关键字13插入到二叉树中

在这里插入图片描述

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

总结二叉树的插入,其实这个操作还是比较简单的只是涉及到将我们的目标元素按照左右子树进行遍历操作然后插入到叶节点中,并没有对二叉树的结构产生较大的改变。

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

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

     从一棵二叉搜索树T中删除一个给定的结点z,这个过程取指向T和z的指针作为输入参数。考虑在图中显示的4种情况,它与前面概括出的三种情况有些不同。
  ● 如果z没有左孩子(图(a)), 那么用其右孩子来替换z,这个右孩子可以是NIL,也可以不是。当z的右孩子是NIL时,此时这种情况归为z没有孩子结点的情形。当z的右孩子非NIL时,这种情况就是z仅有一个孩子结点的情形,该孩子是其右孩子。
  ● 如果z仅有一个孩子且为其左孩子(图(b)), 那么用其左孩子来替换z。
  ● 否则,z既有一个左孩子又有一个右孩子。我们要查找z的后继y,这个后继位于z的右子树中并且没有左孩子。现在需要将y移出原来的位置进行拼接,并替换树中的z。
  ● 如果y是z的右孩子(图©),那么用y替换z,并仅留下y的右孩子。.否则,y位于z的右子树中但并不是z的右孩子(图(d))。在这种情况下,先用y的右孩子替换y,然后再用y替换z。

在这里插入图片描述
在这里插入图片描述

    为了在二叉搜索树内移动子树,定义一个子过程TRANSPLANT,它是用另一棵子树替换一棵子树并成为其双亲的孩子结点。当TRANSPLANT用一棵以v为根的子树来替换一棵以u为根的子树时,结点u的双亲就变为结点v的双亲,并且最后v成为u的双亲的相应孩子。

TRANSPLANT(T,u,v)
 if u.p == NIL
     T.root=v
 else if u == u. p.left
     u. p.left = v
 else u.p.righi=v
 if v ≠ NIL
    v.p= u.p
    

    第1,2行处理u是T的树根的情况。也就是u没有双亲的时候,我们让v作为它的树根,否则,u是其双亲的左孩子或右孩子。如果u是一个左孩子,第3 ,4行负责u. p.left的更新;如果u是一个右孩子,第5行更新u. p.right,其实也就是让v(子树)来替换双亲的左右子树的过程,.我们允许v为NIL,如果v为非NIL时,第6~7行更新v.p。注意到,TRANSPLANT 并没有处理v. left和v.right的更新;这些更新都由TRANSPLANT的调用者来负责。

利用现成的TRANSPLANT过程,下面是从二叉搜索树T中删除结点z的删除过程:

TREE-DELETE(T,z)
   if z.left == NIL
TRANSPLANT(T ,Z,之。right)
  else if  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.lefl = z.left
    y.left.p= y

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

上述的代码过程大家可以按照abcd四个图像分别的分析一下,然后代码就好理解了。

在一棵高度为h的二又搜索树上,实现动态集合操作INSERT和DELETE的运行时间均为O(h)。


我是js同学你好,愿千帆过尽,归来你依旧是少年。文章持续更新,,,
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值