笔记-数据结构与算法-树

  • 视频链接:https://www.bilibili.com/video/BV1VC4y1x7uv?
  • 章节:P56-P73

树是一种基本的非线性数据结构

  • 示例:分类树:界、门、纲、目、科、属、种

特征:

1、层次化:数是一种分层结构,越接近顶部的层越普遍,越接近底部的层次越独特

2、节点之间相互隔离、独立

3、每一个叶节点都具有唯一性,从根开始到达每个种的完全路径来唯一标识每个物种

  • 其他示例:计算机文件系统、HTML文档(嵌套标记)、域名体系

树结构相关术语

  • 节点Node

组成树的基本部分,每个节点具有名称,或“键值”,节点还可以保存额外数据项,数据项根据不同的应用而变。

  • 边Edge

边是组成树的另一个基本部分,每条边恰好连接两个节点,表示节点之间具有关联,边具有出入方向;

每个节点(除根节点)恰有一条来自另一节点的入边,每个节点可以有多条连接到其他节点的出边

  • 根root

树中唯一没有入边的节点

  • 路径path

由边依次连接在一起的节点的有序列表

如:HTML->BODY->UL->LI,是一条路径
在这里插入图片描述

  • 子节点Children

入边均来自于同一个节点的若干节点,称为这个节点的子节点

  • 父节点Parent

一个节点是其所有的出边所连接节点的父节点

  • 兄弟节点Sibling

具有同一个父节点的节点之间称为兄弟节点

  • 子树Subtree

一个节点和其所有子孙节点,以及相关边的集合

  • 叶节点Leaf

没有子节点的节点称为叶节点

  • 层级Level

从根节点开始到达一个节点的路径,所包含的边的数量,称为这个节点的层级

如图:D的层级为2,根节点的层级为0

  • 高度

树中所有节点的最大层级称为树的高度

如图:右图树的高度为2

另一种表示法就是2+1=3 层

在这里插入图片描述

树的定义

  • 定义1

树是由若干节点,以及两两连接节点的边组成,并有如下性质:

  1. 其中一个节点被设定为根;
  2. 每个节点n(除根节点),都恰连接一条来自节点p的边,p是n的父节点;
  3. 每个节点从根开始的路径是唯一的
  4. 如果每个节点最多有两个子节点,这样的数称为“二叉树”
  • 定义2(递归定义)

树是空集或者由根节点及0或多个子树构成(其中子树叶也是树),每个子树的根到根节点具有边相连。

在这里插入图片描述

python实现树

基本操作:

常用方法:

  • binary_tree

    创建仅有根节点的二叉树

  • insert_left/insert_right

    将新节点插入树中作为其直接的左右子节点

  • get_root_val/set_root_val

    取得或返回根节点

  • get_left_child/get_right_child

    返回左右子树

实现方式:

​ 嵌套列表

[root,left,right]

​ 链表形式

def __init__(self, val):
    self.key = val
    self.left = None
    self.right = None

def insert_left(self, val):
    node = BinaryTree(val)
    if not self.left:
        self.left = node
    else:
        node.left = self.left
        self.left = node

树结构应用-表达式解析

在这里插入图片描述

树的遍历

  • 前序遍历

遍历顺序:根节点、左节点、右节点

例子:一本书的章节阅读

封面->第一章->第一章的第一小节->第一章的第二小节->第一章的第二小节的第一子小节->第一章的第二小节的第二子小节->第二章->第二章的第一小节->第二章的第二小节->第二章的第二小节的第一子小节->第二章的第二小节的第二子小节

在这里插入图片描述

def preorder(bt):
    """
    前序遍历
    :return:
    """
    if bt is None:
        return
    print(bt.get_root_val())
    preorder(bt.get_left_child())
    preorder(bt.get_right_child())
  • 中序遍历

遍历顺序:左节点、根节点、右节点

代码:修改print的位置,在递归调用的中间

  • 后序遍历

遍历顺序:左节点、右节点、根节点

代码:修改print的位置,在递归调用的最下面

优先队列与二叉堆

优先队列

根据数据的优先级确定出队顺序,优先队列的出队和队列一样从队首出队;但是优先队列内部,数据项的次序却是由“优先级”来确定:

高优先级的数据项排在队首,而低优先级的数据项则排在后面。这样优先队列的入队操作就比较复杂需要将数据项根据其优先级尽量挤到队列前方。

思考:使用有序表实现的话,入队的时间复杂度只能为O(n)

如果使用二叉堆来实现优先队列,能够将优先队列的入队和出队复杂度都保持在O(log n)

二叉堆可以保证队首的一直是最小值,所以优先级最高

二叉堆

  • 特点

