简介
树可以说是面试官最喜欢考的类型之一了,因为树作为基础数据结构的同时,实用性也很强,工作中树的数据结构也很常用,并且我们可以看到,树的大多数题难度也比较适中,大多是些中等难度的题,简直为了面试而生。当然后面还有面试官最爱的类型,即动态规划和回溯等。其实树的类型题,只要掌握两种模板方法,大部分的题都可以迎刃而解了,一种是递归法,一种是迭代法,因为树的题,大多都是在深搜或广搜的遍历过程中解决的。
理论基础
树是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由 n(n > 0) 个有限节点组成一个具有层次关系的集合。
把它叫做「树」是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。树有以下几个特点:
- 每个节点都只有有限个子节点或无子节点。
- 没有父节点的节点称为根节点。
- 每一个非根节点有且只有一个父节点。
- 除了根节点外,每个子节点可以分为多个不相交的子树。
- 树里面没有环路。
树的类型
接下来介绍几种常见的树类型。
- 无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树。
- 有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树。
- 二叉树:每个节点最多含有两个子树的树称为二叉树,二叉树可以链式存储,也可以顺序存储。
- 满二叉树:叶节点除外的所有节点均含有两个子树的树被称为满二叉树。
- 完全二叉树:除最后一层外,所有层都是满节点,且最后一层缺右边连续节点的二叉树称为完全二叉树。
- 哈夫曼树(最优二叉树):带权路径最短的二叉树称为哈夫曼树或最优二叉树。
- 二叉搜索树:是一颗有序二叉树。具有以下性质: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
- 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
遍历表达法
树主要有两种遍历方式,这两种遍历是图论中最基本的两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
遍历的表达法有4种方法:前序遍历、中序遍历、后序遍历、层次遍历。
例如下图:
前序遍历结果为:ABDECF(根-左-右)
中序遍历结果为:DBEAFC(左-根-右)(仅二叉树有中序遍历)
后序遍历结果为:DEBFCA(左-右-根)
层次遍历结果为:ABCDEF(同广度优先搜索)
那么遍历方式与表达法对应为:
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
做树的遍历时,深度优先遍历的递归法,当然是指用递归实现,迭代法则是用栈实现,因为我们知道,栈其实就是递归的一种是实现结构。而广度优先遍历的迭代法,则用是队列实现。
代码实现
树的代码实现
首先是树的代码实现,其实这就是leetcode上的树实现:
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;
}
}
大家会发现二叉树的代码和链表是差不多的,相对于链表,二叉树的节点里多了一个指针,有两个指针,指向左右孩子。刷leetcode题时,节点的定义默认都定义好了,但面试是不会给你定义的,要自己练到会。
深度优先遍历代码实现
首先我们先讲解用递归法写前中后序深搜代码,写所有的递归代码时都要注意以下两点:
- 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件:如果没有终止条件或终止条件不正确,那程序会一直进行,直到栈溢出。
前序遍历递归法
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);
preorder(root.left, result);
preorder(root.right, result);
}
}
中序遍历递归法
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);
}
}
后序遍历递归法
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); // 注意这一句
}
}
迭代法代码实现
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。接下来我们就用栈来实现迭代法代码。
前序遍历迭代法
/**
* 中-左-右,入栈顺序:中-右-左
*/
class Solution {
public List<Integer> preorderTraversal(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.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return 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;
}
}
统一迭代法
迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。那有没有统一的迭代方式呢?有的, 这种方法也可以叫做标记法。
前序遍历统一迭代法
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;
}
}
中序遍历统一迭代法
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();
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;
}
}
后序遍历统一迭代法
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;
}
}
广度优先遍历代码实现
上面的遍历的代码内容很多,但都是讲的深度优先搜索,接下来讲的是广度优先搜索,即层序遍历。队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出,则是为了模拟深度优先遍历也就是递归的逻辑。
层序遍历递归法
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
checkFun(root,0);
return resList;
}
//DFS--递归方式
public void checkFun(TreeNode node, Integer deep) {
if (node == null) return;
deep++;
if (resList.size() < deep) {
//当层级增加时,list的Item也增加,利用list的索引值进行层级界定
List<Integer> item = new ArrayList<Integer>();
resList.add(item);
}
resList.get(deep - 1).add(node.val);
checkFun(node.left, deep);
checkFun(node.right, deep);
}
}
层序遍历迭代法
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
checkFun(root);
return resList;
}
//BFS--迭代方式--借助队列
public void checkFun(TreeNode node) {
if (node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);
while (!que.isEmpty()) {
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();
while (len > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);
if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
len--;
}
resList.add(itemList);
}
}
}
解题心得
- 树是面试官最喜欢考的类型之一,二叉树又是前三百最多的树类型,要重点掌握。
- 二叉树可以链式存储,也可以顺序存储。顺序存储即用数组存储,用其特性,有时解题很巧妙。
- 树的题,大多都是在广搜或深搜的遍历过程中解决的,遍历通常又有两种方式即递归法和迭代法。
- 树的遍历是常考的内容,常见的优化方式为记忆化搜索和剪枝。
- 要熟练掌握前中后序遍历,前中后序都可用栈实现迭代法。
- 需要自行掌握树的代码定义,避免面试时手忙脚乱。
- 统一风格的迭代法并不好理解,但掌握后,可以更轻松的写出前中后序所有迭代法代码。
算法题目
94. 二叉树的中序遍历
题目解析:迭代法,用指针和栈,按左中右的顺序遍历该数组。其实前中后序都可用栈实现迭代法,后序可以由前序反转所得,中序可由指针判断和栈所得。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 迭代法
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return res;
}
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
// 判断有没有到最左子树
if (cur != null) {
stack.push(cur); // 左
cur = cur.left;
} else {
TreeNode temp = stack.pop();
res.add(temp.val); // 中
cur = temp.right; // 右
}
}
return res;
}
}
98. 验证二叉搜索树
题目解析:递归,需注意,当前值要大于所有左子树的所有值,小于右子树的所有值。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean helper(TreeNode node, long min, long max) {
if (node == null) {
return true;
}
if (node.val <= min || node.val >= max) {
return false;
}
return helper(node.left, min, node.val) && helper(node.right, node.val, max);
}
}
99. 恢复二叉搜索树
题目解析:中序遍历过程中,记录错误两个错误排序节点,最后进行交换,只需要中序遍历一遍就可以了。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 中序遍历
*/
class Solution {
TreeNode t1, t2, pre;
public void recoverTree(TreeNode root) {
helper(root);
int temp = t1.val;
t1.val = t2.val;
t2.val = temp;
}
public void helper(TreeNode root){
if (root == null) return ;
helper(root.left);
if (pre != null && pre.val > root.val) {
if (t1 == null) t1 = pre;
t2 = root;
}
pre = root;
helper(root.right);
}
}
100. 相同的树
题目解析:直接中序遍历两颗树,只要一个值不一样,即返回。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p==null && q==null){
return true;
}
if(p!=null && q!=null && p.val==q.val ){
return isSameTree(p.left,q.left) && isSameTree(p.right,q.right);
}else {
return false;
}
}
}
101. 对称二叉树
题目解析:递归解决,如果root为空,则为对称,为递归出口。否则比较,左子树左边树和右子树右边树 and 左子树右边树和右子树左边树 是否相等。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
// 为空直接返回
if (root == null) {
return true;
}
boolean res = helper(root.left, root.right);
return res;
}
public boolean helper(TreeNode leftTree, TreeNode rightTree) {
// 左右子树同时为空,则不对称
if (leftTree == null && rightTree == null) {
return true;
}
// 一支子树为空,另一支不为空,则不对称
if (leftTree == null || rightTree == null || leftTree.val != rightTree.val) {
return false;
}
// 分别判断:左子树左边树和右子树右边树 and 左子树右边树和右子树左边树 是否相等
boolean res = helper(leftTree.left, rightTree.right) && helper(leftTree.right, rightTree.left);
return res;
}
}
102. 二叉树的层序遍历
题目解析:广度优先,用递归把每层值都扫完即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
return helper(new ArrayList<>(),root,0);
}
List<List<Integer>> helper(List<List<Integer>> res,TreeNode root,int deep){
if(root!=null){
if(res.size()<=deep){
res.add(new ArrayList());
}
List<Integer> list = res.get(deep);
list.add(root.val);
helper(res,root.left,deep+1);
helper(res,root.right,deep+1);
}
return res;
}
}
103. 二叉树的锯齿形层序遍历
题目解析:广度优先,对应层需要判断一下奇偶,决定是头插还是尾插即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
helper(res, root, 0);
return res;
}
private void helper(List<List<Integer>> res, TreeNode root, int deep) {
if (root == null) {
return;
}
if (res.size() == deep) {
res.add(new ArrayList<Integer>());
}
if ((deep & 1) == 1){
res.get(deep).add(0, root.val);
} else {
res.get(deep).add(root.val);
}
helper(res, root.left, deep + 1);
helper(res, root.right, deep + 1);
}
}
104. 二叉树的最大深度
题目解析:直接一手递归,返回左右子树的高度,再做对比即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
}
105. 从前序与中序遍历序列构造二叉树
题目解析:先序遍历的第一个数为root,在中序中确定,中序左边为左子树,右边为右子树,再递归。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
private Map<Integer, Integer> indexMap;
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if(preorder_left > preorder_right)
return null;
//前序遍历的第一个节点为根节点
int preoder_root = preorder_left;
//在中序遍历中定位根节点
int inorder_root = indexMap.get(preorder[preoder_root]);
//先把根节点建立出来
TreeNode root = new TreeNode(preorder[preoder_root]);
//获取左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
//递归构造左子树,并连接到根节点
root.left = myBuildTree(preorder, inorder, preoder_root + 1, preoder_root + size_left_subtree, inorder_left, inorder_root - 1);
root.right = myBuildTree(preorder, inorder, preoder_root + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
indexMap = new HashMap<>();
//因为前序遍历和中序遍历序列长度一致所以只需一个HashMap
for(int i=0; i<n; i++)
indexMap.put(inorder[i], i);
return myBuildTree(preorder, inorder, 0, n-1, 0, n-1);
}
}
106. 从中序与后序遍历序列构造二叉树
题目解析:通过后序最后一位肯定根,然后在中序中分为左右两颗树,再后序中找到两颗树的后序排列方式,最后递归。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
int post_idx;
int[] postorder;
int[] inorder;
Map<Integer, Integer> idx_map = new HashMap<Integer, Integer>();
public TreeNode helper(int in_left, int in_right) {
if (in_left > in_right) {
return null;
}
int root_val = postorder[post_idx];
TreeNode root = new TreeNode(root_val);
int index = idx_map.get(root_val);
post_idx--;
root.right = helper(index + 1, in_right);
root.left = helper(in_left, index - 1);
return root;
}
public TreeNode buildTree(int[] inorder, int[] postorder) {
this.postorder = postorder;
this.inorder = inorder;
post_idx = postorder.length - 1;
int idx = 0;
for (Integer val : inorder) {
idx_map.put(val, idx++);
}
return helper(0, inorder.length - 1);
}
}
107. 二叉树的层序遍历 II
题目解析:广度优先搜索,用队列头插法存值。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 广度优先搜索
*/
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
LinkedList<List<Integer>> res = new LinkedList<>();
if (root == null)
return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
List<Integer> oneLevel = new ArrayList<>();
// 每次都取出一层的所有数据
int count = queue.size();
for (int i = 0; i < count; i++) {
TreeNode node = queue.poll();
oneLevel.add(node.val);
if (node.left != null)
queue.add(node.left);
if (node.right != null)
queue.add(node.right);
}
// 每次都往队头塞
res.addFirst(oneLevel);
}
return res;
}
}
108. 将有序数组转换为二叉搜索树
题目解析:该数组可看做树的中序排列,每次找中间数为根,左右两边为左右子树,当然只有中序排序,结果不唯一。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 左右等分建立左右子树,中间节点作为子树根节点,递归该过程
return nums == null ? null : buildTree(nums, 0, nums.length - 1);
}
private TreeNode buildTree(int[] nums, int l, int r) {
if (l > r) {
return null;
}
// 求中点不要用 int mid = (l + r)/2,有溢出风险
int m = l + (r - l) / 2;
TreeNode root = new TreeNode(nums[m]);
root.left = buildTree(nums, l, m - 1);
root.right = buildTree(nums, m + 1, r);
return root;
}
}
109. 有序链表转换二叉搜索树
题目解析:快慢指针,递归。慢指针每进一步,快指针进两步,找到中点即根。
代码如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public TreeNode sortedListToBST(ListNode head) {
// 快慢指针找到链表的中点,中点作为根结点,两边作为左右子树
if(head == null) return null;
if(head.next == null) return new TreeNode(head.val);
// 快慢指针找中间结点
ListNode fast = head, slow = head, pre = null;
while(fast != null && fast.next != null){
fast = fast.next.next;
pre = slow;
slow = slow.next;
}
// 分割出左链表,用于构造本结点的左子树
pre.next = null;
// 分割出右链表,用于构造本结点的右子树
ListNode rightList = slow.next;
// 用中间结点构造根结点
TreeNode root = new TreeNode(slow.val);
// 构造左子树
root.left = sortedListToBST(head);
// 构造右子树
root.right = sortedListToBST(rightList);
// 返回本结点所在子树
return root;
}
}
110. 平衡二叉树
题目解析:递归查找左右子树,然后判断左右子树的高度差,是否绝对值超过1。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public boolean isBalanced(TreeNode root) {
return getDepth(root) != -1;
}
// 返回二叉树的深度,如果该二叉树不是平衡二叉树则返回-1
public int getDepth(TreeNode root) {
if (root == null) return 0;
int leftHeight = getDepth(root.left);
int rightHeight = getDepth(root.right);
if (leftHeight == -1 || rightHeight == -1) return -1; // 左右子树出现了非平衡二叉树,直接返回
return Math.abs(leftHeight - rightHeight) > 1 ? -1 : Math.max(leftHeight, rightHeight) + 1;
}
}
111. 二叉树的最小深度
题目解析:做广度优先搜索,找到最小值即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
private boolean hasFound = false;
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
Queue<Integer> levels = new LinkedList<>();
queue.add(root);
levels.add(1);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
int level = levels.poll();
if (isLeaf(node)) {
return level;
}
if (node.left != null) {
if (isLeaf(node.left)) {
return level + 1;
}
queue.add(node.left);
levels.add(level + 1);
}
if (node.right != null) {
if (isLeaf(node.right)) {
return level + 1;
}
queue.add(node.right);
levels.add(level + 1);
}
}
return Integer.MAX_VALUE;
}
private boolean isLeaf(TreeNode node) {
return node.left == null && node.right == null;
}
}
112. 路径总和
题目解析:递归的同时,查找路径之和。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return targetSum - root.val == 0;
}
return hasPathSum(root.left, targetSum - root.val)
|| hasPathSum(root.right, targetSum - root.val);
}
}
113. 路径总和 II
题目解析:虽然是道树的题,但却是用经典回溯模板去解题。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 回溯
*/
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
List<Integer> cur = new ArrayList<>();
helper(root, cur, 0, sum);
return res;
}
public void helper(TreeNode node, List<Integer> cur, int sum, int target){
// 1
if(node == null){
return ;
}
if(node.left == null && node.right == null && node.val + sum == target){
cur.add(node.val);
res.add(new ArrayList<>(cur));
cur.remove(cur.size() - 1);
return ;
}
// 2
// 2.1
cur.add(node.val);
// 2.2
helper(node.left, cur, sum + node.val, target);
helper(node.right, cur, sum + node.val, target);
// 2.3
cur.remove(cur.size() - 1);
}
}
114. 二叉树展开为链表
题目解析:递归,从最左子树开始,将左树放在根的右树指向上,左树置空,原右树放在现右树的最右分树上。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public void flatten(TreeNode root) {
if(root == null) return;
flatten(root.left);
flatten(root.right);
TreeNode tmp = root.right;
root.right = root.left;
root.left = null;
while(root.right != null) root = root.right;
root.right = tmp;
}
}
116. 填充每个节点的下一个右侧节点指针
题目解析:递归,root的连接已做好,左右子树可能借来用,这样只用常量额外空间即可完成题目,root.left.next = root.right; root.right.next = root.next.left;。
代码如下:
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _left, Node _right, Node _next) {
val = _val;
left = _left;
right = _right;
next = _next;
}
};
*/
/**
* 递归
*/
class Solution {
public Node connect(Node root) {
if (root == null) {
return null;
}
if (root.left != null) {
// 连接左右子树
root.left.next = root.right;
if (root.next != null) {
// 右子树的next,可以通过root的连接找到
root.right.next = root.next.left;
}
}
connect(root.left);
connect(root.right);
return root;
}
}
117. 填充每个节点的下一个右侧节点指针 II
题目解析:遍历,把每一层存在的所有点,用虚拟头节点串起来即可。
代码如下:
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node next;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, Node _left, Node _right, Node _next) {
val = _val;
left = _left;
right = _right;
next = _next;
}
};
*/
/**
* 遍历
*/
class Solution {
public Node connect(Node root) {
if (root == null) {
return root;
}
Node cur = root;
while (cur != null) {
// 用一个头节点标注下一层的链表开始
Node head = new Node(0);
// 新建虚拟头节点
Node pre = head;
// 用当前层去更改下一层
while (cur != null) {
// 只要左、右子节点存在,则用头节点串起来
if (cur.left != null) {
head.next = cur.left;
head = head.next;
}
if (cur.right != null) {
head.next = cur.right;
head = head.next;
}
cur = cur.next;
}
// 移动到下一层,这里 pre.next(head.next) 为下一层第一个节点
cur = pre.next;
}
return root;
}
}
129. 求根节点到叶节点数字之和
题目解析:直接递归,深度优先搜索即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 深度优先搜索
*/
class Solution {
public int sumNumbers(TreeNode root) {
return helper(root, 0);
}
public int helper(TreeNode root, int temp) {
int res = 0;
if (root == null) {
return temp;
}
temp = temp * 10 + root.val;
// 如果左子树不为空,递归出左子树的值
if (root.left != null) {
res += helper(root.left, temp);
}
// 如果右子树不为空,递归出右子树的值
if (root.right != null) {
res += helper(root.right, temp);
}
// 如果左、右子树为空,返回本叶子节点值
if (root.left == null && root.right == null) {
res = temp;
}
return res;
}
}
140. 单词拆分 II
题目解析:用哈希表做记忆化搜索优化,避免重复的查找。
代码如下:
/**
* 记忆化搜索
*/
class Solution {
public List<String> wordBreak(String s, List<String> wordDict) {
Map<Integer, List<List<String>>> map = new HashMap<Integer, List<List<String>>>();
List<List<String>> wordBreaks = backtrack(s, s.length(), new HashSet<String>(wordDict), 0, map);
List<String> breakList = new LinkedList<String>();
for (List<String> wordBreak : wordBreaks) {
breakList.add(String.join(" ", wordBreak));
}
return breakList;
}
public List<List<String>> backtrack(String s, int length, Set<String> wordSet, int index, Map<Integer, List<List<String>>> map) {
if (!map.containsKey(index)) {
List<List<String>> wordBreaks = new LinkedList<List<String>>();
if (index == length) {
wordBreaks.add(new LinkedList<String>());
}
for (int i = index + 1; i <= length; i++) {
String word = s.substring(index, i);
if (wordSet.contains(word)) {
List<List<String>> nextWordBreaks = backtrack(s, length, wordSet, i, map);
for (List<String> nextWordBreak : nextWordBreaks) {
LinkedList<String> wordBreak = new LinkedList<String>(nextWordBreak);
wordBreak.offerFirst(word);
wordBreaks.add(wordBreak);
}
}
}
map.put(index, wordBreaks);
}
return map.get(index);
}
}
144. 二叉树的前序遍历
题目解析:迭代法,用栈来代替递归(递归本质上,也需要用栈存储局部变量值)把值按中右左的顺序依次存入栈,而后每次循环时依次弹出,为什么要先压入右子树值再压入左子树值呢,因为,先入栈的后出栈,这样可以先存右子树的值,前中后序都可用栈实现迭代法,后序可以由前序反转所得,中序可由指针判断和栈所得。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 迭代法
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return res;
}
// 首先把根节点压入栈
stack.push(root);
while (!stack.isEmpty()) {
TreeNode temp = stack.pop();
// 前序遍历,先把当前节点加入集合
res.add(temp.val); // 中
// 如果右节点不为空则把其加入栈
if(temp.right != null) stack.push(temp.right); // 右
// 如果左节点不为空则把其加入栈
if(temp.left != null) stack.push(temp.left); // 左
}
return res;
}
}
145. 二叉树的后序遍历
题目解析:迭代法,用栈来代替递归(递归本质上,也需要用栈存储局部变量值),把值按中左右的顺序依次存入栈。然后结果为中右左的"前序"遍历,再反转集合,即得到左右中的后序遍历集合。前中后序都可用栈实现迭代法,后序可以由前序反转所得,中序可由指针判断和栈所得。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 迭代法
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) {
return res;
}
// 首先把根节点压入栈
stack.push(root);
while (!stack.isEmpty()) {
TreeNode temp = stack.pop();
// 后序遍历,先压根节点(之后会反转)
res.add(temp.val);
// 如果左节点不为空则把其加入栈
if (temp.left != null) stack.push(temp.left); // 左 先压入左子树节点,这样后出栈,进而组成 中右左 组合
// 如果左节点不为空则把其加入栈
if (temp.right != null) stack.push(temp.right); // 右
}
Collections.reverse(res);
return res;
}
}
199. 二叉树的右视图
题目解析:按层遍历,每次添加最右边的元素,即为我们所需的元素。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 按层遍历
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int count = queue.size();
while(count > 0){
count--;
TreeNode cur = queue.poll();
if(count == 0){
res.add(cur.val);
}
if(cur.left != null){
queue.add(cur.left);
}
if(cur.right != null){
queue.add(cur.right);
}
}
}
return res;
}
}
212. 单词搜索 II
题目解析:这也是一道回溯模板题,之后会详细讲解该模板,这里有所了解即可。
代码如下:
/**
* 回溯 + 字典树
*/
class Solution {
int[][] dirs = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public List<String> findWords(char[][] board, String[] words) {
Trie trie = new Trie();
for (String word : words) {
trie.insert(word);
}
Set<String> ans = new HashSet<String>();
for (int i = 0; i < board.length; ++i) {
for (int j = 0; j < board[0].length; ++j) {
dfs(board, trie, i, j, ans);
}
}
return new ArrayList<String>(ans);
}
public void dfs(char[][] board, Trie now, int i1, int j1, Set<String> ans) {
if (!now.children.containsKey(board[i1][j1])) {
return;
}
char ch = board[i1][j1];
now = now.children.get(ch);
if (!"".equals(now.word)) {
ans.add(now.word);
}
board[i1][j1] = '#';
for (int[] dir : dirs) {
int i2 = i1 + dir[0], j2 = j1 + dir[1];
if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length) {
dfs(board, now, i2, j2, ans);
}
}
board[i1][j1] = ch;
}
}
class Trie {
String word;
Map<Character, Trie> children;
boolean isWord;
public Trie() {
this.word = "";
this.children = new HashMap<Character, Trie>();
}
public void insert(String word) {
Trie cur = this;
for (int i = 0; i < word.length(); ++i) {
char c = word.charAt(i);
if (!cur.children.containsKey(c)) {
cur.children.put(c, new Trie());
}
cur = cur.children.get(c);
}
cur.word = word;
}
}
222. 完全二叉树的节点个数
题目解析:针对完全二叉树的技巧解法,满二叉树的结点数为:2^depth - 1,每颗完全二叉树,分成左右子树,最终都可以分成小的满二叉树,然后小的二叉满树,再用公式计算。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 树
*/
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = getDepth(root.left);
int rightDepth = getDepth(root.right);
if (leftDepth == rightDepth) {
// 左子树是满二叉树
// 2^leftDepth其实是 (2^leftDepth - 1) + 1 ,左子树 + 根结点
return (1 << leftDepth) + countNodes(root.right);
} else {
// 右子树是满二叉树
return (1 << rightDepth) + countNodes(root.left);
}
}
private int getDepth(TreeNode root) {
int depth = 0;
while (root != null) {
root = root.left;
depth++;
}
return depth;
}
}
226. 翻转二叉树
题目解析:递归,递归从最底层交换到上层。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
return helper(root);
}
public TreeNode helper(TreeNode root) {
if (root == null) {
return root;
}
TreeNode temp = helper(root.left);
root.left = helper(root.right);
root.right = temp;
return root;
}
}
230. 二叉搜索树中第K小的元素
题目解析:需要自行建立二叉树数据结构,用于存储子树的结点数量。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 二叉树
*/
class Solution {
public int kthSmallest(TreeNode root, int k) {
MyBst bst = new MyBst(root);
return bst.kthSmallest(k);
}
}
class MyBst {
TreeNode root;
Map<TreeNode, Integer> nodeNum;
public MyBst(TreeNode root) {
this.root = root;
this.nodeNum = new HashMap<TreeNode, Integer>();
countNodeNum(root);
}
// 返回二叉搜索树中第k小的元素
public int kthSmallest(int k) {
TreeNode node = root;
while (node != null) {
int left = getNodeNum(node.left);
if (left < k - 1) {
node = node.right;
k -= left + 1;
} else if (left == k - 1) {
break;
} else {
node = node.left;
}
}
return node.val;
}
// 统计以node为根结点的子树的结点数
private int countNodeNum(TreeNode node) {
if (node == null) {
return 0;
}
nodeNum.put(node, 1 + countNodeNum(node.left) + countNodeNum(node.right));
return nodeNum.get(node);
}
// 获取以node为根结点的子树的结点数
private int getNodeNum(TreeNode node) {
return nodeNum.getOrDefault(node, 0);
}
}
235. 二叉搜索树的最近公共祖先
题目解析:根据二搜索树的特点,如果p.val < root.val < q.val,则 root 为p,q的最近公共祖先。
代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
/**
* 树
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val == p.val) {
return p;
}
if (root.val == q.val) {
return q;
}
// 如果p.val,q.val都小于root.val,则说明两值都在root的左子树中
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestor(root.left, p, q);
}
// 如果p.val,q.val都大于root.val,则说明两值都在root的右子树中
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestor(root.right, p, q);
}
return root;
}
}
236. 二叉树的最近公共祖先
题目解析:递归,root为空,p,q为root时 直接返回root,递归左右,都不为空说明一个在左,一个在右,此时返回root。此时谁不为空返回谁。
代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
/**
* 递归
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 找到叶子节点或找到其中一个节点,即可返回
if (root == null || p == root || q == root) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 节点在左右子树,返回当前根节点
if (left != null && right != null) return root;
// 返回最近的根节点
return left == null ? right : left;
}
}
257. 二叉树的所有路径
题目解析:一手递归,直接梭哈,再打印出路径即可。
代码如下:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
/**
* 递归
*/
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();
helper(res, root, "");
return res;
}
public void helper(List<String> res, TreeNode root, String p){
if(root != null){
StringBuilder str = new StringBuilder(p);
str.append(root.val);
if(root.left == null && root.right == null){
res.add(str.toString());
return;
}
else{
str.append("->");
String sb = str.toString();
helper(res,root.left,sb);
helper(res,root.right,sb);
}
}
}
}