小言_互联网的博客

【数据结构Python描述】树的前序、中序、后序、广度优先遍历详解及Python手工实现

532人阅读  评论(0)

之前的文章中我们手动实现了一个二叉树类LinkedBinaryTree,虽然在该类中我们实现了全部的修改类方法以及绝大多数的非修改类方法,但是对于部分涉及二叉树结点遍历的非修改类方法如__iter__()positions(),我们并未实现。

原因在于:树形结构相较于线性结构(如:列表、链表、栈、队列等)复杂很多,其遍历算法也复杂很多,因此本文将单独介绍并实现树的各种遍历算法。

一、 树的遍历方法简介

1. 树的前序遍历

树的前序遍历:对于树T的前序遍历,首先被访问的树的根结点,然后根结点处的各子树以递归的方式依次被遍历,如果树T是有序的,那么子树(或子结点)被遍历所遵循的顺序按照子树(或子结点)对应根结点(或父结点)的顺序。

例如,还是以前文贾家家谱图为例,自贾代善以降,其子孙辈结点的前序遍历顺序如下图所示:

由此可以给出树的前序遍历伪代码

Algorithm preorder(T, p):
	执行对于位置p处结点访问操作
	for child in T.children(p) do:
		preorder(T, child)  {
   在以child为根结点的子树处递归调用preorder}

2. 树的后序遍历

树的后序遍历:树的后续遍历与前序遍历基本一致,区别仅在于根结点的遍历是第一步还是最后一步。

上述自贾代善以降,其子孙辈结点的后续遍历顺序如下图所示:

由此可以给出树的后序遍历伪代码

Algorithm postorder(T, p):
	for child in T.children(p) do:
		postorder(T, child)  {
   在以child为根结点的子树处递归调用postorder}
	执行对于位置p处结点访问操作

前序后序遍历都是遍历树的高效算法,对于其时间复杂度的分析可以仿照之前实现计算树的高度的算法,即关注每一个位置p处的非递归操作部分

实际上,类似分析树的高度计算算法的时间复杂度,对于每一个位置p处,其非递归操作的时间复杂度为 O ( c p + 1 ) O(c_p+1) O(cp+1),即执行T.children(p)的时间复杂度,其中 c p c_p cp为该位置结点的子结点数量,假设对一个结点的访问操作时间复杂度都是 O ( 1 ) O(1) O(1),则遍历操作的总时间复杂度为 O ( n ) O(n) O(n)

3. 树的广度优先遍历

尽管前序和后续遍历是依次访问树中每个结点的常用高效算法,另外一种遍历的方法可以实现在先遍历完深度 d d d处的所有结点后再遍历所有深度 d + 1 d+1 d+1处所有结点,这种遍历方式称为树的广度优先遍历

实际上,广度优先遍历在象棋、围棋、五子棋等棋类游戏的人机对战中十分常见,例如:在下列(仅列出部分情况)的简化版五子棋格中,一开始棋盘为空,玩家执X,电脑执O,根据玩家先手后可能会在三个不同位置落子,电脑后续会先遍历出所有可能走法然后选择胜率最高的方法。

下面给出了树的广度优先遍历的伪代码,因为我们并未在一次性遍历一个子树的所有结点,因此该过程并非递归式的。事实上,我们在其中使用了一个遵循先进先出原则的队列来保证该算法是按照树的结点层次来进行遍历

Algorithm breadth_first(T):
	初始化队列Q使其包含树T.root()
	while Q is not empty do:
		p = Q.dequeue()
		执行访问位置p处结点的操作
		for child in T.children(p) do:
			Q.enqueue(c)  {
   将位置p处结点的所有子结点加入队列尾部供后续遍历}

由于调用上述算法实现的方法时会进行 n n n次的入队操作enqueue() n n n次的出队操作dequeue(),故该算法的时间复杂度为 O ( n ) O(n) O(n)

4. 二叉树的中序遍历

上述关于一般树的前序、后序、广度优先遍历算法,对于二叉树这类特殊的树也均适用,而此处我们引出另一种适用于二叉树的遍历算法——中序遍历

