7.2 二叉树的基本运算及遍历的python实现

这几节内容,概念少,全靠代码撑住场面。多看代码,才能学习到二叉树的基本知识和应用。

二叉树的基本运算

所谓的基本运算主要包括:括号表示法的树与二叉链存储结构的树互相转换,树节点查找,求树高度。

1. 树节点查找:

最基本的操作了:

class TreeNode:  # 树节点的定义
    def __init__(self, val):
        self.data = val
        self.left = None
        self.right = None

def findNode(node, val):
    if not node:
        return None
    
    if node.data == val:
        return node
    
    if node.left:
        res = findNode(node.left, val)
        if res:
            return res
    if node.right:
        res = findNode(node.right, val)
        if res:
            return res
    return None

2. 求树的高度

def getHeight(treeHead): 
    if not treeHead:
        return 0
    if not treeHead.left and not treeHead.right:
        return 1
    
    height, h = 0, 0
    if treeHead.left:
        h = getHeight(treeHead.left)
        height = max(h, height)
    if treeHead.right:
        h = getHeight(treeHead.right)
        height = max(h, height)
    return height + 1

3. 与括号表示法的互相转换

在教材中称为树的创建及输出,其实就是两种表示方法转换,二叉链结构的树是标准的存储结构,至于括号表示法,主要是由于方便输出(是一个字符串)。从这一基本操作可以领会二叉树各种运算的基本特点,难度还是不小的。

代码如下,二叉树的创建,应用了栈,不熟悉的回前面再看一下;二叉树的输出,应用递归方法。栈、队列、递归,是二叉树中运算的法宝。

tree_str = "A(B(E,T(W,C)),D(,F(P(S(I),V))))"  # 我们定义了一颗蛮复杂的树
print(tree_str)
def createTree(tree_str):
    if not tree_str:
        return None
    p = 0  # 当前节点
    t = 0  # 标记符,表示后面的数值是左子树还是右子树
    stack = []
    head = None

    for char in tree_str:  # 字符串中一共四类符号,分别进行处理
        # print(char)
        if char == "(":   # 遇左括号,当前节点入栈
            stack.append(p)
            t = 1
        elif char == ")":  # 遇右括号,当前节点出栈
            stack.pop(-1)
        elif char == ",":
            t = 2        
        else:  # 此时构建当前节点,并根据标记符处理
            p = TreeNode(char)
            if not head:
                head = p
            else:
                if t == 1:
                    stack[-1].left = p
                else:
                    stack[-1].right = p
    return head

h = createTree(tree_str)
print(h)

def disTree(head):  
    if not head:
        return ""
    tree_dis = ""
    tree_dis += head.data
    if head.left or head.right:
        tree_dis += "("
        tree_dis += disTree(head.left)  # 递归解析左子树
        if head.right:
            tree_dis += ","
        tree_dis += disTree(head.right)
        tree_dis += ")"
    return tree_dis

s = disTree(h)
print(s)

print(s==tree_str)  # 这两个值肯定是一样的

思考:用顺序存储法表示的树,上述问题如何解决?(通过顺序表示法节点标号的性质,十分简单)

二叉树的遍历

二叉树的遍历是指按照一定次序访问树中所有节点,并且每个节点仅被访问一次的过程。遍历是二叉树最基本的运算,是二叉树中其他运算的基础。参考文章 二叉树及其三种遍历

在遍历时根据访问根节点及遍历子树的先后关系,对于非空二叉树,我们有3种遍历方法:先序遍历(PreOrder)、中序遍历(InOrder)、后序遍历(PostOrder),根据访问根节点的次序划分,这3类遍历方法我们也称之为深度优先遍历(Depth First Search),因为遍历时沿着树的深度遍历树的节点,尽可能深的搜索树的分支。

此外,还有一种层序遍历(LevelOrder),我们称之为广度优先遍历(Breadth First Search),从根结点开始沿着树的宽度搜索遍历。

下面我们先看一下深度优先遍历的递归写法,只是调整访问根节点语句的顺序即可,十分简单:

def SearchTree(head):
    if not head:
        return None
    
    print(head.data)        # 先序
    SearchTree(head.left)
    # print(head.data)      # 中序
    SearchTree(head.right)
    # print(head.data)      # 后序

深度优先的非递归写法是本节需要掌握的重点,算法的核心就是使用栈(stack),可见之前线性结构的知识是重要基础。

三种遍历非递归方法的代码就没有递归的那么一致了,我们要分别进行解析(经典数据结构教材中有更详细的解析,不理解的认真学习教材):

1)先序遍历

先序遍历是最基础的深度优先的遍历方式,代码也是最简单的,访问根节点后,有右压右,有左压左。由于栈后进先出,右子树先进栈。应当注意 前面括号表示法如  "A(B(E,T(W,C)),D(,F(P(S(I),V))))" 中字母的顺序即为前序遍历的顺序,可见前序遍历是比较符合正常思维的表达方式。

def PreOrder(head):
    if not head:
        return None    
    stack = []
    stack.append(head)  # 根节点进栈
    while stack:        # 栈不空时循环
        p = stack.pop(-1)
        print(p.data)
        if p.right:     # 右结点先进栈
            stack.append(p.right)
        if p.left:      # 左结点进栈
            stack.append(p.left)

2)中序遍历

从一棵二叉树的最左下结点开始,根结点到左下结点一一进栈;出栈一个结点时访问他(每一个出栈的结点或者无左结点或者左结点已访问,此时才可访问),再处理右子树,即该结点的左结点依次进栈......

def InOrder(head):
    if not head:
        return None
    stack = []
    p = head
    while p or stack:   # 循环条件 
        while p:        # p的所有左结点进栈
            stack.append(p)
            p = p.left
        if stack:       # 栈顶元素 没有左结点或左结点已访问
            p = stack.pop(-1)
            print(p.data)   # 访问该结点
            p = p.right     # 访问后该结点的右子树进栈

3)后序遍历

不起眼的后序遍历是最复杂的遍历,与中序遍历相似的是,先访问左子树,从最左下的结点开始。由于我们要首先访问该结点的左右子树后才访问该结点,所以我们需要一个标记,表示左右子树是否访问过。此外,我们还需要一个变量pre保存刚刚访问过的结点,此时分三种情况 pre是当前的父节点,则此时在向下遍历;pre是当前的左子树,说明我们在从左到右遍历;最后就是其他情况,pre等于当前,表示上回合未将任何节点入栈,此次轮到它自身出栈,或是pre是当前的右节点,那么也轮到它自身出栈。

def postorderTraversal(root):
    if not root:
        return
    stack = [root]
    res = []
    pre_node = None
    while stack:
        p = stack[-1]
        if not pre_node or pre_node.left == p or pre_node.right == p:
            # 在向下遍历
            if p.left:
                stack.append(p.left)
            elif p.right:
                stack.append(p.right)
                    
        elif pre_node == p.left: # 已遍历左子树
            if p.right:
               stack.append(p.right)
        else: # leaf叶子节点时 pre==p / 左右都遍历的,轮到父节点了 pre==p.right
            res.append(p.val)
                stack.pop()
            pre_node = p
        return res

上述写法看起来很麻烦,网上看到了其他的写法,参考 二叉树先序、中序、后序、层次遍历的非递归实现(python),通过两个栈倒腾一下,实现了后序遍历,十分优秀。

实际上,后序遍历的顺序是左-右-根,在stack里我们将树的结点按根-右-左的顺序放入stack2内,然后stack2依次出栈访问结点内容,就形成了左-右-根的后序遍历方式。

def PostOrder2(root):
    stack = [root]
    stack2 = []
    result = []
    while not stack:
        root = stack.pop()
        stack2.append(root)
        if root.left:
            stack.append(root.left)
        if root.right:
            stack.append(root.right)
    while not stack:
        node = stack2.pop()
        result.append(node.data)
    return result

然后我们看一下层次遍历的方法:

二叉树的层次遍历即从上往下、从左至右依次打印树的节点。 先访问的结点其左、右子结点也要先访问,很符合队列的操作原则。其思路就是将二叉树的节点加入队列,出队时将其非空左右孩子依次入队,到队列为空即完成遍历。这是队列的重要应用。需要说明的是,教材上使用的是环形队列,充分利用空间;此外用栈也可解决。

一年前的博客 2.3 python数据结构之队列——应用,简单的两个队列的应用栗子,在python中队列可用deque模块,详细使用可参考 python3 deque(双向队列),主要函数是 append/appendleft, extend/extendleft, pop/popleft, index, insert, reverse, remove;当然,用list也可实现队列的功能,append入队,pop(0)出队,只是pop(0)的时间复杂度是O(n)的中看不中用。

下面我们看用队列实现的二叉树层次遍历:

from collections import deque
def levelOrder(head):
    if not head:
        return None
    queue = deque()
    queue.append(head)
    while queue:
        node = queue.popleft()
        print(node.data)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)

如何按层输出呢?很简单,按层遍历,下一层的结点归入另一个队列里即可,道理类似杨辉三角那道题。

def levelOrder(root: TreeNode) -> List[List[int]]:
        if not root:
            return []
        
        res = []
        queue = [root]
        
        while queue:
            next_level = []
            vals = []
            while queue:
                p = queue.pop(0)
                vals.append(p.val)
                if p.left:
                    next_level.append(p.left)
                if p.right:
                    next_level.append(p.right)
            res.append(vals)
            queue = next_level
        
        return res

 最后我们提出几个问题总结一下这一节:

我们在熟悉二叉树的基本运算特点之上,重点掌握二叉树的遍历方式,深度优先级及广度优先的遍历分别用于解决什么类型的问题? 在深度优先遍历中, 前序遍历、中序遍历及后序遍历分别提供了什么样的信息,分别解决什么问题?

同时,需要强调的一点是,二叉树的遍历,分为结果和过程两类,以后序遍历为例,想要后序遍历的结果,可以用方法二,如果是一道强调过程的题目,那么还是老老实实用方法一。

那么何为强调过程呢?遍历过程中除了包含结点的访问顺序,还有访问该结点时该结点栈中元素等“半路”信息,对于后序遍历,这种信息十分重要(后序遍历是二叉树路径问题的关键),要学会方法一;方法二中stack2只剩下后续遍历的结点的顺序了,不含有任何中间过程。

我们看下一节中更多二叉树的问题来寻找答案。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
二叉树的广度优先遍历又称为层次遍历,它按照从上到下、从左到右的顺序依次访问二叉树的所有节点。要实现二叉树的广度优先遍历,可以使用队列来辅助实现。 首先,我们可以定义一个队列,并将根节点入队。然后,进入循环,当队列非空时,执行以下操作: 1. 出队一个节点,并访问该节点。 2. 若该节点的左子节点不为空,将左子节点入队。 3. 若该节点的右子节点不为空,将右子节点入队。 具体的Python代码如下所示: ```python class TreeNode: def __init__(self, val): self.val = val self.left = None self.right = None def breadth_first_traversal(root): if root is None: return queue = [] queue.append(root) # 将根节点入队 while queue: node = queue.pop(0) # 出队一个节点 print(node.val) # 访问该节点 if node.left: # 若左子节点不为空,将左子节点入队 queue.append(node.left) if node.right: # 若右子节点不为空,将右子节点入队 queue.append(node.right) ``` 在以上代码中,我们首先定义了一个`TreeNode`类,表示二叉树的节点。然后,定义了`breadth_first_traversal`函数,用于实现广度优先遍历。该函数首先对根节点进行判空处理,并定义了一个队列`queue`,将根节点入队。接下来,使用循环遍历队列,每次出队一个节点,并访问该节点的值。然后,判断该节点的左子节点和右子节点是否为空,若不为空,则将它们依次入队。这样,就可以实现二叉树的广度优先遍历。 上述代码只是简单地输出了节点的值,如果需要将遍历结果存储起来,可以将节点的值存入一个列表中,或者进行其他相应的操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值