代码随想录——二叉树的遍历方式

二叉树这章分为6个小节:

  1. 二叉树的遍历方式
  2. 二叉树的属性
  3. 二叉树的修改与构造
  4. 求二叉搜索树的属性
  5. 二叉树公共祖先问题
  6. 二叉搜索树的修改与构造

本章节目录

一、二叉树理论基础

1、二叉树的种类

2、二叉树的存储方式

3、二叉树的遍历方式

4、二叉树的定义

二、二叉树的递归遍历

1)前序遍历:144. 二叉树的前序遍历 - 力扣(LeetCode)

2)中序遍历:94. 二叉树的中序遍历 - 力扣(LeetCode)

3)后续遍历:145. 二叉树的后序遍历 - 力扣(LeetCode)

三、二叉树的迭代遍历

1)前序遍历(迭代法)

2)中序遍历(迭代法)

3)后序遍历(迭代法)

四、二叉树的统一迭代法

1)统一法迭代实现中序遍历

2)统一法迭代实现前序遍历

3)统一法迭代实现后续遍历

五、二叉树的层序遍历


一、二叉树理论基础

1、二叉树的种类

在解题中,二叉树主要有2种形式:满二叉树和完全二叉树。

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

  • 完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。(若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点)

  • 二叉搜索树:是有数值的树,二叉搜索树是一个有序树。如果根节点的左右子树不为空,那么左子树上所有结点值均应小于其根节点值右子树上所有结点值均应大于其子根结点的值。(即它的左右子树也分别都是二叉排序树)

  • 平衡二叉搜索树:又被称为AVL树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

在c++中,map、set、multimap、multiset的底层容器都是平衡二叉搜索树,所以他们增删操作的时间复杂度是logn。而unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。

2、二叉树的存储方式

二叉树可以链式存储,也可以顺序存储。(链式存储用指针, 顺序存储用数组)。

  • 下左图就是链式存储,通过指针把分布在各个地址的节点串联一起。即地址在内存中不是连续的。(更好理解,所以一般都是用链式来存储二叉树
  • 下右图是顺序存储,用数组实现,即元素在内存是连续分布的。(遍历的时候,如果父节点的数组下标是i,那么它的左孩子下标就是2*i+1,右孩子结点下标是2*i+2,所以比较麻烦)

3、二叉树的遍历方式

主要是有两种遍历方式,深度优先和广度优先,拓展的话就有下面4种。

  • 深度优先遍历:先往深走,遇到叶子结点再往回走。按照中间节点的遍历顺序,分为前中后:
    • 前序遍历(递归法or迭代法):中左右
    • 中序遍历(递归法or迭代法):左中右
    • 后续遍历(递归法or迭代法):左右中
  • 广度优先遍历:一层一层去遍历。
    • 层次遍历(迭代法)

深度优先遍历一般用栈来通过递归实现,广度优先遍历一般都是用队列来实现的。

4、二叉树的定义

这里用链表来进行定义,二叉树的定义和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,分别指向左右孩子

一棵树由根节点、左孩子、右孩子构成。根节点是一个结点,是int类型,左右孩子是TreeNode类型,即泛指左右子树

public class TreeNode {
    int val;  // 根节点的值
    TreeNode left;  // 左子树
    TreeNode right;  // 右子树

    TreeNode() {} 
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {  // 分别有根节点,左孩子、右孩子
        this.val = val;
        this.left = left;  // 左右孩子这里并不是单纯指结点,也分别是一个TreeNode 
        this.right = right;  // 泛指左右子树
    }
}

二、二叉树的递归遍历

这里总结了写递归算法的三要素:

  1. 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,并且还要明确每次递归的返回值是什么。
  2. 确定终止条件:操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:会重复调用自己来实现递归的过程。

1)前序遍历:144. 二叉树的前序遍历 - 力扣(LeetCode)

比如按照前面的三步骤:

  1. 首先确定参数和返回值:目标是打印每个结点的值,所以参数是要传入当前结点以及记录存放节点数值的列表。也不需要什么返回值。
  2. 确定终止条件:当遍历到当前节点为空的时候,本层递归就结束了。
  3. 确定单层的遍历逻辑:比如前序是中左右的顺序,每层遍历就是先取中间节点数值,再根据指针找到左右结点的数值。
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) { //只用传入根节点
        List<Integer> result = new ArrayList<Integer>(); // 这个列表存放遍历的结果
        preorder(root, result); // 调用方法,传入根节点和结果列表,会返回工作列表
        return result; 
    }
    // 下面这是一个递归的方法(前序遍历)
    public void preorder(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        result.add(root.val);  // 把根节点的值存入result列表
        preorder(root.left, result);  // 然后递归调用preorder方法,分别取遍历左右子树
        preorder(root.right, result);  // 这是把左右子树的根节点当作新的根节点传入函数
    }
}