二叉树的中序遍历可视为从左向右遍历树T,如下图所示,仍以之前文章中提及的使用二叉树表示有效代数算式为例,在二叉树中序遍历的情况下,只有位置p处的左子树(左子结点)被遍历完毕后才会遍历其右子树(右子结点)。

下面是二叉树中序遍历的伪代码:

Algorithm inorder(p):
	if p has a left child lc then:
		inorder(lc)
	执行访问位置p处结点的操作
	if p has a right child rc then:
		inorder(rc)

二、树的遍历算法实现

之前定义树的ADT时,我们指出树T应该包含下列两个方法:

  • T.positions():生成树T所有位置的一个迭代;
  • T.__iter__():生成保存在树T中所有对象元素的一个迭代。

根据上面介绍的树的各个遍历算法,下面给出positions()方法的实现:

1. 前序遍历算法实现

由于前序遍历算法对所有类型的树都是适用的,因此我们在前述介绍树的实现中定义的抽象基类Tree中对其进行实现,即T.preorder()可产生对树T所有结点的位置的一个迭代。

需要注意的是,在本文之前介绍树的前序遍历算法时,该递归算法接收一个参数即结点位置的引用,以实现以此位置的结点为根结点的整个树或子树的所有结点进行遍历。

针对上述情况,常见的解决方案是根据本文之前介绍树的前序遍历算法,实现一个非公有方法_subtree_preorder(),然后在公有方法preorder()中调用该方法的同时传入根结点位置的引用。具体实现如下述代码所示:

def preorder(self):
    """
    产生一个树的所有结点位置的前序迭代
    :return: 
    """
    if not self.is_empty():
        for p in self._subtree_preorder(self.root()):
            yield p

def _subtree_preorder(self, p):
    """
    针对根结点在位置p处的子树,产生关于子树所有结点位置的一个前序迭代
    :param p: 结点位置
    :return: 
    """
    yield p  # 后续先访问位置p处结点,再访问以其为根结点的子树中所有结点
    for child in self.children(p):  # 
        for each in self._subtree_preorder(child):  # 对child的子树做前序遍历
            yield each  # 懒惰式生成结点位置给生成器调用者

实际上,分析上述代码后可知,preorder()_subtree_preorder()两个均为生成器,且二者均通过惰性方式每次返回一个结点的位置给到调用者,由调用者来进行如何对某一位置进行处理。

基于上述实现的前序遍历,我们可以很容易在树的抽象基类Tree中实现positions()方法:

def positions(self):
    """生成一个树的结点位置的迭代"""
    return self.preorder()

2. 后续遍历算法实现

后续遍历算法的实现和上述前序遍历算法的实现十分类似,唯一的区别在于需要最后才返回整树或子树的根结点的位置:

def postorder(self):
    """
    产生一个树的所有结点位置的后序迭代
    :return: 
    """
    if not self.is_empty():
        for p in self._subtree_postorder(self.root()):
            yield p

def _subtree_postorder(self, p):
    """
    针对根结点在位置p处的子树,产生关于子树所有结点位置的一个后序迭代
    :param p: 结点位置
    :return: 
    """
    for child in self.children(p):
        for each in self._subtree_postorder(child):  # 对child的子树做后序遍历
            yield each  # 懒惰式生成结点位置给生成器调用者
    yield p  # 先遍历以位置p处结点为根结点的子树,再访问位置p处结点

3. 广度优先遍历算法实现

下面是在前述介绍树的实现中定义的抽象基类Tree中实现广度优先遍历算法的代码,这里使用了前述文章中利用单向线性链表实现的队列LinkedQueue

def breadth_first(self):
    """
    生成树通过广度优先遍历得到的所有位置的一个迭代
    :return: 
    """
    if not self.is_empty():
        fringe = LinkedQueue()
        fringe.enqueue(self.root())
        while not fringe.is_empty():
            p = fringe.dequeue()
            yield p
            for child in self.children(p):
                fringe.enqueue(child)

4. 二叉树中序遍历算法实现

由于前序、后续和广度优先遍历算法适用于所有类型的树,因此我们将通过其实现的方法定义在抽象基类Tree中,故这些方法会被类BinaryTreeLinkedBinaryTree等继承使用。

