python进阶之数据结构与算法--中级-二叉搜索树

一、概念:

    在之前的章节中已经为各位介绍过树的概念,实际上树就是一种很常见的ADT,存储方式也是非线性且更加贴近自然的,而树最重要的功能就是用作搜索树。在本节中我们利用搜索树来有效实现有序映射。映射M有三种最基本的方法,分别是setitem(索引赋值),getitem(索引),delitem(按索引删除)。
    假定我们已经知道了键的规律,即有一定顺序时,这时使用树来存储这种键值对也就变得很自然了。本章介绍二叉搜索树,其每个节点上存储的都是(k,v)键值对,使得:p的左子树的键都小于k,p的右子树的所有键均大于k

二、遍历二叉搜索树

2.1 什么遍历方式?

    二叉搜索树中关于键的位置的结构特性使得树的遍历是中序遍历,还记得中序遍历不?

2.2 遍历的原理:

    一般情况下,二叉搜索树的中序遍历是按照键增加的顺序进行的。中序遍历的逻辑这里简述一下,树或子树中先递归遍历左子树,再访问根节点,然后递归遍历右子树,又由于左子树小于右子树,所以左子树递归中序遍历会在该子树上呈现键值递增的迭代,由于右子树大于左子树,且p的值小于右子树,所以右子树依旧是递增遍历的。整体可以呈现一种由左向右(有小到大的)的递归遍历方式

2.3 提供的方法:

     在这里大家都知道递归中序遍历推荐自顶向下的遍历方式,但是我们也可以添加一些根据粒度化的遍历手段,为键的顺序提供更加自然的定位方式。例如:
        first():最小节点
        last():最大节点
        before():返回当前节点p遍历方式上前驱节点的值
        after()返回当前节点p遍历方式上后继节点的值

     二叉搜索树的第一个位置可以从根开始,也可以从任意子树开始遍历整棵树的一部分,并且只要左子树存在就先遍历左子树,同样的通过从根开始享有进行重复的步骤可以到达整棵树中最大的位置。

2.4 伪代码:

2.4.1 节点的后继节点after§的伪代码:

# 注意:这里的注释很重要!!!!好好理解!!!!
Alogorithm after(p):
    if right(p) is not None :# 如果该节点有右子树
        pos = right(p)# 记录右子树位置
        while left(pos) is not None:# 如果该右子树的左子树非空,则一直循环
            pos = left(pos)# 不断将位置更新到当前p的右子树的最左子树才停止
        return pos# 这是最左子树,也是p下一个将会遍历的位置,类似图一的过程!!!
    else:# 当该位置p没有右子树时
        pos = p# 将位置更新为当前位置p
        ancestor = parent(pos)# 记录p的父节点位置
        while ancestor is not None and pos == right(ancestor):# 如果存在父节点且当前节点是父节点的右节点(这样能证明当前节点遍历完成了),则循环不止
            pos = ancestor# pos更新为父节点位置
            ancestor = parent(pos)#ancestor为祖先节点
            # 循环内其实就是一直在寻找祖先节点且每个祖先节点都是其父点的右节点。
        return ancestor# 返回的要不就是没有父节点的祖先节点,或者就是不是其父节点右节点的祖先节点,即p的下一个遍历位置

     图一:if中的内容图解
在这里插入图片描述
     图二:else中的代码图解,途中的p1就是代码中的p
在这里插入图片描述
     这个得到p的后继节点的过程,别看只是一个方法,但是其尤其重要,因为这个思维过程就是中序遍历的核心,为了方便各位演练这个过程,我把上次的图再放在这里,供大家参考:
在这里插入图片描述
     找到当前位置先一个遍历的位置的过程很好的体现了中序遍历的特点,最后一张图就是递归中序遍历的整体效果图,大家可以结合代码仔细想想看。

2.4.2 节点的前驱节点before§的伪代码:

     后继节点都会找了,那么前驱节点自然不难理解了。

# 注意:这里的注释很重要!!!!好好理解!!!!
Alogorithm before(p):
    if left(p) is not None :# 如果该节点有左子树
        pos = left(p)# 记录左子树位置
        while right(pos) is not None:# 如果该左子树的右子树非空,则一直循环
            pos = right(pos)# 不断将位置更新到当前p的左子树的最右子树才停止
        return pos# 这是p的左子树的最右子树,也是p前驱遍历的位置,类似图一的过程!!!
    else:# 当该位置p没有左子树时
        pos = p# 将位置更新为当前位置p
        ancestor = parent(pos)# 记录p的父节点位置
        while ancestor is not None and pos == left(ancestor):# 如果存在父节点且当前节点是父节点的左子树,则循环不止
            pos = ancestor
            ancestor = parent(pos)# 得到祖先的父节点
        return ancestor

     要是想看图就参考上图吧,if中的内容参考73找到71的过程(71是73的前驱节点即:p71 = before(73));else中的内容参考66找到60的过程(即p60 = before(66))

