补充关于二叉搜数树,平衡二叉树

再次介绍关于二叉搜索树

  1. 二叉搜索树 == 二叉排序树 == 二叉查找树,从后面这两个名称来看,可见这个树对于排序查找是非常关键的,上一节我们可以看到它的中序遍历是由小到大排序的,因此这就是二叉排序树的由来。那么对于二叉查找树,它为什么这么叫呢?
  2. 接下来我们从它的增加(插入、构建)、删除查询更改(增删查改四个方面实现代码并且进行时间复杂度的分析。

二叉树查找树的增加(插入、构建)

  1. 内容简单吧,插入的话一定是往叶子节点插,因为后面的二叉树查找树的构建是在前面已经构造的基础上,如果这里需要往树的里面查,那么前面早就按叶子节点插入进去了,所以插入一定是往叶子节点插。
  2. 所以整体的思路就是:if x < node.val ,那么继续搜索node.left, if x > node.val ,那么继续搜索node.right ,if node == None  : 新建一个节点,那么关键的问题是新建的节点如何挂接到父节点的left or right,基于这个的递归思路有2种。
    # 第一种思路就是按照分别构造左子树以及右子树的方式处理,新建节点,后面return,递归函数挂上返回值,可以实现。但是这种方法在递归每一个子树时都返回它的子树重新构建,其实只要最后一个构建上就行,其余的没必要,基于这个缺点有下面的两种方法。
    def creatBinarySortedTree(node, x):
    
            if node == None:
                node = BinaryTree(x)           
    
            if x < node.val:
                node.left = creatBinarySortedTree(node.left, x)
    
            if x > node.val:
                node.right = creatBinarySortedTree(node.right, x)
    
            return node
    # 第一种  递归法
    def creatBinarySortedTree(node, x):
    
            if node.left == None and x < node.val:
                node.left = BinaryTree(x)
                return
    
            if node.right == None and x > node.val:
                node.right = BinaryTree(x)
                return
    
            if x < node.val:
                creatBinarySortedTree(node.left, x)
    
            if x > node.val:
                creatBinarySortedTree(node.right, x)
    # 那么第二种就是利用循环处理,本质上就是上面的一种变形,没啥区别
        def creatBinarySortedTree(node, x):
    
            if node == None:
                node = BinaryTree(x)
                return node
    
            while node:
                if x < node.val:
                    if not node.left:
                        node.left = BinaryTree(x)
                        break
                    node = node.left
                elif x > node.val:
                    if node.right:
                        node,right = BinaryTree(x)
                        break
                    node = node.right
                else:
                    node = node
    # 上述方法都可以,重代码的简单性上考虑就是使用第一种方法,当然后面二种考虑的比较细节,写的就比较多了,相对繁琐一点。

     

二叉查找树的删除节点

  1. 删除节点就可以在任意位置删除了,就不单单是叶子节点的删除了。要考虑被删除的节点没有任何子节点只有左子树、只有右子树、左子树和右子树,接下来分别考虑。
  2. 被删除节点没有任何子节点,此时指向被删除节点指针直接指向None;被删除节点只有左子树,将该节点删除之后,左子树直接替换到该节点,只有右节点也一样,将该节点删除之后,右子树直接替换到右节点,换句换说,指向被删除节点的指针直接指向它的左子树或者右子树;被删除节点既有左子树又有右子树,这个时候就要考虑左子树和右子树里面的哪个节点做新的根节点。考虑了一下根节点有两种选择,一种是选择右子树的最小值做根节点,此时右子树都比该值大,且左子树都比该值小。或者选择左子树的最大值,此时左子树都比该值小,右子树都比该值大。

基于上述的思路分析,删除的代码:

# 在删除之前需要先查询这个节点有没有,因此查询也有递归法和循环判断
# 递归法查询
    def findBinarySortedTree(node, x):

        if node == None:
            return False

        if x == node.val:
            return True

        if x < node.val:
            return findBinarySortedTree(node.left, x)

        if x > node.val:
            return findBinarySortedTree(node.right, x)

# 循环法查询

    def findBinarySortedTree(node, x):

        if node == None:
            return False
        while node:
            if node.val == x:
                return True
            elif node.val < x:
                node = node.right
            else:
                node = node.left
        return False
# 最后发现二者本质上是相同的,只是循环法直接输出结果,而递归法通过return 将结果反向递归出来。

通过上述查询之后,发现存在这个节点,只有存在才可以删除,如果上面return true,然后执行下面的删除代码。

# 二叉搜索树删除的递归版本  这是自己调试的,特别注意对于被删除节点是根节点也可以
    def deleteBinarySortedTree(node, x):

        if node == None:
            return None

        if x < node.val:
            node.left = deleteBinarySortedTree(node.left, x)

        if x > node.val:
            node.right = deleteBinarySortedTree(node.right, x)

        if node.val == x:
            if node.left == None and node.right == None:
                return None

            if node.left == None and node.right != None:
                return node.right

            if node.left != None and node.right == None:
                return node.left

            if node.left != None and node.right != None:
                nodePresent = node.right
                while nodePresent:
                    maxNodeVal = nodePresent
                    nodePresent = nodePresent.left
                deleteBinarySortedTree(c1, maxNodeVal.val)
                maxNodeVal.left = node.left
                maxNodeVal.right = node.right
                return maxNodeVal

        return node
  1. 说明:思路仍然是利用 二叉树的递归模板处理根节点,处理左子树,左子树,返回node注意还有逻辑版本上上面的查询就是逻辑版本。
  2. 上述由于每次处理是单测的,即每次处理要么是左子树,要么是右子树。因此那么return node 可以不用放在后面直接放在每一个单侧的后面,体现出了思路但是比较繁琐。
  3. 切记一点:以前处理二叉树没有涉及到二叉树结果的变化和更改,这里第一次涉及结构的变化和更改,如上述的插入,这里的删除,都是属于结构的变化和更改,比如这里的删除,删除之后如果必须将被删除的节点的左右子树挂到被删除节点的父亲节点上面,如果我们采用循环处理的话,一定是需要记住被删除节点的父亲节点的,这样才能实现但是一定要利用好一个特性,那就是递归函数会自己利用不同函数的调用实现当前父节点的记忆,本质上是由于递归函数属于栈的操作,每次能把被删除节点的父节点先于被删除节点压入栈中,当被删除节点处理完毕以后,被删除节点的父亲节点自动的就弹栈,利用return 被删除节点的左右子树合成的新树,自动的接到了被删除节点的父亲节点上,并且还不用管左右子树,这是非常重要且关键的一点

补充一个关于支持重复数据的二叉树的处理,常见的就是下面的两种处理,在实际的工业中,可以使用下面的两种方法,也可以通过唯一的id作为节点的key,避免数值的重复。

方法1:二叉树的节点不仅会存储一个数据,每一个几点我们通过链表和支持动态扩容的方法进行处理,将值相同的内容存储到一个节点上。

方法2:可以将这个相等的数认为比原来的数大,如果在插入的时候发现原数存在了,那么就将这个数值插到原数的右子树,继续按照二插查找树的规律插入。     在查询的过程中,如果已经查到了这个数,不停止,继续往这个数的右子树去查询,直接查到叶子节点结束。在删除的过程中,也是,按照上述的删除操作先删除一个,然后删除了之后继续搜索该节点的叶子节点,继续按照相同的方法删除。

对二叉查找树的四种操作时间复杂度进行分析

  1. 虽然说是四种操作,但是本质上四种操作都是  以查询为基础的模板,都是 左,右,左...这种操作,将是不断地二分。那么看下图:

通过分析,它执行每次都是左,右,左,右等等这种操作,那么执行的最大次数就是这颗树的高度,因此时间复杂度就是树高。

       对于上图第一个,这个二叉排序树已经退化为一个链表,数据量是n的话,树高也是n,所以此时的时间复杂度将是O(n);这是最糟糕的一种情况。

       对于上图第三个,这个二叉排序树是最理想的情况,数据量是n的话,此时树高是log n,因为每一层都是满的,每一层节点个数是2^x,因此时间复杂度将是log n。这是最好的情况。

      其实对于二叉排序树一般的树的结构是上图的中间那种,这是二叉排序树一般的结构。因此二叉排序树一般的时间复杂度是log n<T<n。存在最好和最坏的情况。

      那么,既然如此,为什么要使用这个二叉排序树呢?其实,我们可以发现,如果能将这个二叉排序树进行某种变换使得他的时间复杂度都能稳定在log n,避免上述大于log n的时间复杂度,那么我们每次的时间复杂度就是log n了,这岂不是很好,所以我们引入了平衡二叉树。它的目的就是为了稳定时间复杂度,它是二叉排序树的一种特殊形式,是在二叉排序树的基础上构建起来的,看下一节

     平衡二叉树

  1. 平衡二叉树定义:任意一个节点的左子树高度 - 右子树高度 只能等于 -1, 0, 1,也就是高度差不能不是2或者>2 ;   平衡二叉树的子树仍然是平衡二叉树。
  2. 提前引入完全二叉树:完全二叉树就是满二叉树从最右开始去掉几个节点得到的二叉树。
  3. 其实平衡二叉树和完全二叉树没有关系,只是由于定义上的问题,使得二者有点重叠。完全二叉树一定是平衡二叉树,平衡二叉树未必是完全二叉树。
  4. 平衡二叉树首先明白是二叉搜索树的构建过程的不断调节。它创建的过程就是二叉搜索树创建的过程,但是每次插入一个节点之后需要判断是否满足平衡二叉树的定义,需要按照相关的规则进行调整。调整的不仅仅是当前的节点,还要递归的调整由此节点引起的父节点,因此这是一个递归的过程。

平衡二叉树的详细代码

  1. 在设计代码之前我们先考虑能做出什么样的调整。
  2. 由于平衡二叉树要求左右子树的高度差不能大于2或者小于-2,所以我们只要检测到 == 2, 那么就开始调整,那么如何调整呢?当我们最开始插入节点的时候出现高度大于2的只可能是下面4种情况。都是最顶点的C的左右子树高度大等于2.
  3. 在递归的过程中,上图转化为一般的节点图如下:
  4. 说明:上述4个图是递归的一般过程,前两个图是相互对称的,后面的两个图是相互对称的,因此代码实现就相当于只有2个图了。
  5. 上述四个图分别叫做LL(左左),RR(右右),LR(左右),RL(右左),它的判别指标是最顶点A如果左子树的高度大于右子树,就是L,节点A的左子树的高度小于右子树就是R,同理子节点也是这么判定的,因此基于这个规则分为4种情况,而不是基于这几个节点A,B,C的转向看形状进行区分的
  6. 基于上述的形状,我们设计代码将上述节点进行旋转,变换,变换为平衡的二叉树。可以发现上述的顶点A的高度差是2,子树节点的高度差是1,因此以A为顶点的的树将是最小的不平衡子树。
  7. 变换的代码:node是最小不平衡二叉树的根节点。下面的代码按自己的理解就好了,至于名称无所谓。
    # 注意这个BinaryTree这个类里面维护的除了左右子树之外还有该节点的高度,高的的获取前面的二叉树是可以实现的,这里就不列举了。
        def rrTurn(node):
            # 主要记住上图,下面的代码不难记。
            newNode = node.left
            node.left = newNode.right
            newNode.right = node
            # 这里的高度由于 X, Y, Z的高度没有变化,因此变化后节点的高度直接max + 1即可,对于动 
              态获取节点的高度的位置,是后面的插入代码插入每一个节点后利用递归获取高度,和这里获取 
              高度的本质不同。
            node.height = max(height(node.left)t, height(node.right)) + 1
            newNode.height = max(height(newNode.left), height(newNode.right)) + 1
            
            return newNode
    
        # ll和rr对称的,只要将上面的right全部更换为left,将left全部更换为right即可。
        def llTurn(node):
            newNode = node.right
            node.right = newNode.left
            newNode.left = node
    
            node.height = max(height(node.left)t, height(node.right)) + 1
            newNode.height = max(height(newNode.left), height(newNode.right)) + 1
    
            return newNode
        
        # 下面的双旋转只要对照图就能发现其实是上面ll以及rr的组合实现。
        def lrTurn(node):
            # 变换了之后高度不必更新了,因为ll和rr里面已经更新过变换后节点的高度了。
            llTurn(node.left)
            rrTurn(node)
            
            
        def rlTurn(node):
            
            rrTurn(node.right)
            llTurn(node)
       # 上述获取高度必须使用这个高度函数,不然上述代码里面直接获取左子树右子树的高度,如果为空不 
       太好写
       def height(node):
           if node == None:
               return -1
           return node.height

     

平衡二叉树的创建、插入节点

思路:就是上述的描述:所以每次按照二插排序树的递归模板插入一个节点之后获取最小不平衡子树的节点然后判断属于上述的哪一种方法进行变换完毕之后更新当前节点的高度继续递归处理父亲节点,因为子树的变化有可能引起父亲的不平衡,父亲树的高度一定是变换的。

    # 更改了这个node的节点之后,其父节点高度需要改变,父节点也可能不平衡,也需要调整。那么如果
    # 不是利用这个递归方案的话,直接利用非递归的话,需要手动保存node的父亲节点,但是如果利用递 
    # 归,利用递归的特性,在return 当前的node之后,父亲节点会继续执行和当前node相同的操作,程 
    # 序不需要保存父亲节点,但是能自动更改父亲节点。
    def createBinaryBalancedTree(node, x):
        if node == None:
            node = BinaryTree(x)
        
        if x < node.val:
            node.left = createBinaryBalancedTree(node.left, x)
            if node.left.height - node.right.height == 2:
                # 可以发现本质上l、r的定义是左子树高度和右子树高度的大小关系,但是这里并没有
                # 用那个判断,而是直接利用 只有上述四种图形的一种等价形式, 就是直接利用插入  
                # 的节点x和当前的节点的left.val比较,如果比她小,一定是ll,比他大说明x插在了
                # node.left的右子树上边,就是lr的情况。
                if x < node.left.val:   # ll的情况
                    rrTurn(node)
                if x > node.left.val:   # lr的情况
                    lrTurn(node)
        
        if x > node.val:
            node.right = createBinaryBalancedTree(node.right, x)
            if node.right.height - node.left.height == 2:
                if x > node.right.val:
                    llTurn(node)
                if x > node.right.val:
                    rlTurn(node)
                    
        node.height = height(node)
        # 因为需要处理完毕返回最终的根节点,作为下次插入数据的根节点
        return node


    # 可以发现AVL就是上述二叉查找树的递归代码模板里面加了那几个判断条件,同时加了获取高度的函
    # 数。


    # 上面是插入一个数据,如果插入一个list呢(insertList)?其实这就是创建AVL和AVL的插入。
    start = True
    while insertList:
         presentVal = insertList.pop()
         if start == True:
            node = None
         # 用上一次返回的新的根结点作为下一次插入数据的根节点
         node = createBinaryBalancedTree(node, presentVal)
         start = False

AVL的删除节点

AVL的删除和二叉排序树的删除是一致的,唯一的区别就是删除之后加上上述的几个判断以及获取height的函数。

    def deleteBinarySortedTree(node, x):

        if node == None:
            return None

        if x < node.val:
            # 因为删除节点的返回值要么在这里,要么在下面的右子树,所以只要在返回的node的这个
            # 地方守着,检测不满足AVL,就调整就可以了。
            node.left = deleteBinarySortedTree(node.left, x)
            # 对比二叉排序树,就多了个下面这个
            # 删除只能在左子树删除,一定是右比左高,当高度差 =2 时,要么就是ll, 要么就是rl,如果是rl就要保证node.right.left不为空,还要保证node.right.right高度小于left的高度。
            if node.right.height - node.left.height == 2:
                # node.right.left不为空,说明是rl型,下面这句话画个图就明白了
                if node.right.left and height(node.right.left) > height(node.right.right):
                    rlTurn(node)
                else:
                    llTurn(node)
            

        if x > node.val:
            node.right = deleteBinarySortedTree(node.right, x)
            # 对比二叉排序树,就多了个下面这个
            if node.left.height - node.right.height == 2:
                if node.left.right and height(node.left.right) > height(node.left.left):
                    rrTurn(node)
                else:
                    lrTurn(node)
   
        if node.val == x:
            if node.left == None and node.right == None:
                return None

            if node.left == None and node.right != None:
                return node.right

            if node.left != None and node.right == None:
                return node.left

            if node.left != None and node.right != None:
                nodePresent = node.right
                while nodePresent:
                    maxNodeVal = nodePresent
                    nodePresent = nodePresent.left
                deleteBinarySortedTree(c1, maxNodeVal.val)
                maxNodeVal.left = node.left
                maxNodeVal.right = node.right
                return maxNodeVal
        # 对比二叉排序树,就多了个下面这行
        node.height = height(node)
        
        return node

对上述的代码进行说明:我们可以不返回node,因为所有的操作都是引用操作,一定能修改,但是如果不返回的话,我们最后的处理完毕的node没法获取,实际上最后处理完的node我们希望迭代的再次去处理,当前最后的node也会处理了,但是从代码完整性可读性、模板套路性(这是最主要的)等方面还是决定返回。事实上,可以不返回,迭代的使用和不迭代使用都可以不反回。

 

那么上述就是关于AVL的一些面试题目,实际的过程中,后面写一个关于   红黑树、B树、B+树,B*树以及各个应用的文档。

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值