对于树的中序遍历算法,由于其显式基于左、右子结点的概念,因此我们将其实现定义在二叉树抽象基类BinaryTree中,在具体实现与前序和后序遍历算法的实现类似:

def inorder(self):
    """
    产生二叉树的一个中序遍历得到的结点位置迭代
    :return: 
    """
    if not self.is_empty():
        for p in self._subtree_inorder(self.root()):
            yield p

def _subtree_inorder(self, p):
    """
    针对根结点在位置p处的子二叉树,产生关于子二叉树树所有结点位置的一个中序迭代
    :param p: 结点位置
    :return: 
    """
    if self.left(p) is not None:  # 如果位置p处结点存在左子结点,则遍历其左子树
        for each in self._subtree_inorder(self.left(p)):
            yield each
    yield p  # 在遍历位置p处结点的左子树之后,且在遍历右子树之前访问位置p处结点
    if self.right(p) is not None:  # 如果位置p处结点存在右子结点,则遍历其右子树
        for each in self._subtree_inorder(self.right(p)):
            yield each

至此,我们实现了本文介绍的所有树的遍历算法,且根据需要可以通过调用不同的算法实现来实现positions()方法。

对于__iter__()方法,只要在positions()方法的基础上将每个返回位置的元素取出再返回即可实现,即:

def __iter__(self):
    """生成一个树的结点元素的迭代"""
    for p in self.positions():
        yield p.element()

该方法和positions()方法一样均定义在一般树的抽象基类Tree中。

三、附录:代码测试

为了综合验证文章【数据结构Python描述】——树的简介、基本概念和手动实现一个二叉树(真·全网最全!)和本文中实现的代码,下面是将两篇文章中实现的代码进行了整合,并实现如下测试案例:

  • 创建三个同类型二叉树tt1t2,其中:
    • t中仅有一个结点,该结点保存元素'A'
    • t1中有三个结点,根结点保存元素'B',其左右子结点分别保存元素'D''E'
    • t2中有三个结点,根结点保存元素'C',其左右子结点分别保存元素'F''G'
  • t1t2t唯一结点处进行链接,使得t1t2分别为在保存'A'元素处结点的左右子树;
  • 分别使用前序、后续、中序和广度优先遍历对上述链接后得到的树t进行遍历。

为方便读者理解,下面先给出最后对上述t使用四种遍历算法的示意图:

前序遍历示意

后序遍历示意

中序遍历示意

广度优先遍历示意

完整测试代码

具体测试代码如下:

from abc import ABCMeta, abstractmethod
from linked_queue import LinkedQueue