2)中序遍历:94. 二叉树的中序遍历 - 力扣(LeetCode)

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root, res);
        return res;
    }
    // 下面这是一个递归的方法(中序遍历)
    void inorder(TreeNode root, List<Integer> list) {
        if (root == null) {
            return;
        }
        inorder(root.left, list); //先遍历左结点,然后中间的根节点,然后右结点
        list.add(root.val);            
        inorder(root.right, list);
    }
}

3)后续遍历:145. 二叉树的后序遍历 - 力扣(LeetCode)

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postorder(root, res);
        return res;
    }
    // 下面这是一个递归的方法(中序遍历)
    void postorder(TreeNode root, List<Integer> list) {
        if (root == null) {
            return;
        }
        postorder(root.left, list);
        postorder(root.right, list);
        list.add(root.val);           // 左右中的遍历顺序
    }
}

三、二叉树的迭代遍历

同样针对上面的3道力扣题目,这里用迭代法来进行二叉树的前中后序遍历。其实就是用栈实现。

1)前序遍历(迭代法)

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。这样出栈才是先左后右(根节点是直接立马出栈的)

先把根节点压入栈。然后开启循环,先弹出栈顶元素,然后这个弹出元素的右、左结点先后压入栈。在下一轮循环中,会先弹出左结点,再把弹出的结点存入result。然后再把这个弹出的结点的右、左结点再压入栈,继续遍历。这样就实现了深度遍历,先把左子树遍历完,再遍历右子树。

// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();  // result 存放遍历后的结果
        if (root == null){
            return result;
        }
        Stack<TreeNode> stack = new Stack<>(); // 初始化一个栈
        stack.push(root); // 首先把根节点压入栈
        while (!stack.isEmpty()){  // 直到栈为空,就都遍历完了
            TreeNode node = stack.pop();  // 首先弹出栈顶元素(最开始就是根节点)
            result.add(node.val);  // 弹出后就把这个结点的值放入结果集
            if (node.right != null){  // 然后先把右结点压入栈
                stack.push(node.right);
            }
            if (node.left != null){  // 再把左结点压入栈
                stack.push(node.left);
            }
            // 这样下一轮循环的时候,就是先弹出的左节点
            // 然后再把左结点的右左结点分别压入栈,这样循环下去
        }
        return result;
    }
}

2)中序遍历(迭代法)

中序遍历是左中右,所以要遍历到左子树最下面的时候再把结点值存入result,所以比较麻烦些。

所以在迭代的时候,就要借助指针的遍历来帮助访问结点,然后还是用栈来处理节点上的元素。

遍历顺序是先遍历左子树,然后访问根节点,最后遍历右子树。

首先,将根节点入栈,然后不断将左子节点入栈,直到遇到一个空节点。此时,从栈中弹出一个节点,访问它,然后将它的右子节点入栈(如果右结点为空的话,就还会弹出再指向右节点),继续这个过程,直到栈为空。

比如上面这个例子,先把5、4、1压入栈,1的左结点为空了,然后就弹出1,记录1,接着访问1的右节点,因为右节点为空,所以再弹出栈顶元素4,记录4,访问4的右节点2,压入栈,发现2的左结点为空,就弹出2记录2.然后发现2的右节点也为空,就弹出5记录5 ,然后访问5的右节点6,把6压入栈,然后发现6的左结点为空,就把6弹出记录。最后结果就是14256.

// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null){
            return result;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;  // cur首先是根节点
        while (cur != null || !stack.isEmpty()){  // 也是循环到栈为空
           if (cur != null){
               stack.push(cur);  // 把cur压入栈
               cur = cur.left;  // 然后cur为左结点
           }else{  // 左结点的左结点这么遍历下去,直到当前节点为空
               cur = stack.pop(); // 就把栈顶元素弹出,
               result.add(cur.val); 
               cur = cur.right; // 然后指向右结点(如果右结点为空,会再执行else弹出,再弹出的就是子树的根节点了)
           }
        }
        return result;
    }
}

3)后序遍历(迭代法)

(类似先序遍历)后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了

// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null){
            return result;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            result.add(node.val);
            if (node.left != null){  // 类似先序遍历
                stack.push(node.left); // 但是这里入栈顺序是中左右,出栈是中右左
            }
            if (node.right != null){
                stack.push(node.right);
            }
        }
        Collections.reverse(result); // 翻转result,就是左右中
        return result;
    }
}

四、二叉树的统一迭代法

上面前中后序的迭代法代码风格其实不统一(不能像递归那样用一个套路),这里就统一下。方法就是把访问的节点放入栈中,把要处理的节点也放入栈中(紧接着放一个空指针作为标记)。

1)统一法迭代实现中序遍历

将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。

入栈顺序是右、中、左,然后在中节点入栈的时候接着入栈一个null,这样弹出顺序就是左中右

