代码随想录训练营D14-二叉树篇 p1 | 理论基础、递归遍历、迭代遍历、统一迭代


文章中的正经图片(非手划拉的)均源自代码随想录文章。

(〇)前瞻

在这里插入图片描述

(一)理论基础

需要了解 二叉树的种类,存储方式,遍历方式 以及二叉树的定义

题目/文章/视频链接

1. 二叉树的种类

在这里插入图片描述

1.1 满二叉树

满二叉树,叶子结点都在最下面一层,并且除叶子结点外,都是度为2的结点。
高度为h,总结点数为n = 2^h - 1;h = log2(n + 1)

1.2 完全二叉树

在满二叉树的基础上,底层没有填满。但底层叶子结点一定从左到右连续的。

1.3 二叉搜索树

二叉搜索树是一个有序树
左孩子 < 父 < 右孩子
图源自代码随想录

1.4 平衡二叉搜索树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
在这里插入图片描述
最右一个不是平衡二叉树。对于值为10的根结点,左子树高度为2,右子树高度为0,差值== 2 > 1。

1.5 大顶堆、小顶堆

大根堆:完全二叉树基础上,根 >= 孩子
小根堆:完全二叉树基础上,根 <= 孩子
(对比二叉排序树BST:左<=根<=右)
在这里插入图片描述
在这里插入图片描述

1.6 编程语言中底层结构

C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。

java中的容器
在这里插入图片描述
PriorityQueue:大顶堆、小顶堆(完全二叉树)
TreeMap:底层红黑树
TreeSet:基于TreeMap,
hashset、hashmap:散列表

2. 存储方式

2种存储方式

1.链式存储
就是链表
在这里插入图片描述
2.顺序存储
在这里插入图片描述
顺序存储中,父结点与孩子结点下标的关系。如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

3. 二叉树的遍历方式

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

  • 深度优先遍历(先沿一条路走到黑,再转其他;前中后序都是指根结点的位置)
    • 前序遍历-根左右(递归法,迭代法)
    • 中序遍历-左根右(递归法,迭代法)
    • 后序遍历-左右根(递归法,迭代法)
  • 广度优先遍历(对于树就是一层一层;对于图,一圈一圈遍历)
    • 层次遍历(迭代法)

在这里插入图片描述

由于递归的底层就是通过栈完成的。所以前中后序中,可以手动通过栈来实现的,来完成非递归的代码。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。

4.二叉树的定义

public class TreeNode{
	int val;
	TreeNode left;
	TreeNode right;
	
	TreeNode(){}
	TreeNode(int val){
		this.val = val;
	}
	TreeNode(int val, TreeNode left,)
}

(二) 递归遍历 (必须掌握)

二叉树的三种递归遍历掌握其规律后,其实很简单

题目/文章/视频链接

1. 递归三步骤

以前序遍历为例:

1)确定递归函数的参数和返回值
由于要打印出前序结点遍历后的结果,所以参数中除了传入根节点外,还要传入一个集合list用于存放遍历后的结果。初次之外,再不需要数据,因此返回值是void

void traversal(TreeNode tree, List<Integer> list)		

2)确定终止条件
遍历到null空结点,自然本层递归要结束了。return 到上一层递归。

if(cur == null) return;	

3)确定单层递归的逻辑
这里是前序遍历,是根左右的顺序。即先存根结点的数值,再去递归左子树、右子树。

list.add(cur.val);
traversal(cur.left, list);
traversal(cur.right, list);		

2. (题目一)144. 二叉树的前序遍历

题目链接

2.1 代码

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        traversal(root, list);
        return list;

    }

    void traversal(TreeNode node, List<Integer> list){
        if(node == null){
            return;
        }
        //先序:根左右
        list.add(node.val);
        traversal(node.left, list);
        traversal(node.right, list);
    }
}

3. (题目二)145. 二叉树的后序遍历

题目链接

3.1 代码

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        traversal(root, list);
        return list;
    }
    
    void traversal (TreeNode node, List<Integer> list){
        if(node == null){
            return;
        }
        //后序:左右根
        traversal(node.left, list);
        traversal(node.right, list);
        list.add(node.val);
    }
}

