python3实现二叉树图文详解

树结构在计算机领域使用十分广泛。在操作系统源程序中,树和森林被用来构造文件系统。我们看到的window和linux等文件管理系统都是树型结构。在编译系统中,如C编译器源代码中,二叉树的中序遍历形式被用来存放C 语言中的表达式。在游戏设计领域,许多棋类游戏的步骤都是按树型结构编写。这一篇我们就来了解下树,并实现一下最基本的二叉树。

树(tree),和我们熟悉的顺序表一样,也是一种抽象的数据结构。不过顺序表是依次首尾相连,而树代表的是一对多的对应关系。如下图所示,是Linux的文件系统的一部分,典型的树形结构

1-tree.png

其结构有点像倒挂的树,根在上,叶在下。其具有如下几个特点

  • 每个节点有0个或多个子节点
  • 没有父节点的节点称为根节点
  • 每一个非根节点有且仅有一个父节点
  • 除了根节点,每个子节点可以分为多个不相交的子树

同时还有一些术语要了解下

  • 节点的度 - 一个节点含有的子树个数称为该节点的度,例如上面root节点的度为4
  • 树的度 - 一棵树中最大的节点的度称为树的度,上面树的度为4
  • 叶节点 - 度为0的节点
  • 兄弟节点 - 具有相同父节点的节点互称兄弟节点
  • 节点的层次 - 从根节点开始,根为第一层,其子节点为第二层,以此类推
  • 树的深度或高度 - 树中节点的最大层次,上面树的高度为3

树的物理存储

上面说的是树的逻辑结构,下面看下物理存储结构。

顺序表存储

从根一直到页,逐层从左到右放进顺序表。例如上面的树就会如下存放

['root','etc','home','var','proc','issue','passwd'...]

链表存储

链表方式,每个节点除了存储自身的内容,还有存储其子节点的地址。这种方式比较普遍。

二叉树

因为有发散关系,树的结构可以很复杂,我们只研究比较有规律的典型结构,二叉树。

二叉树(binary tree),的每个节点最多含有两个子树,例如下图

2-binary.png

二叉树还有几种特殊形式

  • 完全二叉树 - 除了最后一层,其他层的节点数都已经达到了最大值,且最后一层的的所有节点从左到右连续紧密排列,上面就是一个完全二叉树
  • 满二叉树 - 所有叶节点都在最下一层的完全二叉树称为满二叉树,上面的树不是满二叉树
  • 平衡二叉树 - 任何节点的两颗子树高度差不超过1的二叉树

二叉树代码实现

下面用代码实现二叉树,并完成添加节点,遍历节点两种操作。这里的添加结点采用从左至右,每层所有子节点添加完毕以后再添加下一层的方式。而遍历节点又会有深度优先广度优先这两种区别。

下面我们依次来看。

节点类

采用链表方式存储的节点类如下

class Node:
    """binary tree node"""

    def __init__(self, value, lchild=None, rchild=None):
        self.value = value
        self.lchild = lchild
        self.rchild = rchild

每个节点除了存储自身的值,还通过self.lchildself.rchild分别存储左节点和右节点的地址。默认地址都是None,表示没有子节点。

树类

树类在初始的时候只需要有一个根节点的地址即可

class Binary_Tree:
    """binary tree"""

    def __init__(self, root=None):
        self.root = root

默认不带参数的初始是一个空树。

添加节点

添加节点采取从左至右每层依次添加的顺序,每层填满了再添加到下一层

根据树的特性,这时候可以利用一个小技巧,就是利用队列的方式,首先把根节点放入队列,从列表中读取节点的时候就将该节点的子节点从左至右依次放入队列中,有点类似于前面说到的顺序表存储结构。一直读取到某个节点的子节点有一个或都不存在时,将新的节点做为该节点的子节点。

