python数据结构课堂笔记6:树

文章目录

      • 树概念
      • 树结构术语
      • 树的遍历Tree Traversals
      • 优先队列和二叉堆
        • 二叉堆的实现
      • 二叉查找树BST
        • 算法分析
      • 平衡二叉查找树:AVL
        • AVL树总结
      • 总结
      • tips
        • 不可变对象immutable和可哈希对象hashable
        • 所有的树操作都是从根开始的

树概念

  • 树–非线性数据结构
  • 数据结构树分为根、枝、叶等三部分
    一般数据结构的图示把根放在上方、叶放在下方
    分类树是层次化的
    分类树的一个节点的子节点和另一个节点的子节点相互隔离、独立
    分类树每一个叶节点都具有唯一性

树结构术语

  • 节点Node:组成树的基本部分
    每个节点具有名称、或“键值”,节点还可以保存额外数据项,数据项根据不同的应用而变
  • 边Edge:边是组成树的另一个基本部分
    • 每条边恰好连接两个点,表示节点之间具有关联,边有出入方向;
    • 每个节点(除根节点)恰有一条来自另一节点的入边;
    • 每个节点可以有多条连到其他节点的出边;
  • 根Root:树中唯一一个没有入边的节点
  • 路径Path:由边依次连接在一起的节点的有序列表
  • 子节点Children:入边均来自于同一个节点的若干节点,称为这个节点的子节点
  • 父节点Parent:一个节点是其所有出边所连接节点的父节点
  • 兄弟节点Sibling:具有同一个父节点的节点之间称为兄弟节点
  • 子树Subtree:一个节点和其所有子孙节点,以及相关边的集合
  • 叶节点Leaf:没有子节点的节点称为叶节点
  • 层级Level:从根节点开始到达一个节点的路径,所包含的边的数量,称为这个节点的层级
  • 高度:树中所有节点的最大层级称为树的高度
  • 树的定义:
    • 树由若干节点,以及两两连接节点的边组成,并有以下性质:
      • 其中一个节点被设定为根;
      • 每个节点n(除根节点),都恰连接一条来自节点p的边,p是n的父节点;
      • 每个节点从根开始的路径是唯一的
      • 如果每个节点最多有两个子节点,这样的树称为“二叉树”
    • 树是:(递归定义:树是基本结束条件)
      空集;
      或者由根节点及0或多个子树构成(其中子树也是树),每个子树的根到根节点具有边相连
  • 表达式解析树算法思路
    叶节点保存操作数,内部节点保存操作符
    表达式层次决定计算的优先级,越底层的表达式,优先级越高
    树中每一个子树都可以表示为一个子表达式
    1. 用树结构做以下尝试
      从全括号表达式构建表达式解析树
      利用表达式解析树对表达式求解
      从表达式解析树恢复原表达式的字符串形式
    2. 首先,全括号表达式要分解为单词Token列表
      其单词分为括号“()”、操作符“±*/”和操作数“0~9”这几类
      左括号就是表达式的开始,而右括号是表达式的结束
    3. 从左到右扫描全括号表达式的每个单词,根据规则建立解析树
      如果当前单词是“(”,为当前节点添加一个新节点,作为其左子节点,当前节点下降为这个新节点
      如果当前单词是操作符“+,-,/,*”,将当前节点的值设为此符号,为当前节点添加一个新节点作为其右子节点,当前节点下降为这个新节点
      如果当前单词是操作数,将当前节点的值设为此数,当前节点上升为父节点
      如果当前单词是“)”,则当前节点上升为父节点
    4. 创建树过程中关键是对当前节点的跟踪
      创建左右子树可调用insertLeft/Right
      当前节点设置值,可以调用setRootVal
      下降到左右子树可调用getLeft/RightChild
      但是上升到父节点,这个没有办法支持
    5. 我们可以用一个栈来记录跟踪父节点
      当前节点下降时,将下降前的节点push入栈
      当前节点需要上升到父节点时,上升到pop出栈的节点即可
    6. 由于二叉树是一个递归数据结构,自然可以用递归算法来处理
      求值递归函数evaluate
      可从树的底层子树开始,逐步向上层求值,最终得到整个表达式的值
    7. 求值函数evaluate的递归三定律
      基本结束条件:叶节点是最简单的子树,没有左右子树,其根节点的数据项即为子表达式树的值
      缩小规模:将表达式树分为左子树、右子树,即为缩小规模
      调用自身:分别用evaluate计算左子树和右子树的值,然后将左右子树的值依据节点的操作符进行计算,从而得到表达式的值
    8. 增加程序可读性的技巧,函数引用
         import operator
         op = operator.add
      
    9. 全括号表达式解析树代码
    from dataStructurewithPython.pythonds.basic.stack import Stack
    from dataStructurewithPython.pythonds.trees.binaryTree import BinaryTree
    
    def buildParseTree(fpexp):
      fplist = fpexp.split()
      pStack = Stack()
       eTree = BinaryTree('')
      pStack.push(eTree)
      currentTree = eTree
      for i in fplist:
          if i == "(":
              currentTree.insertLeft('')
              pStack.push(currentTree)
              currentTree = currentTree.getLeftChild()
          elif i not in ['+','-','/','*',')']:
              currentTree.setRootVal(int(i))
              parent = pStack.pop()
              currentTree = parent
          elif i in ['+','-','/','*']:
              currentTree.setRootVal(i)
              currentTree.insertRight('')
              pStack.push(currentTree)
              currentTree = currentTree.getRightChild()
          elif i == ")":
              currentTree = pStack.pop()
          else:
              raise ValueError
      return eTree
    
    1. 表达式解析树计算代码
    import operator
    
    def evaluate(pareseTree):
        opers = {'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}
        leftc = pareseTree.getLeftChild()#缩小规模
        rightc = pareseTree.getRightChild()
    
        if leftc and rightc:
           fn = opers[pareseTree.getRootVal()]
           return fn(evaluate(leftc),evaluate(rightc))#递归调用
        else:
           return pareseTree.getRootVal()#基本结束条件
    

树的遍历Tree Traversals

  • 对一个数据集中的所有数据项进行访问的操作叫做“遍历Traversal”
  • 线性结构中,对其所有数据项的访问比较简单直接,按照顺序依次进行即可
  • 树的非线性特点,使得遍历操作较为复杂
  • 我们按照对节点访问次序的不同来区分3种遍历
    • 前序遍历(preorder):先访问根节点,再递归地前序访问左子树、最后前序访问右子树
    • 中序遍历(inorder):先递归地中序访问左子树,再访问根节点,最后中序访问右子树
    • 后序遍历(postorder):先递归地后序访问左子树,再后序访问右子树,最后访问根节点
    • 前序遍历
    def preorder(tree):
       if tree:
           print(tree.getRootVal())
           preorder(tree.getLeftChild())
           preorder(tree.getRightChild()) 
    
    • 后序遍历
    def postorder(tree):
        if tree != None:
             postorder(tree.getLeftChild())
             postorder(tree.getRightChild())
             print(tree.getRootVal())
    
    • 中序遍历
    def inorder(tree):
        if tree != None:
            inorder(tree.getLeftChild())
            print(tree.getRootVal())
            inorder(tree.getRightChild())
    
    • 也可以在Binary Tree类中实现前序遍历的方法:
      需要加入子树是否为空的判断
    def preorder(self):
        print(self.key)
        if self.leftChild:#判断左子树是否存在
           self.leftChild.preorder()
        if self.rightChild:#判断右子树是否存在
           self.rightChild.preorfer()
    
    • 后序遍历:表达式求值
      可以采用后序遍历重写表达式求值代码:
          
    def postordereval(tree):
        opers = {'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}
        res1 = None
        res2 = None
        if tree:
           res1 = postordereval(tree.getLeftChild())
           res2 = postordereval(tree.getRightChild())
           if res1 and res2:
              return opers[tree.getRootVal()](res1,res2)
           else:
              return tree.getRootVal()    
    
    • 采用中序遍历递归算法来生成全括号中缀表达式
    def printexp(tree):
        sVal = " "
        if tree:
           sVal = '(' + printexp(tree.getLeftChild())
           sVal = sVal + str(tree.getRootVal())
           sVal = sval + printexp(tree.getRightChild()) + ')'
        return sVal
    

优先队列和二叉堆

  • 队列的一种变体“优先队列”
  • 优先队列的出队跟队列一样从队首出队
  • 但在优先队列内部,数据项的次序却是由“优先级”来确定的
    高优先级的数据项排在队首,低优先级的数据项排在后面
    优先队列的入队操作就比较复杂,需要将数据项根据其优先级尽量挤在队列前方
  • 实现优先队列的经典方案就是采用二叉堆数据结构
    二叉堆能够将优先队列的入队和出队复杂度都保持在O(long n),需要实现像归并排序一样的性质
  • 二叉堆的有趣之处在于,其逻辑结构像二叉树,却是用非嵌套的列表来实现的
  • 最小key排在队首的称为“最小堆min heap”,反之,最大key排在队首的称为“最大堆max heap”
  • 二叉堆Binary Heap ADT操作定义:
    BinaryHeap():创建一个空二叉堆对象
    insert(k):将新key加入到堆中,O(long n)
    findMin():返回堆中的最小项,最小项仍保留在堆中
    delMin():返回堆中的最小项,同时从堆中删除,O(long n)
    isEmpty():返回堆是否为空
    size():返回堆中key的个数
    buildHeap(list):从一个key列表创建新堆
  • 为了使堆操作能保持在对数水平,就必须采用二叉树结构
  • 同样,如果要使操作始终保持在对数数量级上,就必须始终保持二叉树的“平衡”
    树根左右子树拥有相同数量的节点
  • 我们采用“完全二叉树”的结构来近似实现“平衡”
    完全二叉树,叶节点最多只出现在最底层和次底层,而且最底层的叶节点都连续集中在最左边,每个内部节点都有两个子节点,最多可有1个节点例外
  • 完全二叉树由于其特殊性,可以用非嵌套列表,以相对简单的方式实现,具有很好的性质
    如果节点下标为p,那么其左子节点下标为2p,右子节点坐标为2p+1,其父节点下标为p//2
  • 堆次序Heap Order
    任何一个节点x,其父节点p中的key均小于x中的key
    这样,符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小

二叉堆的实现

  • 二叉堆初始化
    采用一个列表来保存堆数据,其中表首下标为0的项无用,但为了后面代码可以用到简单的整数乘除法,仍保留他.
    堆的性质:在任何一条路径上,都是有一个有序的数列,而且最小要任何一个节点都比父节点大(任何一个节点都比子节点小)

    class BinHeap:
       def __init__(self):
           self.heaplist = [0]
           self.currentSize = 0
    
  • insert(key)方法
    首先,为了保持“完全二叉堆”的性质,新key应该添加到列表末尾。
    新key加到列表末尾,显然无法保持“堆”次序,需要将新key沿着路径来“上浮”到其正确位置

        def percUp(self,i):
            while i // 2 > 0:
               if self.heaplist[i] < self.heaplist[i//2]:
                  temp = self.heaplist[i//2]
                  self.heaplist[i//2] = self.heaplist[i]
                  self.heaplist[i] = temp
               i = i // 2
    
        def insert(self,key):
            self.heapList.append(key)#添加到末尾
            self.currentSize = self.currentSize + 1
            self.percUp(self.currentSize)#新key上浮
    
  • delMin()方法
    移走整个堆中最小的key:根节点heapList[1]
    为了保持“完全二叉树”的性质,只用最后一个节点来代替根节点,仍然破坏了“堆”次序
    解决办法:将新的根节点沿着一条路径“下沉”,直到比两个子节点都小
    下沉路径的选择:如果比子节点大,那么选择较小的子节点来交换下沉

    def percDown(self,i):
        while (i * 2) <= self.currentSize:
            mc = self.minChild(i)
            if self.heapList[i] > self.heapList[mc]:
                temp = self.heapList[i]
                self.heapList[i] = self.heapList[mc]#交换下沉
                self.heapList[mc] = temp
            i = mc#沿路径向下
    
    def minChild(self,i):
        if i * 2 +1 > self.currentSize:
            return i * 2#唯一节点
        else:#返回娇小的
            if self.heapList[i*2] 
  • buildHeap(lst):从无序表生成“堆”
    用下沉法,能够将总代价控制在O(n)

    def buildHeap(self,alist):
        i = len(alist) // 2#从最后节点的父节点开始,因叶节点无需下沉
        self.currentSize = len(alist)
        self.heapList = [0] + alist[:]
        print(len(self.heapList),i)
        while (i >0):
            print(self.heapList,i)
            self.percDown(i)
            i = i - 1
        print(self.heapList,i)
    

二叉查找树BST

  • ADT MAP中,可以采用不同的数据结构和搜索算法来保存和查找key,如有序表数据结构+二分搜索算法(O(long n)),散列表数据结构+散列及冲突解决算法(O(1))

  • ADT Map定义的操作如下:
    Map():创建一个空映射,返回空映射对象;
    put(key,val):将key-val关联对加入映射中,如果key已存在,将val替换旧关联值
    get(key):给定key,返回关联的数据值,如不存在,则返回None
    del:通过del map[key]的语句形式删除key-val关联
    len():返回映射中key-val关联的数目
    in:通过key in map的语句形式,返回key是否存在于关联中,布尔值

  • 二叉查找树BST的性质

    • 比父节点小的key都出现在左子树,比父节点大的key都出现在右子树,查找复杂度可降低到O( l o g 2 n log_2 n log2n)
    • BST的生成跟数据插入的次序有关,插入的次序不同,生成的BST也不同
  • 二叉搜索树的实现:节点和链接结构

    • 需要用到BST和TreeNode两个类,BST的root成员引用根节点TreeNode
    class BinarySearchTree:#BST类
    
    def __init__(self):
        self.root = None
        self.size = 0
    
    def length(self):
        return self.size
    
    def __len__(self):
        return self.size
    
    def __iter__(self):#可供迭代 for node in bst
        return self.root.__iter__()
      
    class TreeNode:#treeNode类
    def __init__(self,key,val,left=None,\
                 right=None,parent=None):
        self.key = key#键值
        self.payload = val#包含的数据项,跟键值关联的数据
        self.leftChild = left#左子节点
        self.rightChild = right#右子节点
        self.parent = parent#指向父节点,方便回溯操作
    
    def hasLeftChild(self):#判断treeNode是否拥有左子节点
        return self.leftChild
    
    def hasRightChild(self):#判断treeNode是否拥有右子节点
        return self.rightChild
    
    def isLeftChild(self):#判断treeNode是否是父节点的左子节点
        return self.parent and \
            self.parent.leftChild == self
    
    def isRightChild(self):#判断treeNode是否是父节点的右子节点
        return self.parent and \
            self.parent.rightChild == self
    
    def isRoot(self):#判断treeNode是否是根节点
        return not self.parent
    
    def isLeaf(self):#判断treeNode是否是叶节点
        return not (self.leftChild or self.rightChild)
    
    def hasAnyChildren(self):#判断treeNode是否有子节点
        return self.leftChild or self.rightChild
    
    def hasBothChildren(self):#判断treeNode是否同时有左右子节点
        return self.leftChild and self.rightChild
    
    def replaceNodeData(self,key,value,lc,rc):#把treeNode的所有值更换
        self.key = key
        self.payload = value
        self.leftChild = lc
        self.rightChild = rc
        if self.hasLeftChild():#如果有左子节点,那么左子节点的父节点也要更换
            self.leftChild.parent = self
        if self.hasRightChild():#如果有右子节点,那么右子节点的父节点也要更换
            self.rightChild.parent = self
    
  • BST.PUT方法:put(key,val)方法,插入key构造BST

    • 首先看BST是否为空,如果一个节点都没有,那么key成为根节点root.key最重要,因为其参加排序。
    • 否则,就调用一个递归函数_put(key,val,root)来放置key.
    def put(self,key,val):
        if self.root:
            self._put(key,val,self.root)#在哪个为根节点的子树上插入key
        else:
            self.root = TreeNode(key,val)
        self.size = self.size + 1
    
  • _put(key,val,self.root)的流程

    • 如果key比currentNode小,那么_put到左子树
      • 但如果没有左子树,那么key就成为左子节点
    • 如果key比currentNode大,那么_put到右子树
      • 但如果没有右子树,那么key就成为右子节点
    def _put(self,key,val,currentNode):
        if key < currentNode.key:
            if currentNode.hasLeftChild():
                self._put(key,val,currentNode.leftChild)
            else:
                currentNode.leftChild = TreeNode(key,val,parent=currentNode)
        else:
            if currentNode.rightChild():
                self._put(key,val,currentNode.rightChild)
            else:
                currentNode.rightChild = TreeNode(key,val,parent=currentNode)
    
  • 索引赋值
    特殊方法:

    def __setitem__(self, k, v):
        self.put(k,v)
    
  • BST.get方法
    在树中找到key所在的节点取到payload

    def get(self,key):
        if self.root:
            res = self._get(key,self.root)#递归函数,找到节点
            if res:
                return res.payload#找到节点
            else:
                return None
        else:
            return None
    
    def _get(self,key,currentNode):
        if not currentNode:
            return None
        elif currentNode.key == key:
            return currentNode
        elif key < currentNode.key:
            return self._get(key,currentNode.leftChild)
        else:
            return self._get(key,currentNode.rightChild)
    
  • 索引取值和归属判断

    • __getitem__特殊方法,实现val = myZipTree[‘PKU’]
    • __contains__特殊方法,实现’PKU’ in myZipTree的归属判断运算符in
    def __getitem__(self, key):
        return self.get(key)
    
    def __contains__(self, key):
        if self._get(key,self.root):
            return True
        else:
            return False
    
  • 迭代器

    • 用for循环枚举字典中的所有key,ADT map也应该实现这样的功能
    • 特殊方法__iter__可以用来实现for迭代
      BST类中的__iter__方法直接调用了TreeNode中的同名方法
    • TreeNode类中的__iter__迭代器
      迭代器函数中用了for迭代,实际上是递归函数
      yeild是对每次迭代的返回值
      中序遍历的迭代
    def __iter__(self):
        if self:#如果根节点不为空,递归基本结束条件
            if self.hasLeftChild():#如果左子树不为空
                for elem in self.leftChild:#递归调用
                    yield elem
            yield self.key#取根
            if self.hasRightChild():#如果右子树不为空
                for elem in self.rightChild:
                    yield elem
    
  • BST.delet方法

    • 最复杂的delet方法
      用_get找到要删除的节点,然后调用remove来删除,找不到则提示错误
    def delet(self,key):
        if self.size > 1:#如果不止一个节点
            nodeToRemove = self._get(key,self.root)#找到需要remove的节点
            if nodeToRemove:
                self.remove(nodeToRemove)
                self.size = self.size - 1
            else:
                raise KeyError('Error,key not in tree')
        elif self.size == 1 and self.root.key == key:#判断树上只有一个节点
            self.root = None
            self.size = self.size - 1
        else:
            raise KeyError('Error,key not in tree')
    
    • __delitem__特殊方法
      实现del myZipTree[‘Pku’’]这样的语句
    def __delitem__(self, key):
        self.delet(key)
    
    • delet中最复杂的是找到key对应的节点之后的remove节点方法!
    • BST.remove方法
      • 从BST中remove一个节点,还要求仍然保持BST性质,分为以下三种情况:
        • 这个节点没有子节点,直接删除
        • 这个节点有1个子节点,将唯一的子节点上移,替换掉被删节点的位置
        • 这个节点有2个子节点
      • 如果这个节点没有子节点,那么直接删除
      if currentNode.isLeaf():#没有子节点的情况
          if currentNode == currentNode.parent.leftChild:#如果其是左子节点
              currentNode.parent.leftChild = None
          else:
              currentNode.parent.rightChild = None#如果其是右子节点
      
      • 如果这个节点有1个子节点,那么就将唯一的子节点上移,替换掉被删节点的位置
        • 替换需要区分以下情况
          • 被删节点的子节点是左子节点还是右子节点?
          • 被删节点本身是父节点的左子节点还是右子节点?
          • 被删节点本身就是根节点?
      else:
          if currentNode.hasLeftChild():
              if currentNode.isLeftChild():#左子节点删除
                  currentNode.leftChild.parent = currentNode.parent
                  currentNode.parent.leftChild = currentNode.leftChild
              elif currentNode.isRigthChild():#右子节点删除
                  currentNode.leftChild.parent = currentNode.parent
                  currentNode.parent.rightChild = currentNode.leftChild
              else:#根节点删除
                  currentNode.replaceNodeData(currentNode.leftChild.key,
                                              currentNode.leftChild.payload,
                                              currentNode.leftChild.leftChild,
                                              currentNode.leftChild.rightChild)
          else:
              if currentNode.isLeftChild():
                  currentNode.rightChild.parent = currentNode.parent
                  currentNode.parent.leftChild = currentNode.rightChild
              elif currentNode.isRightChild():
                  currentNode.rightChild.parent = currentNode.parent
                  currentNode.parent.rightChild = currentNode.rightChild
              else:
                  currentNode.replaceNodeData(currentNode.rightChild.key,
                                              currentNode.rightChild.payload,
                                              currentNode.rightChild.leftChild,
                                              currentNode.rightChild.rightChild)
      
      • 被删除节点有2个子节点
        这时无法简单地将某个子节点上移替换被删节点
        但可以找到另一个合适的节点来替换被删接节点
        这个合适节点就是被删节点的下一个key值节点
        即被删节点右子树中最小的那个,称为“后继”,
        后继只有两种情况,要么是一个叶节点,要么只有唯一的一个右子节点
        将后继节点摘出来,替换掉被删节点
      elif currentNode.hasBothChildren():
          succ = currentNode.findSuccessor()
          succ.spliceOut()
          currentNode.key = succ.key
          currentNode.payload = succ.payload
      
      • TreeNode类:寻找后继节点
        def findSuccessor(self):
            succ = None
            if self.hasRightChild():
                succ = self.rightChild.finMin()
            else:#目前不会遇到
                if self.parent:
                    if self.isLeftChild():
                        succ = self.parent
                    else:
                        self.parent.rightChild = None
                        succ = self.parent.findSuccessor()
                        self.parent.rightChild = self
            return succ
      
        def fimMin(self):
            current = self
            while current.hasLeftChild():
                current = current.leftChild#直到左下角的节点
            return current
      
      • TreeNode类:摘出节点spliceOut()
      def spliceOut(self):
          if self.isLeaf():#摘出叶节点
             if self.isLeftChild():
                 self.parent.leftChild = None
             else:
                 self.parent.rightChild = None
          elif self.hasAnyChildren():
               if self.hasLeftChild():#目前不会遇到
                   if self.isLeftChild():
                       self.parent.leftChild = self.leftChild
                   else:
                       self.parent.rightChild = self.leftChild
                   self.leftChild.parent = self.parent
               else:#摘出带右子节点的节点
                   if self.isLeftChild():
                       self.parent.leftChild = self.rightChild
                   else:
                       self.parent.rightChild = self.rightChild
                   self.rightChild.parent = self.parent
      

算法分析

  • 其性能决定因素在于二叉查找树的高度(最大层次),而其高度又受数据项key插入顺序的影响
  • 如果key的列表是随机分布的话,那么大于和小于根节点Key的键值大致相当
  • BST的高度就是 l o g 2 n log_2 n log2n(n是节点的个数),而且这样的树就是平衡树
  • put方法的最差性能是O( l o g 2 n log_2 n log2n)。
  • 但key列表分布极端情况就完全不同:按照从小到大顺序插入的话,put方法的性能为O(n)

平衡二叉查找树:AVL

  • 在key插入时一直保持平衡的二叉查找树:AVL树
  • 利用AVL树实现ADT MAP,基本与BST相同
  • 不同之处在于二叉树的生成和维护过程
  • AVL树的实现中,需要对每个节点跟踪“平衡因子balance factor”参数
  • 平衡因子是根据节点的左右子树的高度来定义的,确切地说,是左右子树高度差:
    balanceFactor = height(leftSubTree) - height(rightSubTree)
    如果平衡因子大于0,称为“左重left-heavy”,小于0称为“右重right-heavy”
    平衡因子等于0,称为平衡
  • 如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉搜索树称为平衡树
  • 在平衡树操作过程中,有节点的平衡因子超出此范围,则需要一个重新平衡的过程,要保持BST性质!
  • AVL树最差情形下的性能:即平衡因子为1或-1
  • 最多搜索次数h和规模N的关系,AVL树搜索复杂度为O(log n)
  • 新key必定以叶节点形式插入AVL中,叶节点的平衡因子为0,但是叶节点会影响其父节点的平衡因子
    • 作为左子节点插入,则父节点平衡因子会增加1
    • 作为右子节点插入,则父节点平衡因子会减少1
      这种影响会随着父节点到根节点的路径一直传递下去,直到:
    • 传递到根节点为止;
    • 或者某个父节点平衡因子被调整为0,不再影响上层节点的平衡因子为止
      (无论从-1或者1调整到0,都不会改变子树高度)
  • 插入节点时,平衡因子会发生什么变化?
    每个新插入的节点,都是叶节点,平衡因子=0
    它作为左子节点(+1)或右子节点(-1),会让父节点平衡因子发生变化
    如果父节点平衡因子由原来的0变为非0(±1),表示这子树高度变化,平衡因子的影响需要向根方向传播
    超出范围就需要rebalance
  • 重新定义_put方法即可
    def _put(self,key,val,currentNode):
        if key < currentNode.key:
            if currentNode.hasLeftChild():
                self._put(key,val,currentNode.leftChild)
            else:
                currentNode.leftChild = TreeNode(key,val,parent=currentNode)
                self.updateBalance(currentNode.leftChild)#调整因子
        else:
            if currentNode.hasRightChild():
                self._put(key,val,currentNode.rightChild)
            else:
                currentNode.rightChild = TreeNode(key,val,parent=currentNode)
                self.updateBalance(currentNode.rightChild)#调整因子
    def updateBalance(self, node):
        if node.balanceFactor > 1 or node.balanceFactor < -1:
            self.rebalance(node)  # 重新平衡
            return
        if node.parent != None:
            if node.isLeftChild():
                node.parent.balanceFactor += 1
            elif node.isRightChild():
                node.parent.balanceFactor -= 1

            if node.parent.balanceFactor != 0:
                self.updateBalance(node.parent)#调整父节点因子
  • rebalance重新平衡
    主要手段:将不平衡的子树进行旋转ratation
    • 视“左重”或者“右重”进行不同方向的旋转
    • 同时更新相关父节点引用,更新旋转后被影响节点的平衡因子
    • 右重子树左旋转,将右子节点提升为子树的根,将旧根节点作为新根节点的左子节点
      如果新根节点原来有左子节点,则将此节点设置为新左子节点(旧根节点)的右子节点(新左子节点(旧根节点)的右子节点一定为空)
    • 更复杂的情况:左重子树右旋转
      旋转后,新根节点将旧根节点作为右子节点,但新根节点原来已有右子节点,需要将原有的右子节点重新定位。
      原有的右子节点改为旧根节点左子节点
      旧根节点的左子节点在旋转后一定为空
  • 左旋
    def rotateLeft(self, rotRoot):
        newRoot = rotRoot.rightChild#新根节点是旧根节点的右子节点
        rotRoot.rightChild = newRoot.leftChild#旧根节点的右子节点指向新根节点的左子节点
        if newRoot.leftChild != None:
            newRoot.leftChild.parent = rotRoot#如果新根节点有左子节点,那么需要把新根节点的左子节点挂到新左子节点(旧根节点)上
        newRoot.parent = rotRoot.parent#新根节点的父节点调整为旧根节点的父节点
        if rotRoot.isRoot():#判断如果旧根节点是树的根节点的话
            self.root = newRoot#确立新的树根
        else:
            if rotRoot.isLeftChild():#如果旧节点是左子节点
                rotRoot.parent.leftChild = newRoot
            else:
                rotRoot.parent.rightChild = newRoot
        newRoot.leftChild = rotRoot
        rotRoot.parent = newRoot
        rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)#调整旧根节点的平衡因子
        newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)#调整新根节点的平衡因子
  • 右旋
    def rotateRight(self, rotRoot):
        newRoot = rotRoot.leftChild
        rotRoot.leftChild = newRoot.rightChild
        if newRoot.rightChild != None:
            newRoot.rightChild.parent = rotRoot
        newRoot.parent = rotRoot.parent
        if rotRoot.isRoot():
            self.root = newRoot
        else:
            if rotRoot.isRightChild():
                rotRoot.parent.rightChild = newRoot
            else:
                rotRoot.parent.leftChild = newRoot
        newRoot.rightChild = rotRoot
        rotRoot.parent = newRoot
        rotRoot.balanceFactor = rotRoot.balanceFactor - 1 - max(newRoot.balanceFactor, 0)
        newRoot.balanceFactor = newRoot.balanceFactor - 1 + min(rotRoot.balanceFactor, 0)
  • 旋转实现更复杂的情形:单纯的左旋无法实现平衡,左旋变左重,再右旋又变右重
    • 左旋前检查右子节点的因子
      如果右子节点“左重”,先对它实施右旋,再实施原来左旋
    • 同样,在右旋转之前检查左子节点的因子
      如果左子节点“右重”,先对它进行左旋转,再实施原来的右旋转
    def rebalance(self, node):
        if node.balanceFactor < 0:#右重需要左旋
            if node.rightChild.balanceFactor > 0:
                # Do an LR Rotation
                self.rotateRight(node.rightChild)#右节点左重先右旋
                self.rotateLeft(node)
            else:
                # single left
                self.rotateLeft(node)#如果右子节点不是左重,则简单左旋
        elif node.balanceFactor > 0:#左重需要右旋
            if node.leftChild.balanceFactor < 0:
                # Do an RL Rotation
                self.rotateLeft(node.leftChild)#左子节点右重需左旋
                self.rotateRight(node)
            else:
                # single right
                self.rotateRight(node)#如果左子节点不右重,则简单右旋

AVL树总结

  • 经过复杂的put方法,AVL树始终维持平衡,get方法也始终保持在O(long n)高性能
  • 将AVL树的普通方法分为两部分:
    • 需要插入的新节点是叶节点,更新其所有父节点和祖先节点的代价最多是O(log n )
    • 如果插入的新节点引发了不平衡,重新平衡最多需要2次旋转,但旋转的代价和问题规模无关,是常数O(1)
    • 整个put方法的时间复杂度还是O(log n)

总结

  • ADT MAP各种实现算法时间复杂度数量级:
  有序表 散列表 二叉查找树(极端操作会退化到线性结构) AVL树
put O(n) O(1)->O(n) O( l o g 2 n log_2 n log2n)->O(n) O( l o g 2 n log_2 n log2n)
get O( l o g 2 n log_2 n log2n) O(1)->O(n) O( l o g 2 n log_2 n log2n)->O(n) O( l o g 2 n log_2 n log2n)
in O( l o g 2 n log_2 n log2n) O(1)->O(n) O( l o g 2 n log_2 n log2n)->O(n) O( l o g 2 n log_2 n log2n)
del O(n) O(1)->O(n) O( l o g 2 n log_2 n log2n)->O(n) O( l o g 2 n log_2 n log2n)

一般来讲,对于内存和计算时间的要求不高时,用散列表较为合适,然后是AVL树

tips

不可变对象immutable和可哈希对象hashable

  • hashable定义

    • 如果一个对象在其生命周期内有一个固定不变的哈希值(这需要__hash__()方法)[值不变或者id不变]且可以与其他对象进行比较操作
      (这需要__eq__()方法),那么这个对象就是可哈希对象(hashable).可哈希对象必须有相同的哈希值才算做相等。
    • 由于字典(dict)的键(key)和集合(set)[不可变对象]内部使用到了哈希值,所以只有可哈希(hashable)对象才能被用作字典的
      键和集合的元素。
    • 所有python内置的不可变对象都是可哈希的,同时,可变容器(比如列表(list)或者字典(dict))都是不可哈希的。
      用户自定义的类的实例默认情况下都是可哈希的;它们和其它对象都不相等(除了它们自己),它们的哈希值来自id()方法。
  • == 对应 eq
    is对应id()

  • 可哈希对象如果有相同的hash值,就是相等的==

  • 不可哈希对象的相等,要枚举容器里的数据项逐个判断相等

  • id相同的对象,既相等(==),又同一(is):即同一个内存中

所有的树操作都是从根开始的

yield与return功能相似,当执行到yield时,会退出来,同时返回值
区别:return执行时,后面虽然不会执行,但下次再次调用时会从头开始
yield执行时,下次再次调用时,会从断点接着执行(yield处即为断点)

要求不高时,用散列表较为合适,然后是AVL树

你可能感兴趣的:(数据结构python版课堂笔记)