class Tree(metaclass=ABCMeta):
    """表示一般树的抽象基类"""

    class Position(metaclass=ABCMeta):
        """嵌套定义的位置类,其实例对象用于描述结点在树中的位置"""

        @abstractmethod
        def element(self):
            """
            用于返回某位置处的对象元素
            :return: 结点处的对象元素
            """

        @abstractmethod
        def __eq__(self, other):
            """
            使用'=='判断两个Position实例是否代表同一个位置
            :param other: 另一个Position的实例
            :return: Boolean
            """

        @abstractmethod
        def __ne__(self, other):
            """
            使用'!='判断两个Position实例是否不代表同一个位置
            :param other: 另一个Position的实例
            :return: Boolean
            """

    @abstractmethod
    def __len__(self):
        """
        返回树中所有结点数量
        :return: 结点数量
        """

    @abstractmethod
    def root(self):
        """返回代表数根结点的位置对象,如树为空则返回None"""

    @abstractmethod
    def parent(self, p):
        """
        返回位置p处结点的父结点所在位置,如p处为根结点则返回None
        :param p: 某结点所在位置
        :return: 某结点的父结点所在位置
        """

    @abstractmethod
    def num_children(self, p):
        """
        返回位置p处结点的子结点数目
        :param p: 结点位置
        :return: 结点的子结点数目
        """

    @abstractmethod
    def children(self, p):
        """
        生成位置p处结点的所有子结点的一个迭代
        :param p: 结点位置
        :return: 子结点位置
        """

    def preorder(self):
        """
        产生一个树的所有结点位置的前序迭代
        :return:
        """
        if not self.is_empty():
            for p in self._subtree_preorder(self.root()):
                yield p

    def _subtree_preorder(self, p):
        """
        针对根结点在位置p处的子树,产生关于子树所有结点位置的一个前序迭代
        :param p: 结点位置
        :return:
        """
        yield p  # 后续先访问位置p处结点,再访问以其为根结点的子树中所有结点
        for child in self.children(p):  #
            for each in self._subtree_preorder(child):  # 对child的子树做前序遍历
                yield each  # 懒惰式生成结点位置给生成器调用者

    def postorder(self):
        """
        产生一个树的所有结点位置的后序迭代
        :return:
        """
        if not self.is_empty():
            for p in self._subtree_postorder(self.root()):
                yield p

    def _subtree_postorder(self, p):
        """
        针对根结点在位置p处的子树,产生关于子树所有结点位置的一个后序迭代
        :param p: 结点位置
        :return:
        """
        for child in self.children(p):
            for each in self._subtree_postorder(child):  # 对child的子树做后序遍历
                yield each  # 懒惰式生成结点位置给生成器调用者
        yield p  # 先遍历以位置p处结点为根结点的子树,再访问位置p处结点

    def breadth_first(self):
        """
        生成树通过广度优先遍历得到的所有位置的一个迭代
        :return:
        """
        if not self.is_empty():
            fringe = LinkedQueue()
            fringe.enqueue(self.root())
            while not fringe.is_empty():
                p = fringe.dequeue()
                yield p
                for child in self.children(p):
                    fringe.enqueue(child)  # 向队尾加入位置p处结点的子结点

    def positions(self):
        """生成一个树的结点位置的迭代"""
        return self.preorder()

    def __iter__(self):
        """生成一个树的结点元素的迭代"""
        for p in self.positions():
            yield p.element()

    def is_root(self, p):
        """如果位置p处的结点为根结点则返回True"""
        return self.root() == p

    def is_leaf(self, p):
        """如果位置p处的结点无任何子结点则返回True"""
        return self.num_children(p) == 0

    def is_empty(self):
        """如果树为空,则返回True"""
        return len(self) == 0

    def depth(self, p):
        """
        返回位置p处结点的深度
        :param p: 结点位置
        :return: 结点深度
        """
        if self.is_root(p):
            return 0
        else:
            return 1 + self.depth(self.parent(p))

    def _height(self, p):
        """
        返回位置p处结点的高度
        :param p: 结点位置
        :return: 结点高度
        """
        if self.is_leaf(p):
            return 0
        else:
            return 1 + max(self._height(child) for child in self.children(p))

    def height(self, p=None):
        """
        返回位置p处结点的高度,默认返回根结点高度
        :param p: 结点位置
        :return: 结点高度
        """
        if p is None:
            p = self.root()
        return self._height(p)


class BinaryTree(Tree, metaclass=ABCMeta):
    """表示二叉树的抽象基类"""

    @abstractmethod
    def left(self, p):
        """
        返回位置p处结点的左子结点,如该处结点无左子结点则返回None
        :param p: 结点位置
        :return: 结点位置或None
        """

    @abstractmethod
    def right(self, p):
        """
        返回位置p处结点的右子结点,如该处结点无右子结点则返回None
        :param p: 结点位置
        :return: 结点位置或None
        """

    def sibling(self, p):
        """
        返回位置p处结点的兄弟结点,如该处结点无兄弟结点则返回None
        :param p: 结点位置
        :return: 结点位置或None
        """
        parent = self.parent(p)
        if parent is None:  # 如果位置p处为根结点,则该位置处的结点无兄弟结点
            return None
        else:
            if p == self.left(parent):  # 如果位置p处是左结点,则返回右结点位置(可能无)
                return self.right(parent)
            else:  # 如果位置p处是右结点,则返回左结点位置(可能无)
                return self.left(parent)

    def children(self, p):
        """
        生成位置p处结点的子结点迭代
        :param p: 结点位置
        :return: 结点位置迭代
        """
        if self.left(p) is not None:
            yield self.left(p)
        if self.right(p) is not None:
            yield self.right(p)

    def inorder(self):
        """
        产生二叉树的一个中序遍历得到的结点位置迭代
        :return:
        """
        if not self.is_empty():
            for p in self._subtree_inorder(self.root()):
                yield p

    def _subtree_inorder(self, p):
        """
        针对根结点在位置p处的子二叉树,产生关于子二叉树树所有结点位置的一个中序迭代
        :param p: 结点位置
        :return:
        """
        if self.left(p) is not None:  # 如果位置p处结点存在左子结点,则遍历其左子树
            for each in self._subtree_inorder(self.left(p)):
                yield each
        yield p  # 在遍历位置p处结点的左子树之后,且在遍历右子树之前访问位置p处结点
        if self.right(p) is not None:  # 如果位置p处结点存在右子结点,则遍历其右子树
            for each in self._subtree_inorder(self.right(p)):
                yield each