4. (题目三)94. 二叉树的中序遍历

题目链接

4.1 代码

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        traversal(root, list);
        return list;
    }
    void traversal (TreeNode node, List<Integer> list){
        //中序遍历--左根右
        if(node == null){
            return;
        }
        traversal(node.left, list);
        list.add(node.val);
        traversal(node.right, list);
    }
}

(三) 迭代遍历 (基础不好的录友,迭代法可以放过)

迭代法,即非递归。由于编程语言的底层实现递归使用的是栈。所以理论上,在二叉树的相关题目中,能使用递归法解决的,也都能使用迭代法解决。

题目/文章/视频链接

关于二叉树结点的操作一共有两步:访问结点(如何遍历)、处理结点(将结点值存入数组)

1. (题目一)144. 二叉树的前序遍历

题目链接

1.1 思路

如何使用栈来模拟前序遍历的过程?
前序遍历 :根左右
在这里插入图片描述

在这里插入图片描述

首先根结点入栈。
开始循环,条件是栈不为空。
栈顶出栈,其数值填入result数组中;
如果出栈结点有右左孩子,那么出栈结点的右!左!孩子依次入栈(先入栈的右孩子,会在后面出栈;这样才符合前序遍历的中左右,右孩子后出)
直到循环结束,返回数组result

为什么能够通过出栈结点,找到其左右孩子呢?因为栈中存的数据可以是TreeNode类型的。

1.2 代码

public List<Integer> preorderTraversal1(TreeNode root) {
   List<Integer> result = new ArrayList<>();
    //判空
    if(root == null){
        return result;
    }

    Deque<TreeNode> stack = new LinkedList<>();
    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. (题目二)145. 二叉树的后序遍历

题目链接

2.1 思路

如何使用栈来模拟后序遍历的过程?
后序遍历 :左右根

先序遍历是:根左右(在代码中是根右左,因为栈先进后出的特性);
将先序中代码部分左右调换一下,此时整体求的是根右左(此时代码是根左右);
再将整体reverse一下,就是左右根。

2.2代码

public List<Integer> postorderTraversal1(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    //判空
    if(root == null){
        return result;
    }

    //创建栈用于迭代,并且循环前 根结点入栈
    Deque<TreeNode> stack = new ArrayDeque<>();
    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);
    return result;

}

3. (题目三)94. 二叉树的中序遍历

题目链接

3.1 思路

如何使用栈来模拟后序遍历的过程?
后序遍历 :左右根

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
1)处理:将元素放进result数组中
2)访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

在这里插入图片描述

中序遍历。左根右
所以先一路向左,一路压栈,直到左孩子为null
开始弹栈,现在弹出的当前结点,相当于刚刚为null的左孩子的父结点,所以针对这三口之家,现在要去看当前结点的右结点了。右结点又可以看做新的父结点,开始一路向左,直到左孩子为null。
总结:根结点不为null,就一路向左,并压栈;
直到左孩子结点为null,弹栈一个结点,并将方向转为当前的right(转完之后还是要继续一路向左)
综上,循环条件是栈不空 或 当前结点不是null

3.2代码

public List<Integer> inorderTraversal1(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    //判空
    if(root == null){
        return result;
    }

    Deque<TreeNode> stack = new ArrayDeque<>();
    while(!stack.isEmpty() || root != null){
        if(root != null){
            stack.push(root);
            root = root.left;
            continue;
        }
        root = stack.pop();
        result.add(root.val);
        root = root.right;
    }
    return result;

}

4. 实现过程中的问题

使用Deque作为栈时,使用的api应是:pop、push
作为队列时,使用的是add、poll

(四)统一迭代 (基础不好的录友,迭代法可以放过)

统一迭代 (基础不好的录友,迭代法可以放过)

题目/文章/视频链接

先跳过啦

(五) 今日收获,记录一下自己的学习时长

收获挺多,至少不得四五个小时?!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值