代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历

系列文章目录

代码随想录——二叉树篇


文章目录

  • 系列文章目录
  • 二叉树的基础知识
    • 二叉树的种类
      • 满二叉树
      • 完全二叉树
      • 二叉搜索树
      • 平衡二叉搜索树
    • 二叉树的存储方式
    • 二叉树的遍历方式
    • 二叉树结点的写法
  • 递归遍历
  • 迭代遍历
    • 前序(迭代遍历)
    • 中序(迭代遍历)
    • 后序(迭代遍历)
    • 二叉树的统一迭代写法
  • 二叉树遍历方式总结
    • dfs
      • 递归方法
      • 迭代方法
    • bfs
    • 这些遍历方式的复杂度参考[二叉树遍历方式的复杂度](https://blog.csdn.net/qq_43152052/article/details/90111095)


二叉树的基础知识

二叉树的种类

在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。

满二叉树

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
深度为k的满二叉树,拥有2^k-1个节点

完全二叉树

完全二叉树的定义如下:
1.在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值;2.并且最下面一层的节点都集中在该层最左边的若干位置。
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第1张图片
之前我们刚刚讲过优先级队列其实是一个堆堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
(堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆.)

二叉搜索树

前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
左子结点.值 < 根节点.值 < 右子结点.值
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树
下面这两棵树都是搜索树
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第2张图片

平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过****1并且左右两个子树都是一棵平衡二叉树
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第3张图片
C++中****map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn
unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。

二叉树的存储方式

二叉树可以链式存储,也可以顺序存储。

那么链式存储方式就用指针, 顺序存储的方式就是用数组。

顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。

链式存储如图:
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第4张图片
链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?

其实就是用数组来存储二叉树,顺序存储的方式如图:
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第5张图片
用数组来存储二叉树如何遍历的呢?

如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。

所以大家要了解,用数组依然可以表示二叉树。

二叉树的遍历方式

二叉树主要有两种遍历方式:

深度优先遍历dfs:先往深走,遇到叶子节点再往回走。
广度优先遍历bfs:一层一层的去遍历。

那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:

深度优先遍历:
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)

广度优先遍历:
层次遍历(迭代法)

在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。

这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。

看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式

前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
大家可以对着如下图,看看自己理解的前后中序有没有问题。
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第6张图片

二叉树结点的写法

我们来看看链式存储的二叉树节点的定义方式。二叉树的定义 和 链表 是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。
二叉树的结点,有一个val, 有两个指针

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

递归遍历

递归怎么写:

  1. 确定参数
  2. 确定递归终点
  3. 确定单层递归的逻辑

前序

    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        def dfs(node):
            if not node: return
            res.append(node.val)
            dfs(node.left)
            dfs(node.right)

        dfs(root)
        return res

中序

    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        def dfs(node):
            if not node: return
            dfs(node.left)
            res.append(node.val)
            dfs(node.right)

        dfs(root)
        return res

后序

    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        def dfs(node):
            if not node: return
            dfs(node.left)
            dfs(node.right)
            res.append(node.val)
        dfs(root)
        return res

迭代遍历

前序(迭代遍历)

前序遍历是根左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
先把根节点加入栈内,只要栈不空,弹出栈顶元素,用curr记录一下,并把它的值加入Res数组内;接着判断一下右孩子是否存在,存在加入栈;判断左孩子是否在,存在加入栈,然后重复执行这个动作

def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root: return []
        res, stack = [], []
        stack.append(root)
        while len(stack):
            curr = stack.pop()
            res.append(curr.val)
            if curr.right:
                stack.append(curr.right)
            if curr.left:
                stack.append(curr.left)
        return res

中序(迭代遍历)

中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
代码随想录算法训练营第14天 | 二叉树理论基础 递归遍历 迭代遍历_第7张图片

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root: return []
        res, stack = [],[]
        curr = root
        while curr or len(stack):
            if curr != None:
                stack.append(curr)
                curr = curr.left
            else:
                curr = stack.pop()
                res.append(curr.val)
                curr = curr.right
        return res

后序(迭代遍历)

前序: 根左右 ——> 根右左——>左右根

代码逻辑,只要把左子结点和右子结点的入栈顺序调个个儿,res 数组存储的就是根右左的遍历顺序,然后把res数组颠倒输出就行了

def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root: return []
        res, stack = [], []
        stack.append(root)
        while len(stack):
            curr = stack.pop()
            res.append(curr.val)
            if curr.left: stack.append(curr.left)
            if curr.right: stack.append(curr.right)
        res = res[::-1]
        return res

二叉树的统一迭代写法

这个我实在没有余力短时间吸收掌握
参考链接二叉树统一迭代写法

二叉树遍历方式总结

dfs

迭代方法和递归方法遍历二叉树其实都是dfs, 经常笔试题常用的dfs其实经常使用递归去做

递归方法

前序,中序,后序

迭代方法

前序,后序,中序

bfs

在下一个博客

这些遍历方式的复杂度参考二叉树遍历方式的复杂度

你可能感兴趣的:(代码随想录算法训练营打卡,算法,深度优先,数据结构)