temp = [self.root]
while temp:
    nextNode = temp.pop(0)
    if nextNode.lchild is None:
        nextNode.lchild = node
        return
    elif nextNode.rchild is None:
        nextNode.rchild = node
        return
    else:
        temp.append(nextNode.lchild)
        temp.append(nextNode.rchild)

从临时列表的尾部放元素,从头部取元素,达到队列的效果。下面考虑下特殊情况,就是树为空的情况,直接把新的节点做为根节点即可,于是完整实现如下

def add(self, node):
    if self.root is None:
        self.root = node
        return
    temp = [self.root]
    while temp:
        nextNode = temp.pop(0)
        if nextNode.lchild is None:
            nextNode.lchild = node
            return
        elif nextNode.rchild is None:
            nextNode.rchild = node
            return
        else:
            temp.append(nextNode.lchild)
            temp.append(nextNode.rchild)

广度优先遍历

在进行遍历的时候,如果是按照添加时候的顺序,从上至下从左至右依次读取,就叫做广度优先遍历。

思路和添加元素时候一样的,也是利用队列来完成,不过这时候要一直循环到队列为空。

def breadth_travel(self):
    if self.root is None:
        return
    temp = [self.root]
    while temp:
        nextNode = temp.pop(0)
        print(nextNode.value, end=' ')
        if nextNode.lchild is None:
            continue
        elif nextNode.rchild is None:
            temp.append(nextNode.lchild)
            continue
        else:
            temp.append(nextNode.lchild)
            temp.append(nextNode.rchild)

这里为了方便打印在同一行,使用了end=' '

深度优先遍历

深度优先遍历指的是先把根节点一边的子树遍历到最后一层,再去遍历另一边的子树。而在对子树进行遍历的时候采用递归的方式。

根据父节点在两个子节点间的位置分为三种情况:父节点-左子节点-右子节点的先序遍历,左子节点-父节点-右子节点的中序遍历,以及左子节点-右子节点-父节点的后序遍历。

先序遍历

使用递归来实现非常简单

def preorder(self):
    self.__preorder(self.root)
    
def __preorder(self, node):
    if node is None:
        return
    print(node.value, end=' ')
    self.__preorder(node.lchild)
    self.__preorder(node.rchild)

为了实现递归必须要传递一个根节点的地址进去,但是在进行遍历的时候不希望用户带参数进行操作,于是写了两个方法。

遍历过程以下面这张图为例进行说明

2-binary.png

首先打印根节点1,然后是左子树。左子树同样先打印根节点2,再去处理下一层的左子树打印4,接着是8。之后是4这个子树的右子树9,然后是2这个子树的右子树5。然后是1这个根节点的右子树3,之后是3的左子树6,最后是7。所以理论的遍历结果会是[1,2,4,8,9,5,3,6,7],下面验证下

if __name__ == '__main__':
    tree = Binary_Tree()
    tree.add(Node(1))
    tree.add(Node(2))
    tree.add(Node(3))
    tree.add(Node(4))
    tree.add(Node(5))
    tree.add(Node(6))
    tree.add(Node(7))
    tree.add(Node(8))
    tree.add(Node(9))
    tree.breadth_travel()
    print('')
    tree.preorder()

结果符合预期

1 2 3 4 5 6 7 8 9 
1 2 4 8 9 5 3 6 7 
中序遍历

中序遍历只是在修改下打印顺序即可

def inorder(self):
    self.__inorder(self.root)

def __inorder(self, node):
    if node is None:
        return
    self.__inorder(node.lchild)
    print(node.value, end=' ')
    self.__inorder(node.rchild)

遍历过程以下面这张图为例进行说明

2-binary.png

首先处理根节点的左子树,而左子树也是先处理下一层子树,一直到8,然后是8的父节点4和右子节点9。之后是上一层的父节点2和右子节点5。然后是根节点1和右子树。右子树中也是先左子树6,然后是其父节点3和右子节点7。于是理论结果为[8,4,9,2,5,1,6,3,7]

验证下

