目录
二叉树01
1.1 理论基础
1.1.1 二叉树的类型
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树的定义如下:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。
二叉搜索树:有数值的有序树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树。
1.1.2 二叉树的存储方式
链式存储方式就用指针, 顺序存储的方式就是用数组。顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
用数组来存储二叉树如何遍历的呢?如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
1.1.3 二叉树的遍历方式
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历:一层一层的去遍历。
- 层次遍历(迭代法)
深度优先遍历:递归、栈
广度优先遍历:迭代、队列
说到二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。
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;
this.right = right;
}
}
1.2 递归遍历
本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。
递归三要素:
①参数和返回值 ②终止条件 ③单层递归的逻辑
LC144:二叉树的前序遍历
题目链接:前序遍历
先审题,题干:给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
①参数就是根节点,返回值为整数数组
②终止条件:当前遍历的节点为null
③单层递归逻辑(前序遍历):中左右
所以代码:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>(); //返回前序遍历
preorder(root,result); //前序遍历,无返回值
return result;
}
public void preorder(TreeNode root,List<Integer> result){
if(root == null){
return;
}
//前序遍历:中左右
result.add(root.val);
preorder(root.left,result);
preorder(root.right,result);
}
}
同理
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>(); //返回后序遍历
postorder(root,result); //后序遍历,无返回值
return result;
}
public void postorder(TreeNode root,List<Integer> result){
if(root == null){
return;
}
//后序遍历:左右中
postorder(root.left,result);
postorder(root.right,result);
result.add(root.val);
}
}
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>(); //返回后序遍历
inorder(root,result); //后序遍历,无返回值
return result;
}
public void inorder(TreeNode root,List<Integer> result){
if(root == null){
return;
}
//后序遍历:左右中
inorder(root.left,result);
result.add(root.val);
inorder(root.right,result);
}
}
1.3 迭代遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。本题也是通过栈来实现迭代。
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。为什么要先加入右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码。
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){ //判断root是否为空
return result;
}
Stack<TreeNode> stack = new Stack<>(); //Stack存储的是节点TreeNode
stack.push(root); //新建并初始化Stack
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;
}
}
前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素(将元素放入result数组中)。
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;
while(cur != null || !stack.isEmpty()){
if(cur!=null){ //没到底端时只管推进
stack.push(cur);
cur = cur.left; //访问左节点
}else{ //到了底端时指针返回上一级(中),再访问右节点
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return 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); //然后翻转
return result;
}
}
1.4 统一迭代
我们发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
统一风格的迭代法并不好理解,而且想在面试直接写出来还有难度的。所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法。
这里省略