class LinkedBinaryTree(BinaryTree):
    """采用链式结构实现的二叉树"""

    class _Node:  # 用于表示结点的类
        __slots__ = 'element', 'parent', 'left', 'right'

        def __init__(self, element, parent=None, left=None, right=None):
            self.element = element
            self.parent = parent
            self.left = left
            self.right = right

    class Position(BinaryTree.Position):
        """用于描述结点位置的类"""

        def __init__(self, container, node):
            self._container = container
            self._node = node

        def element(self):
            """返回保存在该位置处的对象元素"""
            return self._node.element

        def __eq__(self, other):
            """当self和other表示同一个结点的位置时,返回True"""
            return type(self) is type(other) and other._node is self._node

        def __ne__(self, other):
            """当self和other表示不同结点的位置时返回True"""
            return not (self == other)

    def _pos2node(self, p: Position):
        """如果位置p有效,返回该位置处的结点引用"""
        if not isinstance(p, self.Position):
            raise TypeError('位置p:', p, '必须为准确的Position类型!')
        if p._container is not self:
            raise ValueError('位置p', p, '不属于当前位置列表!')
        if p._node.parent is None and p._node.element is None:
            raise ValueError('位置p', p, '已失效!')
        return p._node

    def _node2pos(self, node):
        """根据给定结点,创建并返回位置引用,当结点为None时返回None"""
        return self.Position(self, node) if node is not None else None

    def __init__(self):
        """创建一个空的二叉树"""
        self._root = None
        self._size = 0

    def __len__(self):
        """返回二叉树实例中结点个数"""
        return self._size

    def root(self):
        """返回代表二叉树根结点位置的对象,当二叉树为空时返回None"""
        return self._node2pos(self._root)

    def parent(self, p):
        """返回代表位置p处结点的父结点的位置对象"""
        node = self._pos2node(p)
        return self._node2pos(node.parent)

    def left(self, p):
        """返回代表位置p处结点的左子结点的位置对象,如无左子结点则返回None"""
        node = self._pos2node(p)
        return self._node2pos(node.left)

    def right(self, p):
        """返回代表位置p处结点的右子结点的位置对象,如无右子结点则返回None"""
        node = self._pos2node(p)
        return self._node2pos(node.right)

    def num_children(self, p):
        """返回位置p处结点的子结点数量"""
        node = self._pos2node(p)
        count = 0
        if node.left is not None:
            count += 1
        if node.right is not None:
            count += 1
        return count

    def positions(self, traversal_algorithm='inorder'):
        if traversal_algorithm == 'inorder':
            return self.inorder()
        if traversal_algorithm == 'preorder':
            return self.preorder()
        if traversal_algorithm == 'postorder':
            return self.postorder()
        if traversal_algorithm == 'breadth_first':
            return self.breadth_first()

    def _add_root(self, e):
        """如果二叉树为空则将元素e封装为结点后置为根结点并返回结点位置,否则抛出ValueError异常"""
        if self._root is not None:
            raise ValueError('此二叉树非空!')
        self._size = 1
        self._root = self._Node(e)
        return self._node2pos(self._root)

    def _add_left(self, p, e):
        """
        为位置p处的结点创建保存元素e的左子结点并返回结点位置,
        如果位置p处结点已有左子结点则抛出ValueError异常
        """
        node = self._pos2node(p)
        if node.left is not None:
            raise ValueError('位置', p, '处已存在左子结点!')
        self._size += 1
        node.left = self._Node(e, parent=node)  # 左子结点的父结点是node
        return self._node2pos(node.left)

    def _add_right(self, p, e):
        """
        为位置p处的结点创建保存元素e的右子结点并返回结点位置,
        如果位置p处结点已有右子结点则抛出ValueError异常
        """
        node = self._pos2node(p)
        if node.right is not None:
            raise ValueError('位置', p, '处已存在左子结点!')
        self._size += 1
        node.right = self._Node(e, parent=node)  # 右子结点的父结点是node
        return self._node2pos(node.right)

    def _replace(self, p, e):
        """替换位置p处结点的元素为e,并返回旧元素"""
        node = self._pos2node(p)
        old = node.element
        node.element = e
        return old

    def _delete(self, p):
        """
        删除位置p处结点并返回结点的对象元素,如果该结点有且仅有一个子结点则使用子结点代替其位置,
        如果该结点有两个子结点,则抛出异常
        """
        node = self._pos2node(p)
        if self.num_children(p) == 2:
            raise ValueError('位置', p, "处有两个子结点!")
        child = node.left if node.left else node.right  # child可能为None
        if child is not None:
            child.parent = node.parent  # 待删除结点的父结点成为其子结点的父结点
        if node is self._root:
            self._root = child  # 待删除的结点如为根结点则起子结点成为新的根结点
        else:
            parent = node.parent
            if node is parent.left:
                parent.left = child
            else:
                parent.right = child
        self._size += 1
        node.parent = None  # 识别已删除结点的约定
        node.element = None  # 识别已删除结点的约定
        return node.element

    def _attach(self, p, t1, t2):
        """将同类型的二叉树t1和t2在本树位置p的叶子结点处分别链接成左右子树"""
        node = self._pos2node(p)
        if not self.is_leaf(p):
            raise ValueError('位置p:', p, '处的结点必须为叶子结点!')
        if not (type(t1) is type(self) and type(t2) is type(self)):
            raise TypeError('t1:', t1, '和t2:', t2, '必须为', type(self))
        self._size += (len(t1) + len(t2))
        if not t1.is_empty():  # 将t1链接为位置p处结点的左子树
            t1._root.parent = node
            node.left = t1._root
            t1._root = None  # 链接后已不存在t1这个二叉树
            t1._size = 0
        if not t2.is_empty():  # 将t2链接为位置p处结点的右子树
            t2._root.parent = node
            node.right = t2._root
            t2._root = None  # 链接后已不存在t2这个二叉树
            t2._size = 0


