zoukankan      html  css  js  c++  java
  • 算法——树和二叉树

    一、树

    1、什么是树?

      树是一种数据结构,比如:目录结构。

      树是一种可以递归定义的数据结构。

      定义:树是由n个节点组成的集合

        如果n=0,那这是一棵空树;

        如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。

      

    2、相关概念

      根节点: 根节点(root)是树的一个组成部分,也叫树根。它是同一棵树中除本身外所有节点的祖先,没有父节点。

      叶子节点(终端节点):一棵树当中没有子节点(即度为0)的结点称为叶子结点,简称“叶子”。 叶子是指度为0的结点,又称为终端结点。

      树的深度(高度):树中节点的最大层次。

      节点的度:一个节点含有的子树的个数称为该节点的度。

      树的度:一棵树中,最大的节点的度称为树的度。

      父节点(双亲节点):若一个节点含有子节点,则这个节点称为其子节点的父节点;

      子节点(孩子节点):一个节点含有的子树的根节点称为该节点的子节点;

      子树:设T是有根树,a是T中的一个顶点,由a以及a的所有后裔(后代)导出的子图称为有向树T的子树。

    3、树的实例——模拟文件系统

    class Node:
        def __init__(self, name, type='dir'):
            self.name = name
            self.type = type    # 类型可以是"dir"或"file"
            self.children = []
            self.parent = None
    """链式存储""" def __repr__(self): return self.name class FileSystemTree: def __init__(self): self.root = Node("/") # 根目录 self.now = self.root # 当前目录 def mkdir(self, name): """创建目录""" if name[-1] != "/": name += "/" # 判断当不是以"/"结尾,添加"/" node = Node(name) # 创建文件夹 self.now.children.append(node) node.parent = self.now def ls(self): """展示当前目录下的所有目录""" return self.now.children def cd(self, name): """切换路径""" if name[-1] != "/": name += "/" # 判断当不是以"/"结尾,添加"/" if name == "../": self.now = self.now.parent return for child in self.now.children: if child.name == name: self.now = child return raise ValueError("invalid dir") tree = FileSystemTree() tree.mkdir("var/") tree.mkdir("bin/") tree.mkdir("usr/") print(tree.root.children) # [var/, bin/, usr/] print(tree.ls()) # [var/, bin/, usr/] tree.cd("bin/") tree.mkdir("python/") print(tree.ls()) # [python/] tree.cd("../") print(tree.ls()) # [var/, bin/, usr/]

      树绝大多数的存储都是和链表一样链式存储。往后指child;往前指parent。通过节点和节点间相互连接的关系来组成这么一个数据结构。

    二、二叉树

      二叉树:度不超过2的树。如下所示:

      

      每个节点最多有两个孩子节点,两个孩子节点被区分为左孩子节点和右孩子节点。

    1、特殊二叉树——满二叉树

      一个二叉树如果每一层的节点数都达到最大值,则这个二叉树就是满二叉树。

    2、特殊二叉树——完全二叉树  

      叶节点只能出现在最下层和次下层,并且最下面一层的节点都集中在该层最左边的若干位置的二叉树。

      

      满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。堆是一个特殊的完全二叉树。

    三、二叉树的存储方式(表示方式)

      二叉树这种数据结构在计算机中的存储方法。

    1、链式存储方式

       二叉树的链式存储:将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。

     (1)节点定义

    class BiTreeNode:
        def __init__(self, data):  # data就是传进去的节点值
            self.data = data
            self.lchild = None
            self.rchild = None

    (2)根据给定图片生成二叉树

      

      代码如下:

    class BiTreeNode:
        def __init__(self, data):
            self.data = data
            self.lchild = None   # 左孩子
            self.rchild = None   # 右孩子
    
    
    # 创建二叉树节点
    a = BiTreeNode("A")
    b = BiTreeNode("B")
    c = BiTreeNode("C")
    d = BiTreeNode("D")
    e = BiTreeNode("E")
    f = BiTreeNode("F")
    g = BiTreeNode("G")
    
    # 节点连接
    e.lchild = a
    e.rchild = g
    a.rchild = c
    c.lchild = b
    c.rchild = d
    g.rchild = f
    
    # 指定根节点
    root = e
    
    print(root.lchild.rchild.data)  # C

    2、顺序存储方式

      所谓顺序存储方式就是二叉树用列表来存储。如下图所示就是用列表来存储二叉树。

      

      如上图二叉树标出了元素所对应的索引,则可以有以下结论:

    (1)父节点和左孩子节点的编号下标有什么关系?

      父与左子下标关系:0-1  1-3 2-5 3-7 4-9

      i (父)——>2i+1 (子)

      如果已知父亲节点为i,那么他的左孩子节点为2i+1

    (2)父节点和右孩子节点的编号下标有什么关系?

      父与右子下标关系:0-2 1-4 2-6 3-8 4-10

      i (父)——>2i+2 (子)

      如果知道父亲节点为i,那么他的右孩子节点为2i+2

    (3)知道孩子找父亲规律?  

      知道左孩子求父节点:(n-1)/2=i

      知道右孩子求父节点:(n-2)/2=i 

    四、二叉树的遍历方式

      

    1、前序遍历:EACBDGF

      访问根节点操作发生在遍历其左右子树之前。

    def pre_order(root):
        """前序遍历"""
        if root:   # 如果不为空(递归条件)
            print(root.data, end=',')   # 访问自己
            pre_order(root.lchild)      # 递归左子树
            pre_order(root.rchild)      # 递归右子树
    
    pre_order(root)   # E,A,C,B,D,G,F,

    2、中序遍历:ABCDEGF

      访问根节点的操作发生在遍历其左右子树之间。

    def in_order(root):
        """中序遍历"""
        if root:
            in_order(root.lchild)     # 递归左子树
            print(root.data, end=',') # 访问自己
            in_order(root.rchild)     # 递归右子树
    
    in_order(root)      # A,B,C,D,E,G,F,  

    3、后序遍历:BDCAFGE

      访问根节点的操作发生在遍历其左右子树之后。

    def post_order(root):
        """后序遍历"""
        if root:
            post_order(root.lchild)    # 递归左子树
            post_order(root.rchild)    # 递归右子树
            print(root.data, end=",")  # 访问自己
    
    post_order(root)    # B,D,C,A,F,G,E,

    4、层次遍历:EAGCFBD

      层次遍历很好理解,需要利用到队列。不仅适用二叉树也适用多叉树。

      用一个队列保存被访问的当前节点的左右孩子以实现层序遍历。

    from collections import deque
    
    def level_order(root):
        """层次遍历"""
        queue = deque()
        queue.append(root)
        while len(queue) > 0:    # 只要队不空
            node = queue.popleft()   # 出队
            print(node.data, end=',')
            if node.lchild:
                queue.append(node.lchild)
            if node.rchild:
                queue.append(node.rchild)
    
    level_order(root)   # E,A,G,C,F,B,D,

    5、给定一个树的两种遍历方式,就可推导出这个树

      例如:前序遍历——EACBDGF;中序遍历——ABCDEGF。

      由此可知E是根节点,E的左边包含ABCD,右边包含GF。且A是根节点的左节点、G是根节点的右节点。

      BCD是A的子节点,由于中序遍历ABCD可知A的左节点是空的,右节点包含BCD,由前序ACBD可知C是A的右子节点。再由中序遍历BCD可知B是C的左节点,D是C的右节点。

      GF是根节点右边节点,G是右节点,F是G的子节点。由中序GF可知F是G节点的右节点。至此推导出整个树。

    五、二叉树应用——二叉搜索树

      二叉搜索树是一颗二叉树且满足性质:设x是二叉树的一个节点。如果y是x左子树的一个节点,那么y.key <= x.key;如果y是x右子树的一个节点,那么y.key >= x.key。

      

      总结来说:二叉搜索树的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有节点的值均大于它根节点的值;它的左右子树也都是二叉搜索树。

    1、二叉搜索树的插入

    class BiTreeNode:
        def __init__(self, data):
            self.data = data
            self.lchild = None   # 左孩子
            self.rchild = None   # 右孩子
            self.parent = None   # 加了parent就是双链表
    
    
    class BST:
        def __init__(self, li=None):
            self.root = None
            if li:
                for val in li:
                    self.insert_no_rec(val)
    
        def insert(self, node, val):
            """
            递归插入
            :param node: 节点
            :param val: 要插入的值
            :return:
            """
            if not node:
                node = BiTreeNode(val)
            elif val < node.data:
                node.lchild = self.insert(node.lchild, val)
                node.lchild.parent = node
            elif val > node.data:
                node.rchild = self.insert(node.lchild,val)
                node.rchild.parent = node
            # else:  # "="  else不用写了
            return node
    
        def insert_no_rec(self, val):
            """非递归插入"""
            p = self.root
            if not p:   # 空树的情况处理
                self.root = BiTreeNode(val)
                return
            while True:
                if val < p.data:   # 小于根节点往左边走
                    if p.lchild:   # 如果左孩子存在
                        p = p.lchild
                    else:          # 左子树不存在
                        p.lchild = BiTreeNode(val)
                        p.lchild.parent = p
                        return
                elif val > p.data:    # 大于根节点往右边走
                    if p.rchild:  # 如果右孩子存在
                        p = p.rchild
                    else:         # 右子树不存在
                        p.rchild = BiTreeNode(val)
                        p.rchild.parent = p
                        return
                else:         # 等于的时候,什么都不干(类似集合)
                    return
    
        def pre_order(self, root):
            """前序遍历"""
            if root:  # 如果不为空(递归条件)
                print(root.data, end=',')  # 访问自己
                self.pre_order(root.lchild)  # 递归左子树
                self.pre_order(root.rchild)  # 递归右子树
    
        def in_order(self, root):
            """中序遍历"""
            if root:
                self.in_order(root.lchild)  # 递归左子树
                print(root.data, end=',')  # 访问自己
                self.in_order(root.rchild)  # 递归右子树
    
        def post_order(self, root):
            """后序遍历"""
            if root:
                self.post_order(root.lchild)  # 递归左子树
                self.post_order(root.rchild)  # 递归右子树
                print(root.data, end=",")  # 访问自己
    
    
    tree = BST([4,6,7,9,2,1,3,5,8])
    tree.pre_order(tree.root)
    print("")
    tree.in_order(tree.root)
    print("")
    tree.post_order(tree.root)
    """
    4,2,1,3,6,5,7,9,8,
    1,2,3,4,5,6,7,8,9,  # 注意中序是有序的
    1,3,2,5,8,9,7,6,4,
    """
    

      可以注意到中序遍历输出的是有序的,做如下验证:

    import random
    li = list(range(500))
    random.shuffle(li)
    tree = BST(li)
    tree.in_order(tree.root)  # 0,1,2,3,4,5,...,496,497,498,499
    

      这是因为二叉搜索树的性质导致二叉搜索树的左孩子一定是最小的,因此它的中序序列一定是升序的。

    2、二叉搜索树的查询操作

    class BST:
        """代码省略"""
    
        def query(self, node, val):
            """
            递归查询
            :param node: 要递归的节点
            :param val: 要查询的值
            :return:
            """
            if not node:   # 如果node是空,则找不到
                return None   # 递归终止条件
    
            if val > node.data:   # 大于node的值往右边找
                return self.query(node.rchild, val)
            elif val < node.data:  # 小于node的值往左边找
                return self.query(node.lchild, val)
            else:
                return node    # 值相同返回当前节点
    
        def query_no_rec(self, val):
            """非递归查询"""
            p = self.root
            while p:   # 如果树不为空
                if p.data < val:  # 大于p的值往右边找
                    p = p.rchild
                elif p.data > val:  # 小于p的值往左边找
                    p = p.lchild
                else:
                    return p
            return None   # 树为空,递归终止条件
    
    import random
    li = list(range(0, 500, 2))
    random.shuffle(li)
    
    tree = BST(li)
    print(tree.query_no_rec(3))  # None
    print(tree.query_no_rec(6))  # <__main__.BiTreeNode object at 0x103d01cc0>
    print(tree.query_no_rec(6).data)   # 6

    3、二叉搜索树的删除操作

    (1)如果要删除的节点是叶子节点

      操作方法是:直接删除

       

    (2)如果要删除的节点只有一个孩子

      操作方法是:将此节点的父亲与孩子连接,然后删除该节点。

      

    (3)如果要删除的节点有两个孩子

      操作方法:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。

      

    (4)代码实现如下所示:

    class BiTreeNode:
        def __init__(self, data):
            self.data = data
            self.lchild = None   # 左孩子
            self.rchild = None   # 右孩子
            self.parent = None   # 加了parent就是双链表
    
    
    class BST:
        """代码省略"""
        def __remove_node_1(self, node):
            """情况1:node是叶子节点"""
            if not node.parent:   # 此叶子节点没有父节点,说明树中就这一个节点
                self.root = None   # 将这唯一的节点删除
    
            if node == node.parent.lchild:   # node是父亲的左孩子
                node.parent.lchild = None    # 父亲与node断联系
                node.parent = None           # node与父亲断联系(这句可写可不写)
            else:    # node是父亲的右孩子
                node.parent.rchild = None    # # 父亲与node断联系
    
        def __remove_node_21(self, node):
            """情况2-1:node只有一个左孩子"""
            if not node.parent:   # 如果node是根节点
                self.root = node.lchild    # 将node的左孩子置为根节点
                node.lchild.parent = None  # 将新根节点的父亲设为空
            elif node == node.parent.lchild:  # 如果node是它父亲的左孩子
                node.parent.lchild = node.lchild   # node父节点的左孩子设为node的左孩子
                node.lchild.parent = node.parent   # node左孩子的父节点设为node的父节点
            else:  # 如果node是它父亲的右孩子
                node.parent.rchild = node.lchild   # node父节点的右孩子指向node的左孩子
                node.lchild.parent = node.parent   # node左孩子的父亲指向node的父节点
    
        def __remove_node_22(self, node):
            """情况2-2:node只有一个右孩子"""
            if not node.parent:   # 如果node是根节点
                self.root = node.rchild  # 将node的右孩子置为根节点
                node.rchild.parent = None  # 将新根节点的父亲设为空
    
            elif node == node.parent.lchild:   # 如果node是父亲的左孩子
                node.parent.lchild = node.rchild   # 将node父节点的左孩子指向node的右孩子
                node.rchild.parent = node.parent
    
            else:   # 如果node是父亲的右孩子
                node.parent.rchild = node.rchild   # 将node父节点的右孩子指向node的右孩子
                node.rchild.parent = node.parent
    
        def delete(self, val):
            if self.root:   # 如果不是空树
                node = self.query_no_rec(val)
                if not node:  # 如果node不存在
                    return False
                if not node.lchild and not node.rchild:   # 如果node是叶子节点
                    self.__remove_node_1(node)
                elif not node.rchild:    # 如果没有右孩子(只有一个左孩子)
                    self.__remove_node_21(node)
                elif not node.lchild:    # 如果没有左孩子(只有一个右孩子)
                    self.__remove_node_22(node)
                else:    # 如果两个孩子都有
                    min_node = node.rchild
                    while min_node.lchild:   # 一直查找node右孩子的左子树的左孩子,直到没有为止
                        min_node = min_node.lchild
                    node.data = min_node.data   # 将min_node.data的值赋给node.data
                    # 删除min_node
                    if min_node.rchild:   # 如果min_node只有右孩子
                        self.__remove_node_22(min_node)
                    else:  # 如果min_node没有孩子
                        self.__remove_node_1(min_node)
    
    
    tree = BST([1,4,2,5,3,8,6,9,7])
    tree.in_order(tree.root)   # 1,2,3,4,5,6,7,8,9,
    print("")
    tree.delete(4)
    tree.in_order(tree.root)   # 1,2,3,5,6,7,8,9,
    print("")
    tree.delete(1)
    tree.delete(8)
    tree.in_order(tree.root)   # 2,3,5,6,7,9,
    

    4、二叉搜索树的效率  

      平均情况下,二叉搜索树进行搜索的时间复杂度为O(logn)。

      最坏情况下,二叉搜索树可能非常偏斜,时间复杂度退化到O(n)。如下所示:

      

      解决方案:

      (1)随机化的二叉搜索树(打乱顺序插入),有时是是不是插入的那打乱插入就不好用。

      (2)AVL树

    六、AVL树

      AVL树 

    七、二叉搜索树扩展应用——B树

      B树(B-Tree):B树是一棵自平衡的多路搜索树。常用于数据库的索引,最常用数据库的索引就是哈希表、B树。

      如下所示,一个节点存了两个值,分成了三路。

      

      

  • 相关阅读:
    翻译:关于Evaluation Stack
    beanshell 响应数据的解析与变量的保存
    nmon 采坑
    linux 防火墙管理
    服务器 安装docker (启动坑了很久才成功)docker-compose
    数据库负载均衡 happroxy 中间器(Nginx)容器的安装与配置
    docker 中搭建 mysql pxc 集群
    字节面试
    中缀表达式转为后缀表达式
    SpringBoot解决thymeleaf引入公共部分问题
  • 原文地址:https://www.cnblogs.com/xiugeng/p/9645855.html
Copyright © 2011-2022 走看看