if __name__ == '__main__':
    tree = Binary_Tree()
    tree.add(Node(1))
    tree.add(Node(2))
    tree.add(Node(3))
    tree.add(Node(4))
    tree.add(Node(5))
    tree.add(Node(6))
    tree.add(Node(7))
    tree.add(Node(8))
    tree.add(Node(9))
    tree.breadth_travel()
    print('')
    tree.preorder()
    print('')
    tree.inorder()

结果符合预期

1 2 3 4 5 6 7 8 9 
1 2 4 8 9 5 3 6 7 
8 4 9 2 5 1 6 3 7 
后序遍历

后续排列就不分析了,直接给出代码和结果

def postorder(self):
    self.__postorder(self.root)

def __postorder(self, node):
    if node is None:
        return
    self.__postorder(node.lchild)
    self.__postorder(node.rchild)
    print(node.value, end=' ')

验证结果为

8 9 4 5 2 6 7 3 1 

通过遍历结果还原树

这里指的当然是深度优先遍历,因为广度优先本身就是按照从上往下的顺序打印的。

如果要反向推到,从遍历结果到树的原型。首先必须要有2种遍历结果,同时其中一个必须是中序遍历。为什么这么说呢?因为只有中序遍历可以帮助我们找到根节点

假如有先序结果和中序结果如下

1 2 4 8 9 5 3 6 7 
8 4 9 2 5 1 6 3 7 

首先通过先序确定根节点是1,然后通过中序确定左子树和右子树。

接着又可以通过先序确定左子树的根节点2,如法炮制对中序结果进行分段,一直到最后。

完整代码

#! /usr/bin/env python
# -*- coding: utf-8 -*- 
# @author: xiaofu
# @date: 2020-Jul-24

class Node:
    """binary tree node"""

    def __init__(self, value, lchild=None, rchild=None):
        self.value = value
        self.lchild = lchild
        self.rchild = rchild


class Binary_Tree:
    """binary tree"""

    def __init__(self, root=None):
        self.root = root

    def add(self, node):
        if self.root is None:
            self.root = node
            return
        temp = [self.root]
        while temp:
            nextNode = temp.pop(0)
            if nextNode.lchild is None:
                nextNode.lchild = node
                return
            elif nextNode.rchild is None:
                nextNode.rchild = node
                return
            else:
                temp.append(nextNode.lchild)
                temp.append(nextNode.rchild)

    def breadth_travel(self):
        if self.root is None:
            return
        temp = [self.root]
        while temp:
            nextNode = temp.pop(0)
            print(nextNode.value, end=' ')
            if nextNode.lchild is None:
                continue
            elif nextNode.rchild is None:
                temp.append(nextNode.lchild)
                continue
            else:
                temp.append(nextNode.lchild)
                temp.append(nextNode.rchild)


    def preorder(self):
        self.__preorder(self.root)


    def inorder(self):
        self.__inorder(self.root)


    def postorder(self):
        self.__postorder(self.root)


    def __preorder(self, node):
        if node is None:
            return
        print(node.value, end=' ')
        self.__preorder(node.lchild)
        self.__preorder(node.rchild)

    def __inorder(self, node):
        if node is None:
            return
        self.__inorder(node.lchild)
        print(node.value, end=' ')
        self.__inorder(node.rchild)

    def __postorder(self, node):
        if node is None:
            return
        self.__postorder(node.lchild)
        self.__postorder(node.rchild)
        print(node.value, end=' ')


if __name__ == '__main__':
    tree = Binary_Tree()
    tree.add(Node(1))
    tree.add(Node(2))
    tree.add(Node(3))
    tree.add(Node(4))
    tree.add(Node(5))
    tree.add(Node(6))
    tree.add(Node(7))
    tree.add(Node(8))
    tree.add(Node(9))
    tree.breadth_travel()
    print('')
    tree.preorder()
    print('')
    tree.inorder()
    print('')
    tree.postorder()

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值