算法——树和二叉树

时间:2023-02-13 22:34:40

一、树

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树。

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

  算法——树和二叉树