1、二叉堆的特别之处在于,其逻辑结构上像二叉树,却是用非嵌套的列表来实现的
2、最小key排在队首的称为最小堆min heap
3、最大key排在队首的是最大堆max heap

  • 最小堆基本操作:

    • BinaryHeap():创建一个空二叉堆对象
    • insert(k):将新key加入到堆中
    • find_min: 返回堆中的最小项,最小项仍保留在堆中
    • del_min: 返回堆中的最小项,同时从堆中删除
    • is_empty(): 返回堆是否为空
    • size(): 返回堆中key的个数
    • build_heap(list): 从一个key列表创建新堆
  • 使用非嵌套列表实现二叉堆

为了使堆操作能保持在对数O(log n)水平上,就必须采用二叉树结构,同样如果是操作始终保持在对数数量级上,就必须始终保持二叉树的平衡,即满二叉树(下图,左右节点数必须相同)。

在这里插入图片描述

但是满二叉树的数量节点要始终保持在2^k - 1,如果数据项总数不是这样就无法使用。

可以采用“完全二叉树”的结构来近似的实现这种平衡

完全二叉树

完全二叉树,叶节点最多只出现在最底层和次底层,而且最底层的叶节点连续集中在最左边,每个内部节点都有两个子节点,最多可有1个节点例外。
在这里插入图片描述

  • 特点

完全二叉树,可以用非嵌套列表,以简单的方式实现,具有很好的性质,如果节点的下标为p,那么其左子节点下标为2p,右子节点为2p+1,其父节点下标为p//2

在这里插入图片描述

012345678910
0591114181921331727
  • 堆次序

任何一个节点x,其父节点p中的key均小于x中的key。这样符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小。

在这里插入图片描述

二叉查找树

P68

Binary Search Tree

映射结构,使用二叉查找树保持key,实现key的快速搜索

  • 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是不存在于关联中,布尔值
  • 性质

比父节点小的key都出现在左子树,

比父节点大的key都出现在右子树。
在这里插入图片描述

注意:原数据列项为70 31 93 94 14 23 73的顺序插入,这是用70作为树根,如果使用其他值比如31作为树根,那生成的BST就完全不同,所以插入顺序不同生成BST就不同

算法分析【以put为例】

性能决定因素在于二叉搜索树的高度(最大层次),而其高度受数据项key插入顺序的影响

key的列表是随机分布的话,那么大于和小于根节点key的键值分布大致相等,bst的高度就是log 2 n(n是节点的个数),这样的树称为平衡树,put方法最差性能为O(log 2 n)【参考二分法查找】

key列表分布极端情况就完成不同,按照从小到大顺序插入的话,生成一个单列表的形式,put方法的性能为O(n)

在这里插入图片描述

python实现

TreeNode
class 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 has_left_child(self):
        return self.leftChild

    def has_right_child(self):
        return self.rightChild

    def is_left_child(self):
        return self.parent and self.parent.leftChild == self

    def is_right_child(self):
        return self.parent and self.parent.rightChild == self

    def is_root(self):
        return not self.parent

    def is_leaf(self):
        return not (self.rightChild or self.leftChild)

    def has_any_children(self):
        return self.rightChild or self.leftChild

    def has_both_children(self):
        return self.rightChild and self.leftChild

    def replace_node_data(self, key, value, lc, rc):
        self.key = key
        self.payload = value
        self.leftChild = lc
        self.rightChild = rc
        if self.has_left_child():
            self.leftChild.parent = self
        if self.has_right_child():
            self.rightChild.parent = self

    def __iter__(self):
        """
        递归函数
        """
        # 判断非None
        if self:
            if self.has_left_child():
                # 遍历左子树,调用__iter__,
                # for循环就是递归调用自身
                for ele in self.leftChild:
                    # 返回子树key值
                    yield ele
            # 返回当前节点的key值
            yield self.key
            # 遍历右子树
            if self.has_right_child():
                for ele in self.rightChild:
                    yield ele
BinarySearchTree
class BinarySearchTree:

    def __init__(self):
        self.root = None
        self.size = 0

    def length(self):
        return self.size

    def __len__(self):
        return self.size

    def __iter__(self):
        return self.root.__iter__()
  • put

put(key,val) 插入key构造BST

  • bst为空,那么key成为根节点root
  • bst不为空,调用递归函数_put(key,val,root)来放置key_
    • _put(key,val,current_node)
      • 如果key比current_node小,那就_put到左子树,没有左子树,就成为左子节点
      • 如果key大,那就_put到右子树,没有右子树就成为右子节点
      • 相同时,替换payload值
