【数据结构与算法】最适合新手小白的教程——半步二叉树(包你看懂!)

hello大家好!还是标准的打招呼语哈哈哈。好久没更新博客了(国庆玩hai了),回来之后又忙着论文最后阶段的冲刺,还有竞赛的冲刺,还跑去搞了学生会的换届工作。这样一写才发现原来这段时间这么忙(bushi)。也是因为这些七七八八的事情,才把博客更新拖到现在QAQ

好了,咱们话不多说,直接进入今天正题——来种树(   )

1、二叉树概要

老规矩先上点概念哈。二叉树是一种树形数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树的特点使得它在计算机科学中有着广泛的应用,包括数据存储、排序、搜索等。

以下是一些专业名词:

  • 根节点(Root Node): 二叉树的最顶层节点,没有父节点。
  • 子节点(Child Node): 每个节点可以有左子节点和右子节点。
  • 叶子节点(Leaf Node): 没有任何子节点的节点。
  • 父节点(Parent Node): 有子节点的节点。
  • 深度(Depth): 从根节点到某个节点的路径长度。
  • 高度(Height): 从某个节点到叶子节点的最长路径长度。

常见的二叉树大致可以分为以下几类(这里就不给图了,概念上面都有实在想象不出来就去百度图片一下吧~):
 

(1)满二叉树:每个节点都有两个子节点,并且所有叶子节点都在同一层。

(2)完全二叉树:除了最后一层外,每一层的节点都是满的,且最后一层的叶子节点从左到右排           列。

(3)二叉搜索树(BST, Binary Search Tree): 对于每一个节点,左子树中的所有节点值小于该节点,右子树中的所有节点值大于该节点。

二叉树的常见操作有:

(1)遍历: 常见的遍历方式有前序遍历(Pre-order)、中序遍历(In-order)、后序遍历(Post-order)和层序遍历(Level-order)。

(2)插入: 在二叉搜索树中,可以根据节点值大小插入新的节点。

(3)删除: 删除二叉树中的节点时,需考虑节点是叶子节点、只有一个子节点或有两个子节点的情况。

(4)查找: 在二叉搜索树中,可以根据节点值进行高效的查找。

#二叉树内容还蛮多的,我会多写几篇,现在讲到的这些在后面都会再详细的提及

2、二叉树的节点结构

在二叉树的数据结构中,每个节点的结构通常包含以下三个部分:

  1. 数据域(data): 存储节点的值或数据。
  2. 左子节点指针(left): 指向当前节点的左子节点。如果没有左子节点,通常为 nullNone
  3. 右子节点指针(right): 指向当前节点的右子节点。如果没有右子节点,通常为 nullNone

代码实现如下:

class TreeNode:
    def __init__(self, data=0, left=None, right=None):
        self.data = data    # 节点的数据域
        self.left = left    # 左子节点指针
        self.right = right  # 右子节点指针

3、二叉树的前序、中序、后序遍历

前序遍历、中序遍历和后序遍历是二叉树遍历的三种经典方式,每种方式访问节点的顺序不同。它们分别是根据节点与其左右子节点的访问顺序来命名的。我会从递归和非递归(迭代)两种角度来分别说明他们的代码实现。(这里建议大家可以先去查一下栈这种结构,我前面还没有讲到过,后面的文章才会写)

(1) 前序遍历(Pre-order Traversal: 根-左-右)

  • 访问顺序: 先访问当前节点(根节点),再访问左子树,最后访问右子树。

  • 步骤:

    1. 访问根节点。
    2. 递归遍历左子树。
    3. 递归遍历右子树。
  • 示例:
    对于如下二叉树:

                              其遍历结果为1、2、4、5、3

递归方法

def preorder_recursive(root):
    if root:
        # 输出当前节点的数据(根)
        print(root.data, end=' ')
        # 递归遍历左子树
        preorder_recursive(root.left)
        # 递归遍历右子树
        preorder_recursive(root.right)

#递归的代码很简便,相信大家都能看得懂,我就不详细赘述了。其实你可以把他理解为一种命令语句,你要这个代码做什么就直接打什么,不用拐着绕弯直接写

#会先输出左侧数字是因为 preorder_recursive(root.left)这一行写在上面,代码是按顺序执行的

非递归方法

