递归三部曲
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。二叉树的题一般是传入根节点和放遍历结果的数组,返回值一般是void因为直接放参数里
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
基础知识
一、二叉树的种类
1. 满二叉树:深度为k,有2^k-1个节点的二叉树
2. 完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
3. 二叉搜索树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
4. 平衡搜索二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
二、二叉树的存储方式
1. 链式存储(普遍):用链表指针,存节点元素、左指针、右指针
2. 顺序存储:用数组,如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2
三、二叉树的遍历方式
1. 深度优先(借助栈使用递归):遇到叶子节点再返回
- 前序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
2. 广度优先(队列):一层一层遍历
- 层序遍历
四、定义二叉树
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. 深度:二叉树里面任意一个节点到根节点的距离(包括它和根节点,路上有几个节点)。从上往下计数,将跟节点的深度给叶子节点,+1,所以用前序遍历,
2. 高度:二叉树里面任意一个节点到叶子节点的距离(包括它和叶子节点,路上有几个节点)。从下往上计数,将叶子节点的高度传给上面的节点,所以用后序遍历
- 根节点的高度就是二叉树的最大深度
题目一:二叉树的递归遍历
2. 前序遍历
(1)确定递归函数的参数和返回值:参数是cur和res数组,返回值是void
(2)确定终止条件:if(cur == null)return;
(3)确定单层递归的逻辑:根左右
3. 代码
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans=new ArrayList<>();
solution(root,ans);
return ans;
}
public void solution(TreeNode cur, List<Integer> ans){
if (cur==null)return;
ans.add(cur.val);
solution(cur.left,ans);
solution(cur.right,ans);
}
题目二:二叉树的迭代遍历(借助栈)
1. 前序(根左右):用一个栈和答案数组来实现,先放入根节点,然后弹出到数组中,再放入right、left并弹出left(和遍历顺序相反但是弹出顺序是对的),再遍历左子树
2. 前序遍历:根左右代码随想录 (programmercarl.com)
public static List<Integer> preorderTraversal(TreeNode root) {//迭代法的前序遍历,根左右
Stack<TreeNode> stack =new Stack<>();//操作栈
List<Integer> ans=new ArrayList<>();//答案ans
stack.push(root);//先插入根节点
while (!stack.isEmpty()){//边界条件是栈不为空
TreeNode temp=stack.peek();//最上面的元素给temp并且弹出
stack.pop();
if (temp!=null)ans.add(temp.val);//有可能是null
else continue;//是null的话直接进入下一次循环
stack.push(temp.right);//入栈顺序和遍历顺序相反
stack.push(temp.left);
}
return ans;
}
3. 后续遍历:是左右根,反过来是根右左,也就是用前序遍历的方法生成后续遍历,更改这三行
while (!stack.isEmpty()){//边界条件是栈不为空
stack.push(temp.left);//1.
stack.push(temp.right);//2.
}
return ans.reversed();//3.
题目三:层序遍历(借助队列)
1. 常规层序遍历
(1)创建ans和deque
(2)插入根节点
(3)while循环进行每层遍历
List<List<Integer>> ans=new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<>();//借助队列
if (root==null)return ans;//判断根节点
else deque.offer(root);//先插入根节点
while (!deque.isEmpty()){
int size=deque.size();//本层的节点数量
List<Integer> level=new ArrayList<>();//放本层节点的List
for (int i=0;i<size;i++){//本层节点的个数
TreeNode temp=deque.pop();//弹出最前面的节点
level.add(temp.val);
if (temp.left!=null)deque.offer(temp.left);//插入他的左孩子
if (temp.right!=null)deque.offer(temp.right);//插入他的右孩子
}
ans.add(level);//将一层的节点放入ans中
}
return ans;
2. 二叉树的层序遍历
107. 二叉树的层序遍历 II - 力扣(LeetCode):返回节点值是自底向上的顺序,return的时候调用reversed函数,其余和层序遍历一样
return ans.reversed();
3. 二叉树的右视图
199. 二叉树的右视图 - 力扣(LeetCode)while中判断是否为最后一个元素,是的话就加入ans中
public List<Integer> rightSideView(TreeNode root) {
List<Integer> ans=new ArrayList<>();
Deque<TreeNode> deque=new LinkedList<>();
if (root==null)return ans;
else deque.offer(root);
while (!deque.isEmpty()){
int size=deque.size();
for (int i=0;i<size;i++){
if (i==size-1)ans.add(deque.peek().val);
TreeNode temp=deque.pop();
if (temp.left!=null)deque.offer(temp.left);
if (temp.right!=null)deque.offer(temp.right);
}
}
return ans;
}
4. 二叉树的层平均值
637. 二叉树的层平均值 - 力扣(LeetCode):多添加double 类型的sum,每层sum更新
public static List<Double> averageOfLevels(TreeNode root) {
List<Double> ans=new ArrayList<>();//double类型
Deque<TreeNode> deque=new LinkedList<>();
if (root==null)return ans;
else deque.offer(root);
while (!deque.isEmpty()){
double sum=0.0;//设和
int size=deque.size();
for (int i=0;i<size;i++){
TreeNode temp=deque.pop();
sum+=temp.val;//更新sum
if (temp.left!=null)deque.offer(temp.left);
if (temp.right!=null)deque.offer(temp.right);
}
ans.add(sum/size);//将平均值添加到ans中
}
return ans;
}
5. N叉树层序遍历
429. N 叉树的层序遍历 - 力扣(LeetCode) :在遍历节点的N个孩子节点的时候,用for循环插入到deque中
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> ans=new ArrayList<>();
Deque<Node> deque=new LinkedList<>();
if (root==null)return ans;
else deque.offer(root);
while (!deque.isEmpty()){
int size=deque.size();
List<Integer> level=new ArrayList<>();
for (int i=0;i<size;i++){
Node temp=deque.pop();
level.add(temp.val);
//遍历孩子节点
List<Node> child=temp.children;
if (child.size()!=0){
for (Node ch:child){
deque.offer(ch);
}
}
}
ans.add(level);
}
return ans;
}
6. 在每行中找到最大值
515. 在每个树行中找最大值 - 力扣(LeetCode) :定义value存储最大值,先定义为Integer.MIN_VALUE
public List<Integer> largestValues(TreeNode root) {
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> deque=new LinkedList<>();
if (root==null)return ans;
else deque.offer(root);
while (!deque.isEmpty()){
int size=deque.size();
int value=Integer.MIN_VALUE;
for (int i=0;i<size;i++){
TreeNode temp=deque.pop();
value = Math.max(value,temp.val);
if (temp.left!=null)deque.offer(temp.left);
if(temp.right!=null)deque.offer(temp.right);
}
ans.add(value);
}
return ans;
}
7. 8. 填充每个节点的下一个右侧节点指针
116. 填充每个节点的下一个右侧节点指针 先将第一个拿出来当cur,其余的当next,进行递归
public Node connect(Node root) {
Deque<Node> deque=new LinkedList<>();
if (root!=null)deque.offer(root);
while (!deque.isEmpty()){
int size =deque.size();
//先拿出一个头节点当cur
Node cur=deque.pop();
if (cur.left!=null)deque.offer(cur.left);
if (cur.right!=null)deque.offer(cur.right);
for (int i=1;i<size;i++){
//再取剩余的节点当next
Node next=deque.pop();
if (next.left!=null)deque.offer(next.left);
if (next.right!=null)deque.offer(next.right);
//往后,cur给next
cur.next=next;
cur=next;
}
}
return root;
}
9. 二叉树的最大深度
1. 层序遍历法:while循环,每次循环完之后ans++,就是求有几行
public int maxDepth(TreeNode root) {
Deque<TreeNode> deque=new LinkedList<>();
if (root==null)return 0;
else deque.offer(root);
int ans=0;
while (!deque.isEmpty()){
int size=deque.size();
for (int i=0;i<size;i++) {
TreeNode temp = deque.pop();
if (temp.left != null) deque.offer(temp.left);
if (temp.right != null) deque.offer(temp.right);
}
ans++;
}
return ans;
}
2. 递归法:后序遍历,因为先求叶子节点的深度再传给根节点
- 返回值是int,传入的参数是Treenode root
- 终止条件:遍历到根节点时,高度就是0 if(node==null) return 0;
- 递归逻辑:左节点的高度,右节点的高度,取最大值+1
public static int maxDepth1(TreeNode root) {
if (root==null) return 0;
int left=maxDepth1(root.left);
int right=maxDepth1(root.right);
int middle=Math.max(left,right)+1;
return middle;
}
10. 二叉树的最小深度
1. 层序遍历:当左叶子节点和右椰子节点都为null的时候,输出ans
public int minDepth(TreeNode root) {
Deque<TreeNode> deque=new LinkedList<>();
if (root==null)return 0;
else deque.offer(root);
int ans=0;
while (!deque.isEmpty()){
int size=deque.size();
ans++;
for (int i=0;i<size;i++) {
TreeNode temp = deque.pop();
if (temp.left==null && temp.right==null)return ans;
if (temp.left != null) deque.offer(temp.left);
if (temp.right != null) deque.offer(temp.right);
}
}
return ans;
}
2. 递归法:后序遍历,因为先求叶子节点的深度再传给根节点
- 返回值是int,传入的参数是Treenode root
- 终止条件:遍历到叶子节点的假孩子节点时,高度就是0 if(node==null) return 0;
- 递归逻辑:左节点的高度,右节点的高度,取最大值+1
public int minDepth(TreeNode root) {
if (root==null)return 0;
int left=minDepth(root.left);
int right=minDepth(root.right);
//左为空取右,右为空取左
if (left==0) return right+1;
if (right==0)return left+1;
//左右都不为空,取小
return Math.min(left,right)+1;
}
题目四:翻转二叉树
1. 思路
(1)选择方法:递归(前序、中序、后序)和非递归
(2)递归三部曲(前序和后序都是可以的):
- 返回值是新的根节点,传入的参数是root
- 终止条件:碰到空节点的时候 if(root==null) return root
- 递归逻辑:前序是中左右,先swap(root.left, root.right)再invert(roo.left),invert(root.right)
(3)中序不可以:左中右
2. 代码
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
invertTree(root.left);
invertTree(root.right);
// 直接交换root的left和right指针
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
return root;
}
3. 不对的情况:只是操作了指向root.left和root.right的指针,没有实际交换。在 swap
方法内部对 left
和 right
参数的修改(即改变它们指向的对象)并不会影响到方法外部的原始引用。
public TreeNode invertTree(TreeNode root) {
if (root==null)return root;
swap(root.left,root.right);
invertTree(root.left);
invertTree(root.right);
return root;
}
//只是操作了指向root.left和root.right的指针,没有实际交换
public static void swap(TreeNode left,TreeNode right){
TreeNode temp=left;
left=right;
right=temp;
}
题目五:对称二叉树
1. 思路:因为要收集完左右子树的信息,返回给根节点,根节点才能进行判断,所以用后序遍历
(1)递归函数的参数是Treenode left ,Treenode right,返回值是Boolean型
(2)终止条件:
左空,右不空 | 左不空,右空 | 左空,右空 | 都不为空,值不相等 | |
结果 | false | false | ture | false |
(3)递归条件:
- 左Boolean outside = compare(left.left,right.right);
- 右boolean inside = compare(left.right,right.left);
- 中outside&&inside
2. 代码
public boolean isSymmetric(TreeNode root) {
return compare(root.left,root.right);
}
public boolean compare(TreeNode left,TreeNode right){
if (left ==null && right!=null)return false;
if (left!=null && right==null)return false;
if (left==null && right==null)return true;
if (left.val!=right.val)return false;
//此时只有左右的值都相等的情况
boolean outside= compare(left.left,right.right);
boolean inside=compare(left.right,right.left);
return outside&&inside;
}
题目六:完全二叉树的节点个数
1. 222. 完全二叉树的节点个数 - 力扣(LeetCode) 普通二叉树可以用层序遍历,或者递归
public int countNodes(TreeNode root) {
if(root==null)return 0;
int left=countNodes(root.left);
int right=countNodes(root.right);
int middle=left+right+1;
return middle;
}
题目七:平衡二叉树
1. 高度差不超过
public static boolean isBalanced(TreeNode root) {
if (compare1(root)==-1) return false;
else return true;
}
public static int compare1(TreeNode root){
if (root==null)return 0;
int left=compare1(root.left);
int right=compare1(root.right);
if (left==-1 || right==-1)return -1;
if (left-right>1||right-left>1)return -1;
else return Math.max(left,right)+1;
}
题目八:二叉树的所有路径
1. 257. 二叉树的所有路径 - 力扣(LeetCode)给定一个二叉树,返回所有从根节点到叶子节点的路径
2. 思路:前序遍历,这样输出的时候,才能让根节点指向孩子节点
- 递归的参数是根节点、传入临时数组记录单条路径、最终数组记录结果;返回的类型是List
- 终止条件:if(cur.left==null && cur.right==null) 将临时数组加入到最终数组中(在左右的代码中可以控制cur节点不为空)
- 单层处理逻辑:中左右
- 中:往临时数组中加入root的值,放在终止条件之前,这样叶子节点就不会被拉下
- 左、右:先判断该节点是否为空,不为空再递归,然后弹出元素,进行回溯
public static List<String> binaryTreePaths(TreeNode root) {
List<String> ans=new ArrayList<>();
if (root==null)return ans;
List<Integer> path=new ArrayList<>();
huisu(root,path,ans);
return ans;
}
public static void huisu(TreeNode root, List<Integer> path, List<String> ans){
//中
path.add(root.val);
//叶子节点
if (root.left ==null && root.right==null){
StringBuilder sb=new StringBuilder();
for (int i=0;i<path.size()-1;i++){//将答案拼接在一起
sb.append(path.get(i)).append("->");
}
sb.append(path.get(path.size()-1));
ans.add(sb.toString());//将答案加入ans中
return;//跳出此次循环
}
//左
if (root.left!=null){
huisu(root.left,path,ans);
path.remove(path.size()-1);
}
//右
if (root.right!=null){
huisu(root.right,path,ans);
path.remove(path.size()-1);
}
}
题目九:左叶子之和
1. 404. 左叶子之和 - 力扣(LeetCode) 孩子节点&是其根节点的左孩子
2. 思路:用后序,将叶子节点符合的值一层一层返回给上层。每个节点返回的值都是,它左子树和右子树的和
- 递归的参数是根节点;返回的类型是int
- 终止条件:
- 空节点if(root==null) return 0
- 叶子节点if(root.left==null && root.right==null) return0(当遍历到左叶子节点的父亲节点的时候,才会处理左叶子节点 )
- 单层处理逻辑:中左右
- 中:往临时数组中加入root的值,放在终止条件之前,这样叶子节点就不会被拉下
- 左、右:先判断该节点是否为空,不为空再递归,然后弹出元素,进行回溯
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
int leftValue = sumOfLeftLeaves(root.left); // 左
int rightValue = sumOfLeftLeaves(root.right); // 右
int midValue = 0;
if (root.left != null && root.left.left == null && root.left.right == null) {
midValue = root.left.val;
}
int sum = midValue + leftValue + rightValue; // 中
return sum;
}
题目十:找树左下角的值
1. 层序遍历更方便,记录每一行第一个值
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int res = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode poll = queue.poll();
if (i == 0) res = poll.val;
if (poll.left != null)queue.offer(poll.left);
if (poll.right != null) queue.offer(poll.right);
}
}
return res;
}
题目十一:路径总和
1. 题目:给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和
2. 思路
(1)因为没有对于中的处理,所以什么顺序都可以
- 递归的
- 参数是treenode和计数器count(初始化为target做--操作)
- 返回的类型是Boolean,如果找到一条合适的路径就一级一级返回true
- 终止条件:
- 叶子节点if(node.left==null && node.right==null && count==0)
- 叶子节点if(node.left==null && node.right==null && count!=0)
- 单层处理逻辑:中左右
- 中:往临时数组中加入root的值,放在终止条件之前,这样叶子节点就不会被拉下
- 左、右:先判断该节点是否为空,不为空再递归,然后弹出元素,进行回溯
public static boolean hasPathSum(TreeNode root, int targetSum) {
if (root==null)return false;
return huisu(root,targetSum-root.val);
}
public static boolean huisu(TreeNode node,int count){
if (node.right==null && node.left==null && count==0)return true;//边界条件
if (node.left==null && node.right==null)return false;
if (node.left!=null){
count-=node.left.val;
if(huisu(node.left,count))return true;
count+=node.left.val;
}
if (node.right!=null){
count-=node.right.val;
if(huisu(node.right,count))return true;
count+=node.right.val;
}
return false;
}
2. 路径总和二
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
recur(root, targetSum);
return res;
}
void recur(TreeNode root, int tar) {
if (root == null) return;
path.add(root.val);
tar -= root.val;
if (tar == 0 && root.left == null && root.right == null)
res.add(new LinkedList<Integer>(path));
recur(root.left, tar);
recur(root.right, tar);
path.removeLast();
}
}
题目十二:构造二叉树
1. 从中序和后序遍历二叉树
(1)递归函数的返回值是Treenode,参数是inorder和posorder
(2)如果postorder的size是0,则为空;如果是1就是叶子节点。节点元素是postorder的最后一个节点。
(3)单层递归的逻辑
找到中序数组中的切割点,用index来保存切割点的位置
切中序用index:切割完之后得到左中序、右中序数组
切后续:用中序数组里的数组大小
public TreeNode buildTree(int[] inorder, int[] postorder) {
if(postorder.length == 0 || inorder.length == 0)
return null;
return buildHelper(inorder, 0, inorder.length, postorder, 0, postorder.length);
}
private TreeNode buildHelper(int[] inorder, int inorderStart, int inorderEnd, int[] postorder, int postorderStart, int postorderEnd){
if(postorderStart == postorderEnd)
return null;
int rootVal = postorder[postorderEnd - 1];
TreeNode root = new TreeNode(rootVal);
int middleIndex;
for (middleIndex = inorderStart; middleIndex < inorderEnd; middleIndex++){
if(inorder[middleIndex] == rootVal)
break;
}
int leftInorderStart = inorderStart;
int leftInorderEnd = middleIndex;
int rightInorderStart = middleIndex + 1;
int rightInorderEnd = inorderEnd;
int leftPostorderStart = postorderStart;
int leftPostorderEnd = postorderStart + (middleIndex - inorderStart);
int rightPostorderStart = leftPostorderEnd;
int rightPostorderEnd = postorderEnd - 1;
root.left = buildHelper(inorder, leftInorderStart, leftInorderEnd, postorder, leftPostorderStart, leftPostorderEnd);
root.right = buildHelper(inorder, rightInorderStart, rightInorderEnd, postorder, rightPostorderStart, rightPostorderEnd);
return root;
}