def put(self, key, val):
    if self.root:
        self._put(key, val, self.root)
    else:
        self.root = TreeNode(key, val)
    self.size = self.size + 1

def _put(self, key, val, current_node):
    if key < current_node:
        if current_node.has_left_child():
            self._put(key, val, current_node.leftChild)
        else:
            current_node.leftChild = TreeNode(key, val, parent=current_node)
    elif key > current_node:
        if current_node.has_right_child():
            self._put(key, val, current_node.rightChild)
        else:
            current_node.rightChild = TreeNode(key, val, parent=current_node)
    else:
        current_node.payload = val
        
    def __setitem__(self, key, value):
        """
        []方式添加键和值
        :param key:
        :param value:
        :return:
        """
        self.put(key, value)
  • get

    get(key),找到key对应的val值

    • bst为空,返回None
    • _get(key, self.root)查找节点,对应key不存在,返回None,找到key,返回对应payload
      • 根节点则直接返回
      • 非根节点,_get(key,current_node)递归查找
        • key比current_node小,从左子树中查找,没有左子节点则返回None
        • 相等时直接返回current_node.payload
        • key比current_node大,从右子树中查找,没有右子节点则返回None
def get(self, key):
    if self.root:
        ret = self._get(key, self.root)
        if ret:
            return ret.payload
    return None

def _get(self, key, current_node):
    if not current_node:
        return None
    elif key < current_node.key:
        return self._get(key, current_node.leftChild)
    elif key == current_node.key:
        return current_node
    else:
        return self._get(key, current_node.rightChild)

def __getitem__(self, item):
    """
    []查询
    :param item:
    :return:
    """
    return self.get(item)

def __contains__(self, item):
    """
    判断是否包含'in'
    :param item:
    :return:
    """
    res = self.get(item)
    if res:
        return True
    return False
  • __iter__

用来实现for迭代,遍历key。for key in my_tree: print(key,my_tree[key])
BST类中的__iter__方法直接调用TreeNode中的同名方法
TreeNode __iter__,函数中用了for迭代,实际上是递归函数yield是对每次迭代的返回值,中序遍历的迭代

yield 生成器中使用,返回对应值并记录上下文

def __iter__(self):
    return self.root.__iter__()
  • delete

delete(key),删除节点

  1. bst.size=0,bst为空,抛异常

  2. bst.size=1,则bst只有root

    1. self.root.key==key,则初始化root=None,size-1=0
    2. self.root.key !=key,抛异常
    def delete(self, key):
        if self.size == :
            raise KeyError('error')
        elif self.size == 1 and self.root.key == key:
            self.root = None
            self.size = 0
        elif self.size > 1:
            re_node = self._get(key, self.root)
            if re_node:
                self.remove_node(re_node)
                self.size = self.size - 1
            else:
                raise KeyError('error')      
    
  3. bst.size >1:使用_get(key,self.root)递归查找节点

    1. key不存在,抛异常

    2. key存在,remove方法实现删除节点的细节,删除后,size-1

      1. remove节点之后需要保持二叉树的结构不变,需要从3种情况进行分析

        1. 这个节点是叶节点(没有子节点),直接删除,操作:父节点的左子or右子设为None

          在这里插入图片描述

          def remove_node(self, node):
              if node.is_leaf:
                  # 判断当前在父节点的位置
                  if node.parent.leftChild == node:
                      node.parent.leftChild = None
                  else:
                      node.parent.rightChild = None
          
        2. 被删除节点有1个节点时,将这个唯一的子节点上移,替换掉被删除的节点,替换操作需要区分几种情况

          1. 被删除节点的子节点是左,还是右子节点
          2. 被删除本身是其父节点的左、右
          3. 被删除本身是根节点

          在这里插入图片描述

          else:  # this node has one child
              if node.has_left_child():  # 当前节点拥有唯一的左子节点
                  if node.is_left_child():  # 左子节点删除
                      node.leftChild.parent = node.parent
                      node.parent.leftChild = node.leftChild
                  if node.is_right_child():  # 右子节点删除
                      node.leftChild.parent = node.parent
                      node.parent.rightChild = node.leftChild
                  else:  # 根节点删除
                      node.replace_node_data(node.leftChild.key,
                                             node.leftChild.payload,
                                             node.leftChild.leftChild,
                                             node.leftChild.rightChild,
                                             )
              else:  # 当前节点拥有唯一的右子节点
                  if node.is_left_child():  # 左子节点删除
                      node.rightChild.parent = node.parent
                      node.parent.leftChild = node.rightChild
                  if node.is_right_child():  # 右子节点删除
                      node.rightChild.parent = node.parent
                      node.parent.rightChild = node.rightChild
                  else:  # 根节点删除
                      node.replace_node_data(node.rightChild.key,
                                             node.rightChild.payload,
                                             node.rightChild.leftChild,
                                             node.rightChild.rightChild,
                                             )
          
        3. 被删除节点有2个子节点,无法直接将某个子节点上移替换被删除节点,但可以找另一个合适的节点来替换被删除节点,选用右子树中的最小节点,称为后继

          1、右子树任意大于左树全部,右子树的最小值,满足大于左树全部小于右树全部的要求

          2、这个后继节点最多就一个满足,使用这个后继节点替换被删除节点

          3、后继节点可能是叶节点or拥有唯一的右子节点

          在这里插入图片描述

          elif node.has_both_children():  # 节点拥有2个子节点
              # 查找后继
              succ = node.find_successor()
              # 后继子节点与后继父节点关联
              succ.splice_out()
              # 后继节点替换被删除节点key,value
              node.key = succ.key
              node.payload = succ.payload
          
          # class TreeNode
          def find_successor(self):
              succ = None
              if self.has_right_child():  # 被删除节点拥有2个子节点,所以这里必为true
                  succ = self.rightChild.find_min()  # 右子树的最小值
              return succ
          
          def find_min(self):
              current = self
              while current.has_left_child():  # 递归查找直至没有左子(比当前小的节点)
                  current = current.leftChild
              return current
          
          1. 摘出后继节点,将后继节点的子节点与父节点进行关联

            1. 后继节点为叶节点,将父节点的左or右子设置为None(同remove的第一种情况)
            2. 后继节点拥有右子节点,父的左or右子与后继的右子关联,右子父关联父节点
            # class TreeNode
            def splice_out(self):  # 摘出后继节点
                if self.is_leaf():  # 后继节点为叶节点
                    if self.is_left_child():
                        self.parent.leftChild = None
                    else:
                        self.parent.rightChild = None
            
                elif self.has_right_child():  # 后继节点有一个右子节点
                    # 极端情况,就是BST树被删除的节点的右子节点没有左子树,正好是最小的key
                    # find_min方法中没有进入while循环
                    if self.is_right_child():
                        self.parent.rightChild = self.rightChild
                    else:  # 最小值一般为父节点的左子树
                        self.parent.leftChild = self.rightChild
                    self.rightChild.parent = self.parent
            