def preorder_iterative(root):
    if not root:
        return
    # 初始化栈,并将根节点入栈
    stack = [root]#创建一个名为 stack 的列表,这个列表将用作栈
                    #(后进先出,LIFO)的数据结构。用中括号 [ ] 创建一个列表,
                     #并将 root 节点作为列表的第一个元素。
                  #将 root 节点放入栈中,表示从根节点开始遍历。
    while stack:
        # 弹出栈顶节点并输出其数据(根)
        node = stack.pop()
        print(node.data, end=' ')
        # 先将右子节点入栈,因为栈是后进先出,这样能先访问左子节点
        if node.right:
            stack.append(node.right)
        # 再将左子节点入栈
        if node.left:
            stack.append(node.left)

#这个方法相较于递归就比较麻烦难看懂了。在前序遍历的过程中,使用栈的目的是模拟递归调用的行为,以便在不使用递归的情况下遍历二叉树。

#通过 stack,我们可以控制节点访问的顺序。每次从栈中弹出一个节点,访问该节点的数据,然后将它的子节点(右、左)依次压入栈中。由于栈是后进先出,先压入的右子节点会在左子节点之后被弹出,从而实现前序遍历的顺序(根-左-右)。

#stack.pop(): 移除并返回栈顶的节点。这里的栈顶元素是上一次压入栈的节点,也就是我们接下来要访问的节点。

#在二叉树的遍历过程中,我们会逐个访问每个节点,并输出它的 data。通过使用 end=' ',我们可以将每个节点的值输出在同一行,以空格分隔。

#还要注意入栈顺序,栈都是先进后出,所以我们应该先压入右,再压入左

2. 中序遍历(In-order Traversal: 左-根-右)

  • 访问顺序: 先访问左子树,然后访问当前节点(根节点),最后访问右子树。

  • 步骤:

    1. 递归遍历左子树。
    2. 访问根节点。
    3. 递归遍历右子树。
  • 示例:
    对于如下二叉树:

                            其遍历结果为:4、2、5、1、3

递归方法

def inorder_recursive(root):
    if root:
        # 递归遍历左子树
        inorder_recursive(root.left)
        # 输出当前节点的数据(根)
        print(root.data, end=' ')
        # 递归遍历右子树
        inorder_recursive(root.right)

#简单说一下逻辑,只要节点一直有左侧子节点,就会一直执行if下面的第一行,到4,没有左节点,那么执行第二行 print(root.data, end=' '),然后到第三行,也没有右侧节点,则返回root为2的时候,因为下去到4是执行第一行,此时返回则要执行第二行就是输出2,然后再到第三行代码去看右侧节点,以此类推

非递归方法

def inorder_iterative(root):
    stack = []  # 用于存放节点的栈
    current = root  # 当前处理的节点
    while current or stack:
        # 一直深入到当前子树的最左节点,将沿途节点压入栈
        while current:
            stack.append(current)
            current = current.left
        # 弹出栈顶元素(最左节点)并访问
        current = stack.pop()
        print(current.data, end=' ')
        # 转向右子树
        current = current.right

#代码上要注意,root就是我们设置好的二叉树节点,他的结构我都已经写在上面了。我用AI生成了一个代码执行过程,供大家参考。