三、搜索

3.1实现原理:

     二叉搜索树的结构特性产生最重要的结果是搜索算法。我们可以将二叉搜索树的形式表达成决策树的形式去定位一个特定的键。在每个节点需要思考的问题就是它到底是大于,小于还是等于该节点的值。如果大于,就搜索其右子树,如果小于就搜索其左子树,如果等于那就是找到了,如果最终得到的是空子树,那证明没有搜索到结果。如果要搜索的键key出现在p为根的子树中,则调用checkForValue(T, p, k),返回找到的节点的关联值。否则返回搜索路径的最终位置。

3.2 伪代码实现:

# 搜索某个期望节点k是否在树中:
Algorithm checkForValue(key, p):
    if key == p.key():
        return p
    elif key < p.key() and p.left() is not None:
        return checkForValue(key, p.left())
    elif key > p.key() and p.right() is not None:
        return checkForValue(key, p.right())
    return p

四、插入和删除

4.1 实现原理:

4.1.1 插入:

     二叉搜索树的插入元素的方法其实很简单,原理是由之前提及的三、搜索的方法,如果找到了指定的节点,就把该节点修改,如果未找到,最终返回树顶,然后新元素可以接在树顶的那个节点之后,如果值大于该节点,就将其添加为这个节点的右节点。否则添加为左节点。

4.1.2 删除:

     删除这个东西可没有插入那么简单,学习这么多种数据类型也应该看出来了,所有这种容器类的结构都有一个特点,删除都不太好搞,二叉搜索树本来就不简单,删除自然我会轻而易举。我这里基于网络资源给大家整理了一句话:
     删除最重要,最难的地方在于,保持其原有的数据特点,例如顺序就很重要,二叉搜索树由中序遍历而来,其搜索时的节点位置呈现一定规律(例如左节点小于p,p小于右节点),那么删除节点之后一定要保持原有的数据结构不能被破坏!!!
     第一,如果节点是线性的,加入p至多有一个子树,那这种删除很简单,用子树替代当前节点即可,如果没有子树,就把p直接干掉置空就行。
     但是,第二是重点,如果p有两个孩子节点,就不能轻易的删除一个节点p,因为这样必然会导致p的两个孩子成为孤儿子树(游离子树,或称垃圾树),这样数据丢失了,那真的很难找回来了。所以,接下来总结的话认真记下来!咱们前边写过一个before§的方法的伪代码实现,这个函数会得到p前驱节点,而前驱节点也是所有祖先前驱节点中最后一个,同时,前驱节点比p小,所以,前驱节点刚好是p的左子树的最右端那个节点pbefor,那么删除p,相当于用pbefore替换p本身即可,pbefore是p的左子树中最大的一个,但是又小于p右子树中的所有子树,即只要用pbefore替换p即大工告成且不会破坏二叉搜索树的顺序,最后只需要直接删除pbefore即可(左子树的最右端的节点,势必没有子树了)

4.2 伪代码实现:

4.2.1 插入:

Algorithm insertTree(key, v)
    p = checkForValue(key, T.root())
    if key == p.key():
        p.key() = v
    elif key<p.key() and p.left is None:
        p.left.add((key,v))
    else:
        p.right.add((key,v))

4.2.2 删除:

Algorithm deleteTree(key, p):
    p1 = checkForValue(key, p)#p是从某个节点开始查找,p可以是root
    if p1 is not None:
        if p1.child_num() == 0:
            p1 = None
        elif p1.child_num() == 1:
            p1.children() replace p1
        elif p1.child_num() == 2:
            pbefore = before(p1)
            pbefore replace p1
            del pbefore
    return p1

     下一节,我们开始撸代码,这里先把理论知识搞定哈。另外下一节,内容很精彩,各位届时一定留意哈。
佛系养生编程,请关注小白piao,带你轻松学python。
公众号:
在这里插入图片描述
也可以添加小白piao的个人微信进行沟通哦,本人从事python开发和IT计算机培训已有多年,经验相对丰富,也可以给各位初学的朋友给个了解的机会,定期还有免费公开课,带你们走进编程的大门,互相学习,互勉,广交天下朋友。
个人微信:
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白piao

创作不易,支持一下!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值