if __name__ == '__main__':
    t = LinkedBinaryTree()  # 创建一个二叉树t
    p1 = t._add_root('A')  # 为二叉树t添加保存'A'的根结点

    t1 = LinkedBinaryTree()  # 创建一个二叉树t1
    p2 = t1._add_root('B')  # 为二叉树t1添加保存'B'的根结点

    t2 = LinkedBinaryTree()  # 创建一个二叉树t2
    p3 = t2._add_root('C')  # 为二叉树t2添加保存'C'的根结点

    p4 = t1._add_left(p2, 'D')
    p5 = t1._add_right(p2, 'E')

    p6 = t2._add_left(p3, 'F')
    p7 = t2._add_right(p3, 'G')

    t._attach(p1, t1, t2)  # 将二叉树t1和t2在t包含'A'的结点处进行链接,使得t1和t2分别为在保存'A'元素处结点的左右子树

    for each in t.positions(traversal_algorithm='preorder'):
        print(each.element(), end='\t')  # A	B	D	E	C	F	G
    print()

    for each in t.positions(traversal_algorithm='postorder'):
        print(each.element(), end='\t')  # D	E	B	F	G	C	A
    print()

    for each in t.positions(traversal_algorithm='inorder'):
        print(each.element(), end='\t')  # D	B	E	A	F	C	G
    print()

    for each in t.positions(traversal_algorithm='breadth_first'):
        print(each.element(), end='\t')  # A	B	C	D	E	F	G
    print()


转载:https://blog.csdn.net/weixin_37780776/article/details/108654555
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场