详细执行过程

  1. 初始状态:

    • stack = [](初始化为空栈)。
    • current = 1(当前节点为根节点 1)。
    • 栈:[]
  2. 第1次循环:

    • 内层循环: 当前节点为 1,压入栈,并继续访问其左子节点 2
      • 栈:[1]
      • current = 2
    • 内层循环: 当前节点为 2,压入栈,并继续访问其左子节点 4
      • 栈:[1, 2]
      • current = 4
    • 内层循环: 当前节点为 4,压入栈,并继续访问其左子节点(None)。
      • 栈:[1, 2, 4]
      • current = None(因为 4 没有左子节点)
    • 弹出并访问: 栈顶元素 4 弹出并访问。
      • 输出:4
      • 栈:[1, 2]
      • current = None(因为 4 没有右子节点)
  3. 第2次循环:

    • 弹出并访问: 栈顶元素 2 弹出并访问(因为这次循环一开始current = None)。
      • 输出:4 2
      • 栈:[1]
      • current = 5(转向 2 的右子节点 5
  4. 第3次循环:

    • 内层循环: 当前节点为 5,压入栈,并继续访问其左子节点(None)。
      • 栈:[1, 5]
      • current = None(因为 5 没有左子节点)
    • 弹出并访问: 栈顶元素 5 弹出并访问。
      • 输出:4 2 5
      • 栈:[1]
      • current = None(因为 5 没有右子节点)
  5. 第4次循环:

    • 弹出并访问: 栈顶元素 1 弹出并访问。
      • 输出:4 2 5 1
      • 栈:[]
      • current = 3(转向 1 的右子节点 3
  6. 第5次循环:

    • 内层循环: 当前节点为 3,压入栈,并继续访问其左子节点(None)。
      • 栈:[3]
      • current = None(因为 3 没有左子节点)
    • 弹出并访问: 栈顶元素 3 弹出并访问。
      • 输出:4 2 5 1 3
      • 栈:[]
      • current = None(因为 3 没有右子节点)
  7. 结束循环: 栈为空,且 current 也为 None,遍历结束。

3. 后序遍历(Post-order Traversal: 左-右-根)

  • 访问顺序: 先访问左子树,再访问右子树,最后访问当前节点(根节点)。

  • 步骤:

    1. 递归遍历左子树。
    2. 递归遍历右子树。
    3. 访问根节点。
  • 示例:
    对于如下二叉树:

                              其遍历结果为:4、5、2、3、1

递归方法

def postorder_recursive(root):
    if root:
        # 递归遍历左子树
        postorder_recursive(root.left)
        # 递归遍历右子树
        postorder_recursive(root.right)
        # 输出当前节点的数据(根)
        print(root.data, end=' ')

#和前面的内容都大差不差,这里就不再赘述啦~

非递归方法

def postorder_iterative(root):
    if not root:
        return
    # 初始化栈和输出列表
    stack = []
    output = []
    stack.append(root)
    while stack:
        # 弹出栈顶节点,并将其数据存入输出列表(倒序处理)
        node = stack.pop()
        output.append(node.data)
        # 先将左子节点压入栈(因为在最终输出时它需要先处理,我们是倒序的)
        if node.left:
            stack.append(node.left)
        # 再将右子节点压入栈
        if node.right:
            stack.append(node.right)
    # 反转输出列表得到后序遍历顺序(因为我们在倒序存储)
    print(' '.join(map(str, output[::-1])))

#这里对底下的代码print(' '.join(map(str, output[::-1])))做出解释。这是对列表 output 进行反转操作。[::-1] 是 Python 切片的用法,它表示将 output 列表从尾到头反转。map(str, output[::-1]) 意思是对反转后的 output 列表中的每一个元素调用 str() 函数,把这些元素转换成字符串。join() 是一个字符串方法,它将列表中的元素连接成一个单独的字符串,并且在每两个元素之间插入指定的分隔符。在这里,' '.join(...) 表示用空格' ')来连接列表中的每个字符串元素。

#Python 没有专门的栈数据结构,但可以使用列表(list)来模拟栈的行为,也就是 stack = []。

4、打印二叉树

这个问题是很多大厂面试的的时候都会询问的问题,也有很多种答案,这里我提供一种主流代码,仅供大家参考

class Node:
    def __init__(self, value=0):
        self.value = value
        self.left = None
        self.right = None

def print_tree(head, height=0, to="H", length=10):
    """
    打印二叉树的函数
    :head: 当前处理的节点
    :height: 当前节点的层数 (根节点为 0)
    :to: 表示节点类型 (H 表示根节点, v 表示父节点在左上方, ^ 表示父节点在左下方)
    :length: 每个节点占用的打印宽度
    """
    if head is None:
        return

    # 先打印右子树
    print_tree(head.right, height + 1, "v", length)

    # 生成当前节点的打印内容
    val = to + str(head.value) + to
    lenV = len(val)
    lenL = (length - lenV) // 2  # 左边的空格数
    lenR = length - lenV - lenL  # 右边的空格数
    print(' ' * (length * height) + ' ' * lenL + val + ' ' * lenR)

    # 再打印左子树
    print_tree(head.left, height + 1, "^", length)

# 构建测试用的二叉树
if __name__ == "__main__":
    """
                 1
               2   3
             4    5 6
               7
    """
    head = Node(1)
    head.left = Node(2)
    head.right = Node(3)
    head.left.left = Node(4)
    head.right.left = Node(5)
    head.right.right = Node(6)
    head.left.left.right = Node(7)

    print_tree(head, 0, "H", 10)

#定义了一个简单的节点类 Node,包括 valueleftright 属性,用来表示节点的值和左右子节点。

#递归逻辑:

  • 递归地先打印右子树,并在打印时增加高度。
  • 生成当前节点的打印内容,计算左右空格,以控制节点的打印位置。
  • 打印当前节点的值,并使用空格来控制缩进。
  • 递归地打印左子树。

最后运行出来的图案会是这样的:

5、完成二叉树宽度优先遍历(求一颗二叉树的宽度)

这也是一个很经典的题目,要实现二叉树的宽度优先遍历(Breadth-First Traversal,通常也叫层序遍历),需要使用一个队列(Queue)来辅助存储当前层的节点。宽度优先遍历会逐层访问二叉树中的节点,从根节点开始依次访问每一层的所有节点。

宽度优先遍历的步骤

  1. 创建一个队列,将根节点加入队列。
  2. 循环执行以下步骤,直到队列为空
    • 从队列中取出最前面的节点,并访问它。
    • 如果该节点有左子节点,则将左子节点加入队列。
    • 如果该节点有右子节点,则将右子节点加入队列。

代码演示:

from collections import deque

class Node:
    def __init__(self, value=0):
        self.value = value
        self.left = None
        self.right = None

def breadth_first_traversal(root):
    if not root:
        return

    queue = deque([root])  # 初始化队列并将根节点加入队列

    while queue:
        node = queue.popleft()  # 从队列的前端取出节点
        print(node.value, end=' ')  # 访问节点的值

        # 如果左子节点存在,则将其加入队列
        if node.left:
            queue.append(node.left)

        # 如果右子节点存在,则将其加入队列
        if node.right:
            queue.append(node.right)

# 测试构建二叉树并进行遍历
if __name__ == "__main__":
    root = Node(1)
    root.left = Node(2)
    root.right = Node(3)
    root.left.left = Node(4)
    root.left.right = Node(5)
    root.right.right = Node(6)
    root.left.right.right = Node(7)

    print("Breadth-First Traversal:")
    breadth_first_traversal(root)

#定义了一个节点类 Node,包括 value(节点的值)、left(左子节点)和 right(右子节点)

#依次取出队列中的节点并访问它们,直到所有节点都被访问。使用 while 循环,当队列不为空时执行以下操作:如果该节点有右子节点,则将其加入队列。如果该节点有左子节点,则将其加入队列。从队列中取出一个节点 node,并打印它的值。使用 deque(双端队列)初始化队列,并将根节点加入队列。检查根节点是否为空,如果是空树,直接返回。

#测试构建二叉树并调用遍历方法:构建了一棵二叉树,并调用 breadth_first_traversal 进行遍历。

我们以一个具体的二叉树为例,演示如何进行宽度优先遍历(也称为层序遍历)。假设我们有以下二叉树:

我用AI给大家做了一个步骤说明,供大家参考

宽度优先遍历步骤说明

我们按照宽度优先遍历(层序遍历)的规则,逐层遍历并访问节点。需要一个队列(FIFO,先进先出)来保持当前层次的节点顺序。

初始化:
  • 队列:[1](初始化,根节点 1 放入队列)。
步骤1:访问根节点 1
  • 从队列中取出节点 1,并访问它。
  • 访问 1 后,将它的左右子节点 23 加入队列。
  • 输出:1
  • 队列:[2, 3]
步骤2:访问节点 2
  • 从队列中取出节点 2,并访问它。
  • 访问 2 后,将它的左右子节点 45 加入队列。
  • 输出:1 2
  • 队列:[3, 4, 5]
步骤3:访问节点 3
  • 从队列中取出节点 3,并访问它。
  • 访问 3 后,将它的右子节点 6 加入队列(3 没有左子节点)。
  • 输出:1 2 3
  • 队列:[4, 5, 6]
步骤4:访问节点 4
  • 从队列中取出节点 4,并访问它。
  • 访问 4 后,它没有子节点,因此不需要将任何节点加入队列。
  • 输出:1 2 3 4
  • 队列:[5, 6]
步骤5:访问节点 5
  • 从队列中取出节点 5,并访问它。
  • 访问 5 后,将它的右子节点 7 加入队列(5 没有左子节点)。
  • 输出:1 2 3 4 5
  • 队列:[6, 7]
步骤6:访问节点 6
  • 从队列中取出节点 6,并访问它。
  • 访问 6 后,它没有子节点,因此不需要将任何节点加入队列。
  • 输出:1 2 3 4 5 6
  • 队列:[7]
步骤7:访问节点 7
  • 从队列中取出节点 7,并访问它。
  • 访问 7 后,它没有子节点,因此不需要将任何节点加入队列。
  • 输出:1 2 3 4 5 6 7
  • 队列:[](队列为空,遍历结束)

最后输出结果为:1 2 3 4 5 6 7

6、一些小tip

终于是写到最后了,不得不说二叉树的内容真的多,后面还有一堆QAQ

但是这些内容都还蛮重要的,大家有空还是多敲敲,可以练练手感,多去leetcode观光。明年上半年还有比赛呢

结尾也不写太多了,下次可能更新机器学习的内容,正好论文写完了,做个整理工作。

现在已经是半夜了,老实睡觉去了,明天早八QAQ

祝大家有美好的一天,晚安~~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值