额外说明:
在这里插入图片描述

AVL树-平衡二叉查找树

视频P71开始

基本上与BST的实现相同,不同之处在于二叉树的生成与维护过程。

实现方式:AVL树需要对每个节点跟踪“平衡因子balance factor”参数。

  • 平衡因子

平衡因子是根据节点的左右子树的高度来定义的,确切来说是左右子树的高度差:

balanceFactor = height(leftSubTree) - height(rightSubTree)

如果平衡因子大于0,称为“左重left-heavy”,小于零称为“右重”【下图】,平衡因子等于0,则称为平衡。

在这里插入图片描述

  • 平衡树

如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉树搜索树称为平衡树。

  • 操作:重新平衡

在平衡树操作过程中,有节点的平衡因子超出此范围,则需要一个重新平衡的过程(要保持BST的性质)
在这里插入图片描述

见上图,可以将B提上去作根节点,A降下来作左子节,这样就完成平衡了

  • AVL树的性能

AVL树最差下的性能,平衡因子为1或者-1。

下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,看问题规模(总节点数N)和对比次数(树的高度h)之间的关系

在这里插入图片描述
性能结论:AVL树的搜索时间复杂度为O(log n)

树的小结

映射结构时间复杂度对比

有序表(索引映射)散列表二叉查找树AVL树
putO(n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
getO(log 2 n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
inO(log 2 n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
delO(n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)

散列表存在散列冲突的存在,使时间复杂度增加,大于O(1),最差为O(n)

二叉查找树在极端插入顺序情况下,生成一个类似线性表的结构,使时间复杂度增加至O(n)

下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,看问题规模(总节点数N)和对比次数(树的高度h)之间的关系

[外链图片转存中…(img-Qp3JwJs9-1715872409511)]

性能结论:AVL树的搜索时间复杂度为O(log n)

树的小结

映射结构时间复杂度对比

有序表(索引映射)散列表二叉查找树AVL树
putO(n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
getO(log 2 n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
inO(log 2 n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)
delO(n)O(1)->O(n)O(log 2 n)->O(n)O(log 2 n)

散列表存在散列冲突的存在,使时间复杂度增加,大于O(1),最差为O(n)

二叉查找树在极端插入顺序情况下,生成一个类似线性表的结构,使时间复杂度增加至O(n)

推荐:有序使用散列表,更复杂的情况可以使用AVL树

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值