先压入5。然后执行循环,node=5,不为空,弹出5。然后把 6 5 null 4 依次压入栈。然后下一轮,node=4,不为空,把4弹出,然后把 2 4 null 1压入栈。此时栈中为 6 5 null 2 4 null 1。然后下一轮,node=1,不为空,就弹出1,然后把 1 null压入栈,此时栈中为 6 5 null 2 4 null  1 null。然后下一轮,node=null,就弹出栈顶这个null,标记node=1,然后再弹出、,记录这个弹出的1到result。下一轮,node=null,然后同样的执行else,弹出null再弹出4,记录4.然后再一轮,栈顶node=2,会执行if,把2弹出后,再压入2 null,此时栈中为 6 5 null 2 null。然后下一轮node就为null,弹出null和2,记录2.然后下一轮node还是为null,弹出null和5,记录5.再下一轮node为6,弹出再压入6 null。最后一轮node为null,弹出null和6,记录6.

class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<>();
    Stack<TreeNode> st = new Stack<>(); // 同样用一个栈实现
    if (root != null) st.push(root); // 先把根节点压入栈
    while (!st.empty()) { // 循环直到栈为空
        TreeNode node = st.peek();  // node为栈顶节点
        if (node != null) {
            st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
            if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
            st.push(node);                          // 添加中节点
            st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。

            if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
        } else { // 直到栈顶结点为空
            st.pop();           // 将空节点弹出
            node = st.peek();    // 重新取出栈中元素
            st.pop();
            result.add(node.val); // 加入到结果集
        }
    }
    return result;
}
}

2)统一法迭代实现前序遍历

入栈顺序就是右、左、中,然后还是在中节点后加一个null。其他不变。弹出顺序就是中左右

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
    }
}

3)统一法迭代实现后续遍历

入栈顺序就是中、右、左,然后还是在中节点后加一个null。其他不变,弹出顺序就是左右中

class Solution {
   public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> result = new LinkedList<>();
        Stack<TreeNode> st = new Stack<>();
        if (root != null) st.push(root);
        while (!st.empty()) {
            TreeNode node = st.peek();
            if (node != null) {
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                st.push(node);                          // 添加中节点
                st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
                if (node.right!=null) st.push(node.right);  // 添加右节点(空节点不入栈)
                if (node.left!=null) st.push(node.left);    // 添加左节点(空节点不入栈)         
                               
            } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.peek();    // 重新取出栈中元素
                st.pop();
                result.add(node.val); // 加入到结果集
            }
        }
        return result;
   }
}

五、二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣(LeetCode)

层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

主要看借助队列实现的方法:首先把根节点6放入que。

  1. 第1轮循环:len=1,把que中的结点6弹出并存入结果集,然后把6的左右结点4、6放入que
  2. 第2轮循环:len=2,内部循环2次,第一次先弹出4,存入13,第二次弹出7,存入58
  3. 第3轮循环:lem=4,内部循环4次,分别弹出1358.
  4. 注意每一轮循环都会新建一个列表,来存储当前层的值。
class Solution {
    public List<List<Integer>> resList = new ArrayList<List<Integer>>(); // 一个二维列表
    public List<List<Integer>> levelOrder(TreeNode root) {
        //checkFun01(root,0);  // 可以用递归的方式,也可以用队列来实现
        checkFun02(root);
        return resList;
    }

    //BFS--递归方式
    public void checkFun01(TreeNode node, Integer deep) {
        if (node == null) return;
        deep++; //deep记录层数,每次深入一层,深度 deep 增加。

        if (resList.size() < deep) { // 第一轮deep为1
            //如果resList的大小小于deep,则创建一个新的子列表并添加到 resList 中。
            List<Integer> item = new ArrayList<Integer>();
            resList.add(item);  //将当前节点的值添加到对应深度的子列表中。
        }
        resList.get(deep - 1).add(node.val);

        checkFun01(node.left, deep);  // 递归调用,分别对左子节点和右子节点进行遍历。
        checkFun01(node.right, deep);
    }

    //BFS--迭代方式--借助队列
    public void checkFun02(TreeNode node) {
        if (node == null) return;
        Queue<TreeNode> que = new LinkedList<TreeNode>();
        que.offer(node);  // 先把根节点加入队列

        while (!que.isEmpty()) {  // 循环直到队列为空
            // 在每次循环迭代中,创建一个新的列表 itemList,用于存储当前层的节点值。
            List<Integer> itemList = new ArrayList<Integer>();
            int len = que.size();  // 获取当前层的节点数量。初始que里面只有一个根节点,len=1

            while (len > 0) { 
                TreeNode tmpNode = que.poll(); //取出根节点,记录到itemList
                itemList.add(tmpNode.val);
                // 然后把根节点的左右结点分别加入到que中。
                if (tmpNode.left != null) que.offer(tmpNode.left);
                if (tmpNode.right != null) que.offer(tmpNode.right);
                len--; // 第一轮只循环1次(第二轮que中有2个结点,遍历2次,第三轮遍历4次)
            }

            resList.add(itemList);
        }

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值