基本操作
- 创建值为v的节点
TreeNode* node = new TreeNode(v)
- 让S是T的左节点
T->left=S;
- 二叉树遍历框架,典型的非线性递归遍历结构:
/* 基本的二叉树节点 */
class TreeNode {
int val;
TreeNode left, right;
}
void traverse(TreeNode root) {
if(root==NULL){
return ;
}
// 先序
traverse(root.left);
// 中序
traverse(root.right);
// 后序
}
二叉树的思维模式
二叉树思维模式分为:
- 遍历思维模式:通过一次遍历函数+外部维护全局变量得到答案
- 分解思维模式:分解成子问题,定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案
遍历思维
404. 左叶子之和
给定二叉树的根节点 root ,返回所有左叶子之和。
这题一次遍历就可以得到结果
class Solution {
int res = 0;
public int sumOfLeftLeaves(TreeNode root) {
traversal(root);
return res;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
traversal(root.left);
traversal(root.right);
// 前中后无所谓
if (root.left!=null && root.left.left==null && root.left.right==null){
res+=root.left.val;
}
}
}
129. 求根节点到叶节点数字之和
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
遍历一遍就可以知道路径的数字,到叶子节点的时候相加
class Solution {
int sum = 0;
int path = 0;
public int sumNumbers(TreeNode root) {
traverse(root);
return sum;
}
private void traverse(TreeNode root){
if (root==null){
return ;
}
// 叶子节点
if (root.left==null && root.right==null){
path = path*10+root.val;
sum+=path;
path = path/10;
return ;
}
// 前序:进入节点
path = path*10+root.val;
traverse(root.left);
traverse(root.right);
// 退出节点
path = path/10;
}
}
226. 翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
遍历思维:遍历一遍,把每个节点的左右子树交换
分解思维:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
// 「遍历」的思路
class Solution {
// 主函数
public TreeNode invertTree(TreeNode root) {
// 遍历二叉树,交换每个节点的子节点
traverse(root);
return root;
}
// 二叉树遍历函数
void traverse(TreeNode root) {
if (root == null) {
return;
}
/**** 前序位置 ****/
// 每一个节点需要做的事就是交换它的左右子节点
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
// 遍历框架,去遍历左右子树的节点
traverse(root.left);
traverse(root.right);
}
}
// 「分解问题」的思路
class Solution2 {
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
// 利用函数定义,先翻转左右子树
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
// 然后交换左右子节点
root.left = right;
root.right = left;
// 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
return root;
}
}
404.左叶子之和
给定二叉树的根节点 root ,返回所有左叶子之和。
遍历一遍树,找到左叶子节点累加起来节点值
class Solution {
int res = 0;
public int sumOfLeftLeaves(TreeNode root) {
traversal(root);
return res;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
traversal(root.left);
traversal(root.right);
// 前中后无所谓
if (root.left!=null && root.left.left==null && root.left.right==null){
res+=root.left.val;
}
}
}
后序
后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
652. 寻找重复的子树
给你一棵二叉树的根节点 root ,返回所有 重复的子树 。
对于同一类的重复子树,你只需要返回其中任意 一棵 的根结点即可。
如果两棵树具有 相同的结构 和 相同的结点值 ,则认为二者是 重复 的。
找是否重复,首先要知道自己长什么样,再知道别人长什么样,对比。
- 知道自己长什么样
知道左子树+右子树+自己 就知道自己长什么样了,我们再后序遍历的位置通过拼接字符串的方式把二叉树序列化。 - 知道别人长什么样
通过hashmap将每个树的样子存起来
class Solution {
// 记录所有子树
HashMap<String,Integer> subTrees = new HashMap<>();
// 记录重复的子树根节点
LinkedList<TreeNode> res = new LinkedList<>();
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
traverse(root);
return res;
}
private String traverse(TreeNode root){
if (root==null){
return "#";
}
String left = traverse(root.left);
String right = traverse(root.right);
// 知道自己长什么样
String myself = String.join(",",left,right,root.val+"");
// 知道别人长什么样,看看重复不重复
int count = subTrees.getOrDefault(myself,0);
if (count==1){
res.addLast(root);
}
subTrees.put(myself, count+1);
return myself;
}
}
243. 二叉树的最大直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
二叉树的最大直径等于左右两树的最大深度之和
所以我们可以定义一个求二叉树最大深度的函数,在求最大深度的过程中,后序遍历的位置已经知道了左右子树的最大深度,更新最长直径即可。
class Solution {
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return res;
}
/**
* 求子树的最大深度
* @param root
* @return
*/
public int maxDepth(TreeNode root){
if (root==null){
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
// 最长直径=左右子树的最大深度之和
res = Math.max(leftDepth+rightDepth, res);
return Math.max(leftDepth,rightDepth)+1;
}
}
分解思维
100. 相同的树
判断两个树是否一样
分解思维: 判断左右子树是否一样
public boolean isSame(TreeNode root, TreeNode subRoot){
if (root==null && subRoot==null){
return true;
}
if (root==null || subRoot==null){
return false;
}
if (root.left==null && subRoot.left!=null){
return false;
}
if (root.right==null && subRoot.right!=null){
return false;
}
return isSame(root.left, subRoot.left) && isSame(root.right, subRoot.right) && root.val==subRoot.val;
}
101.对称二叉树
判断二叉树是否轴对称
对称是左右两边相等。我们可以递归遍历left树和right树,那这两个树对称的条件是 当前节点的值相等 && left左子树和right右子树是轴对称 && left右子树和right左子树是轴对称
class Solution {
public boolean isSymmetric(TreeNode root) {
return dfs(root,root);
}
private boolean dfs(TreeNode root1, TreeNode root2){
if (root1==null && root2==null){
return true;
}
if (root1==null || root2==null){
return false;
}
return root1.val==root2.val && dfs(root1.left,root2.right) && dfs(root1.right,root2.left);
}
}
572. 另一棵树的子树
给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。
递归遍历root,看root是否和subRoot相等,不相等的话就看左子树或右子树有没有相等的
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root==null){
return subRoot==null;
}
// 判断当前root与subRoot是否相同
if (isSame(root, subRoot)){
return true;
}
// 判断左右子树中是否有
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
1367.二叉树中的链表
给你一棵以 root 为根的二叉树和一个 head 为第一个节点的链表。
如果在二叉树中,存在一条一直向下的路径,且每个点的数值恰好一一对应以 head 为首的链表中每个节点的值,那么请你返回 True ,否则返回 False 。
一直向下的路径的意思是:从树中某个节点开始,一直连续向下的路径。
思路和上题找相同子树一样, 只不过判断相同换成了链表
class Solution {
public boolean isSubPath(ListNode head, TreeNode root) {
// 说明找完了
if (head==null){
return true;
}
if (root==null){
return false;
}
// 当找到一个二叉树节点的值等于链表头结点时
if (head.val==root.val){
// 判断其余是否相同
if (check(head, root)){
return true;
}
}
// 在左右子树找
return isSubPath(head, root.left) || isSubPath(head, root.right);
}
private boolean check(ListNode head, TreeNode root){
if (head==null){
return true;
}
if (root==null){
return false;
}
if (head.val==root.val){
return check(head.next, root.left) ||check(head.next, root.right);
}
return false;
}
}
114 题「将二叉树展开为链表」
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
此题可以用分解做, 已经把左右子树拉平后,我们要做的操作:
- 将左子树作为右子树
- 将原先的右子树接到当前右子树的末端
class Solution {
public void flatten(TreeNode root) {
if (root==null){
return ;
}
// 把左右子树拉平
flatten(root.left);
flatten(root.right);
TreeNode rightNode = root.right;
TreeNode leftNode = root.left;
// 将左子树作为右子树
root.left = null;
root.right = leftNode;
// 接上原来的右子树
TreeNode p = root;
while (p.right!=null){
p = p.right;
}
p.right=rightNode;
}
}
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
注意递归条件:
- 为叶子节点返回1
- 左子树或右子树为空要返回大的那个
- 左右子树都不为空才取最小值
class Solution {
public int minDepth(TreeNode root) {
if (root==null){
return 0;
}
// 叶子节点
if (root.left==null && root.right==null){
return 1;
}
int m1 = minDepth(root.left);
int m2 = minDepth(root.right);
// 左子树或右子树有一个为空 返回大的那个
if (root.left==null || root.right==null){
return m1+m2+1;
}
return Math.min(m1, m2)+1;
}
}
二叉树构造
二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。
中序+先序/ 后序可以唯一确定一颗二叉树:
- 根节点从先序遍历或后序遍历找,下一次的索引都要依赖左子树的叶子节点数:
- 先序遍历:左子树的先序范围:[preStart+1,preStart+leftSize]
右子树的先序范围:[preStart+leftSize+1, preEnd] - 后序遍历:左子树的先序范围:[postStart,postStart+leftSize-1]
右子树的先序范围:[postStart+leftSize, postEnd-1]
- 先序遍历:左子树的先序范围:[preStart+1,preStart+leftSize]
- 左右子树从中序遍历划分,base case 也是中序的 inStart>inEnd 就返回null
先序+后序不能唯一确定一颗二叉树:
- 左子树的leftSize需要根据 前序遍历的第二个元素 在后续遍历的index来确定
- pre左子树:[preStart+1,preStart+leftSize]、pre右子树:[preStart+leftSize+1,preEnd]
post左子树:[postStart,index] post 右子树[index+1, postEnd-1]
654. 最大二叉树
给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
创建一个根节点,其值为 nums 中的最大值。
递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
找到当前数组的最大值就是根节点,数组的左半部分就是root.left, 右半部分就是root.right
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums,0,nums.length-1);
}
private TreeNode build(int[] nums, int start, int end){
// base case
if (start>end){
return null;
}
// 找到当前数组中的最大值
int max = -1;
int index = -1;
for (int i = start; i <=end; i++) {
if (nums[i]>max){
max = nums[i];
index = i;
}
}
// 根节点
TreeNode root = new TreeNode(max);
// 找左子树
root.left = build(nums, start, index-1);
// 找右子树
root.right = build(nums, index+1, end);
return root;
}
}
105. 从前序与中序遍历序列构造二叉树
给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
示例 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
示例 2:
Input: preorder = [-1], inorder = [-1]
Output: [-1]
我们可以从前序遍历知道第一个肯定是根节点,然后通过这个根节点去中序遍历中找就可以得到以当前根节点的左子树和右子树,然后继续递归即可。
我们这里先把中序遍历的结果放到hashmap中 这样就可以方便查找
这道题难的是确定先序遍历的索引:这个需要通过左子树的节点数推导
左子树的先序范围:[preStart+1,preStart+leftSize]
右子树的先序范围:[preStart+leftSize+1, preEnd]
class Solution {
Map<Integer/*值*/,Integer/*索引*/> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i<inorder.length;i++){
map.put(inorder[i],i);
}
return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
}
private TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd){
if (preStart>preEnd){
return null;
}
// 根节点 为先序遍历的第一个值
TreeNode root = new TreeNode(preorder[preStart]);
// 在中序遍历中找到根节点,左半部分就是左子树,右半部分就是右子树
int index = map.get(preorder[preStart]);
// 左子树的个数
int leftSize = index-inStart;
root.left = build(preorder, preStart+1, preStart+leftSize, inorder, inStart, index-1);
root.right = build(preorder, preStart+leftSize+1, preEnd, inorder, index+1, inEnd);
return root;
}
}
106. 从中序遍历与后序遍历序列构造二叉树
和105 的区别就是 找根节点从先序变成了后序
左子树的先序范围:[postStart,postStart+leftSize-1]
右子树的先序范围:[postStart+leftSize, postEnd-1]
class Solution {
HashMap<Integer,Integer> map = new HashMap<>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
for (int i=0;i<inorder.length;i++){
map.put(inorder[i],i);
}
return build(inorder,0,inorder.length-1, postorder, 0, postorder.length-1);
}
private TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorde, int postStart, int postEnd){
if (inStart>inEnd){
return null;
}
// 找根节点
int rootVal = postorde[postEnd];
TreeNode root = new TreeNode(rootVal);
// 根节点在中序遍历的索引
int index = map.get(rootVal);
int leftSize = index-inStart;
// 左子树
root.left = build(inorder, inStart, index-1, postorde, postStart, postStart+leftSize-1);
// 右子树
root.right = build(inorder, index+1,inEnd,postorde,postStart+leftSize, postEnd-1);
return root;
}
}
889.通过后序和前序遍历结果构造二叉树
后序和前序不能唯一确定一颗树,此题只需要返回其中的答案即可
1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。
2、然后把前序遍历结果的第二个元素作为左子树的根节点的值。
3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。
pre左子树:[preStart+1,preStart+leftSize]、pre右子树:[preStart+leftSize+1,preEnd]
post左子树:[postStart,index] post 右子树[index+1, postEnd-1]
class Solution {
Map<Integer,Integer> map = new HashMap<>();
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
for (int i=0;i<postorder.length;i++){
map.put(postorder[i],i);
}
return build(preorder, 0, preorder.length-1, postorder, 0, postorder.length-1);
}
private TreeNode build(int[] preorder, int preStart, int preEnd, int[] postorder, int postStart, int postEnd){
if (preStart>preEnd){
return null;
}
if (preStart==preEnd){
return new TreeNode(preorder[preStart]);
}
// 根节点
int rootVal = preorder[preStart];
TreeNode root = new TreeNode(rootVal);
// 先序遍历根节点的下一个节点 是左子树的根节点
int leftRootVal = preorder[preStart+1];
int index = map.get(leftRootVal);
// 左子树长度
int leftSize = index-postStart+1;
// 左子树
root.left = build(preorder, preStart+1, preStart+leftSize, postorder, postStart, index);
// 右子树
root.right = build(preorder, preStart+leftSize+1, preEnd, postorder, index+1, postEnd-1);
return root;
}
}
题型
路径
- 从根节点到叶节点的路径
- 用maxDepth计算最大深度
- 在【叶子节点】进行判断和选择
- 任意节点间的路径:基本都是定一个新的函数oneSideMax算出一边的最值,在后序遍历的位置更新全局变量
- 首先要定义出新的oneSide函数计算什么:我们知道了左右子树返回的什么值 就能求出要的那个res
- 124.二叉树中的最大路径和:用oneSideMax 计算「单边」最大路径和
- 687.最长同值路径:定义从 root 开始值为 parentVal 的最长树枝长度,这样以root为根节点的最长同值路径就是左右子树加起来
- 在【后序遍历的位置】 更新全局变量:进行将左右子树的结果相加+root.val,因为后序的位置已经知道当前root左右子树的遍历结果
- 首先要定义出新的oneSide函数计算什么:我们知道了左右子树返回的什么值 就能求出要的那个res
- 如果要保存路径:
- 需要通过全局变量path来记录,遍历过程中 在前序遍历的位置path加上节点,在后序遍历位置path减去节点
根节点和叶子节点之间
257. 二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
所有路径可以通过一次遍历得到,遍历到叶子节点的时候生成路径
【用path记录当前遍历到的节点,用res记录结果】
进入节点的时候添加到path,出节点的时候从path中移除,到叶子节点的时候path就是 一条从根节点到叶子节点的路径,添加到res
class Solution {
// 递归时的路径
LinkedList<String> path = new LinkedList<>();
// 结果
LinkedList<String> res = new LinkedList<>();
public List<String> binaryTreePaths(TreeNode root) {
traverse(root);
return res;
}
private void traverse(TreeNode root){
if (root==null){
return ;
}
// 前序遍历位置 节点进入
path.addLast(root.val+"");
// 叶子节点
if (root.left==null && root.right==null){
// 将根节点到叶子节点的路径添加进去
res.addLast(String.join("->",path));
}
// 遍历左右子树
traverse(root.left);
traverse(root.right);
// 后序遍历位置 节点移除
path.removeLast();
}
}
112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
分解成左子树或右子树是否有满足targetSum-root.val,如果是根节点且值为targetSum则返回true
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root==null){
return false;
}
if (root.left==null && root.right==null && root.val==targetSum){
return true;
}
return hasPathSum(root.left, targetSum-root.val) || hasPathSum(root.right, targetSum-root.val);
}
}
113. 路径总和2
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
和257. 二叉树的所有路径的区别是 加了个target,所以只用在叶子节点判断下当前路径和是否等于target
class Solution {
List<Integer> path = new LinkedList<>();
List<List<Integer>> res = new LinkedList<>();
int target;
int sum = 0;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
target = targetSum;
traversal(root);
return res;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
path.addLast(root.val);
sum+=root.val;
// 是叶子节点
if (root.left==null && root.right==null){
if (target==sum){
List<Integer> curRes = new LinkedList<>(path);
res.addLast(curRes);
}
}
traversal(root.left);
traversal(root.right);
path.removeLast();
sum-=root.val;
}
}
任意节点之间
124. 二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
最大路径和 = 左子树单边路径和+右子树单边路径和+root.val
class Solution {
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
if (root==null){
return 0;
}
oneSideMax(root);
return res;
}
/**
* 计算单边最大路径和
* @param root
* @return
*/
private int oneSideMax(TreeNode root){
if (root==null){
return 0;
}
int leftMaxSum = Math.max(0, oneSideMax(root.left));
int rightMaxSum = Math.max(0, oneSideMax(root.right));
// 记录最大路径和
res = Math.max(res, leftMaxSum+rightMaxSum+root.val);
return Math.max(leftMaxSum,rightMaxSum)+root.val;
}
}
543. 二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
二叉树任意节点的直径 = 左子树的最大深度+右子树的最大深度
所以我们只用定义求单边最大深度的函数即可
class Solution {
int res = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return res;
}
/**
* 求子树的最大深度
* @param root
* @return
*/
public int maxDepth(TreeNode root){
if (root==null){
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
// 最长直径=左右子树的最大深度之和
res = Math.max(leftDepth+rightDepth, res);
return Math.max(leftDepth,rightDepth)+1;
}
}
687. 最长同值路径
给定一个二叉树的 root ,返回 最长的路径的长度 ,这个路径中的 每个节点具有相同值 。 这条路径可以经过也可以不经过根节点。
两个节点之间的路径长度 由它们之间的边数表示。
定义函数:以root为根和parentVal值相等的最大长度
这样在后序遍历位置更新全局变量时直接将左右子树结果相加即可
class Solution {
int res = 0;
public int longestUnivaluePath(TreeNode root) {
if (root==null){
return 0;
}
maxLen(root,root.val);
return res;
}
// 以root为根节点的二叉树中,从 root 开始值为 parentVal 的最长树枝长度
private int maxLen(TreeNode root, int parentVal){
if (root==null){
return 0;
}
int leftMax = maxLen(root.left, root.val);
int rightMax = maxLen(root.right, root.val);
//
res = Math.max(leftMax+rightMax,res);
// root值和parentVal值不一样那就是0
if (root.val!=parentVal){
return 0;
}
// 左右最大 加上自己本身
return Math.max(leftMax,rightMax)+1;
}
}
437. 路径总和III
前缀和的思想
class Solution {
public int pathSum(TreeNode root, int targetSum) {
// 记录当前遍历到的前缀和,对应的个数
Map<Long, Integer> prefix = new HashMap<Long, Integer>();
prefix.put(0L, 1);
return dfs(root, prefix, 0, targetSum);
}
public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) {
if (root == null) {
return 0;
}
int ret = 0;
curr += root.val;
// 访问到当前路径和,put
ret = prefix.getOrDefault(curr - targetSum, 0);
prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);
ret += dfs(root.left, prefix, curr, targetSum);
ret += dfs(root.right, prefix, curr, targetSum);
// 访问结束,撤离
prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);
return ret;
}
}
二叉搜索树
二叉搜索树(BST)的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。
二叉搜索树的中序遍历结果是有序的。
- 二叉搜索树的遍历
根据二叉树搜索树的中序遍历结果是有序的,可以再中序遍历的位置进行修改。void traverse(TreeNode root) { if (root == null) return; traverse(root.left); // 中序遍历代码位置 print(root.val); traverse(root.right); }
- 二叉搜索树中第k小的元素:就是在中序遍历的位置累加count 当count==k就找到了
- 把二叉搜素树转化为累加树:就是把左右子树遍历顺序反过来(就是降序遍历),当前节点值就是之前遍历的sum
- 基本操作:增删改
增删都需要接收递归的返回值TreeNode deleteNode(TreeNode root, int key) { if (root.val == key) { // 找到啦,进行删除 } else if (root.val > key) { // 去左子树找 root.left = deleteNode(root.left, key); } else if (root.val < key) { // 去右子树找 root.right = deleteNode(root.right, key); } return root; }
- 插入:根据BST的特性找到应该插的位置(递归到root为null也就是到了叶子节点的孩子)
- 恢复二叉搜索树:找到两个错误的节点,就是在中序遍历的位置看哪个prev 比root大,找到后进行交换
- 构造
遍历
二叉搜索树中第k小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
因为知道二叉搜索树的中序遍历结果是有序的,所以只要遍历一遍在中序的位置记录一下到第几个节点了,如果是k说明找到了
class Solution {
int count = 0;
int kV ;
int res;
public int kthSmallest(TreeNode root, int k) {
kV = k;
traversal(root);
return res;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
traversal(root.left);
// 中序的顺序就是从小到大的
count++;
if (count==kV){
res = root.val;
return ;
}
traversal(root.right);
}
}
利用「BST 中序遍历就是升序排序结果」这个性质,每次寻找第 k 小的元素都要中序遍历一次,最坏的时间复杂度是 O(N),N 是 BST 的节点个数。这个效率不高
优化思路:
比如说你让我查找排名为 k 的元素,当前节点知道自己排名第 m,那么我可以比较 m 和 k 的大小:
1、如果 m == k,显然就是找到了第 k 个元素,返回当前节点就行了。
2、如果 k < m,那说明排名第 k 的元素在左子树,所以可以去左子树搜索第 k 个元素。
3、如果 k > m,那说明排名第 k 的元素在右子树,所以可以去右子树搜索第 k - m - 1 个元素。
节点需要知道自己的排名就需要在二叉树节点中维护额外信息。每个节点需要记录,以自己为根的这棵二叉树有多少个节点。
有了 size 字段,外加 BST 节点左小右大的性质,对于每个节点 node 就可以通过 node.left 推导出 node 的排名,从而做到我们刚才说到的对数级算法。
538. 把二叉搜素树转化为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
也就是把大于当前数的节点都找到加起来,那我们只要知道降序打印节点即可。二叉搜索树的中序遍历结果是升序的,左右子树反过来遍历就得到降序遍历了
class Solution {
int sum=0;
public TreeNode convertBST(TreeNode root) {
traversal(root);
return root;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
traversal(root.right);
// 降序遍历
sum+=root.val;
// 将 BST 转化成累加树
root.val = sum;
traversal(root.left);
}
}
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
这个题一下子就想这用分解思想,用当前节点和左右节点比,再递归比左右子树,但是不正确!!BST 的每个节点应该要小于右边子树的所有节点, 不能光和他的左节点比,所以我们要记录左边的最小值和右边的最大值
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root,null,null);
}
/* 限定以 root 为根的子树节点必须满足 max.val > root.val > min.val */
private boolean isValidBST(TreeNode root, TreeNode min, TreeNode max){
if (root==null){
return true;
}
// 根节点应该比最小值大
if (min!=null && root.val<=min.val){
return false;
}
// 根节点应该比最大值小
if (max!=null && root.val>=max.val){
return false;
}
return isValidBST(root.left, min, root) && isValidBST(root.right, root, max);
}
}
700. 二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
利用BST左小右大的特性
class Solution {
TreeNode res;
public TreeNode searchBST(TreeNode root, int val) {
if (root==null){
return null;
}
if (root.val==val){
// 找到了根
res = root;
return res;
}else if(root.val<val){
return searchBST(root.right, val);
}else{
return searchBST(root.left,val);
}
}
}
增删改
701. 在二叉搜索树中的插入操作
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root==null) {
//插入
return new TreeNode(val);
}
if (root.val>val){
// 插到左边
root.left = insertIntoBST(root.left, val);
}
if (root.val<val){
// 应该插到右边
root.right = insertIntoBST(root.right, val);
}
return root;
}
}
99.恢复二叉搜索树
给你二叉搜索树的根节点 root ,该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树 。
- 找到错误的两个节点: 左子树比root大
- 交换
class Solution {
// 需要被交换的两个节点
TreeNode first = null;
TreeNode second = null;
TreeNode prev = new TreeNode(Integer.MIN_VALUE);
public void recoverTree(TreeNode root) {
// 1、找到错误的两个节点: 左子树比root大
traversal(root);
// 2、交换
int tmp = first.val;
first.val = second.val;
second.val = tmp;
}
private void traversal(TreeNode root){
if (root==null){
return ;
}
traversal(root.left);
// 中序遍历结果是升序的
if (prev.val>root.val){
if (first==null){
first = prev;
}
second = root;
}
prev = root;
traversal(root.right);
}
}
450. 删除二叉搜索树中的节点
- 找到需要删除的节点
TreeNode deleteNode(TreeNode root, int key) { if (root.val == key) { // 找到啦,进行删除 } else if (root.val > key) { // 去左子树找 root.left = deleteNode(root.left, key); } else if (root.val < key) { // 去右子树找 root.right = deleteNode(root.right, key); } return root;
}
```
2.删除这个节点
删除分几种情况:
- A 恰好是末端节点,两个子节点都为空,直接删除
- A 只有一个非空子节点,那么它要让这个孩子接替自己的位置。
- A有两个子节点,那么需要找到右子树中最小的节点或左子树中最大的节点接替自己
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root==null){
return null;
}
// 1、 找到要删除的节点
if (root.val==key){
// 2、删除
// 2.1 是叶子节点 直接删
if (root.left==null && root.right==null){
return null;
}
// 2.2 只有左节点 或只有右节点
if (root.left==null){
return root.right;
}
if (root.right==null){
return root.left;
}
// 2.3 左右节点都有,找到右子树最小的节点后替换当前节点
// 找到右子树最小的节点
TreeNode minNode = fingRightMin(root.right);
// 删除找到的最小节点
root.right = deleteNode(root.right, minNode.val);
// 替换掉当前节点
minNode.left = root.left;
minNode.right = root.right;
root=minNode;
}else if(root.val>key){
root.left = deleteNode(root.left,key);
}else{
root.right = deleteNode(root.right,key);
}
return root;
}
/**
* 一直往左走就是最小的
* @param root
* @return
*/
private TreeNode fingRightMin(TreeNode root){
while(root.left!=null){
root=root.left;
}
return root;
}
}
构造
二叉树的构建问题遵循固定的套路,构造整棵树可以分解成:先构造根节点,然后构建左右子树。
108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length-1);
}
private TreeNode build(int[] nums, int lo, int hi){
if (lo>hi){
return null;
}
// 中间节点就是根节点
int mid = (hi+lo)/2;
TreeNode root = new TreeNode(nums[mid]);
root.left = build(nums, lo, mid-1);
root.right = build(nums, mid+1, hi);
return root;
}
}
96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
- 以i为节点的二叉搜索树的个数=i.left 的BST个数* i.right的BST个数,所以定义count函数为count(int lo, int hi) ,闭区间 [lo, hi] 的数字能组成 count(lo, hi) 种 BST
举个例子,比如给算法输入 n = 5,也就是说用 {1,2,3,4,5} 这些数字去构造 BST。
首先,这棵 BST 的根节点总共有几种情况?
显然有 5 种情况对吧,因为每个数字都可以作为根节点。
比如说我们固定 3 作为根节点,这个前提下能有几种不同的 BST 呢?
根据 BST 的特性,根节点的左子树都比根节点的值小,右子树的值都比根节点的值大。
所以如果固定 3 作为根节点,左子树节点就是 {1,2} 的组合,右子树就是 {4,5} 的组合。 - 遍历过程中,用memo[][] 记录下
class Solution {
// 备忘录
int [][] memo;
public int numTrees(int n) {
memo = new int[n+1][n+1];
return count(1,n);
}
/**
* 计算区间[lo,hi]组成的bst个数
* @param lo
* @param hi
* @return
*/
private int count(int lo, int hi){
//空区间,也就对应着空节点 null,虽然是空节点,但是也是一种情况,所以要返回 1 而不能返回 0。
if (lo>hi){
return 1;
}
if (memo[lo][hi]!=0){
return memo[lo][hi];
}
int res = 0;
for (int i=lo;i<=hi;i++){
// i的值为根节点
int left = count(lo, i-1);
int right = count(i+1, hi);
// 左子树BST个数和右子树BST个数的乘积 就是以i为根节点的BST个数
res = res + left*right;
}
memo[lo][hi] = res;
return res;
}
}
95. 不同的二叉搜索数 2
给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。
1、穷举 root 节点的所有可能。
2、递归构造出左右子树的所有有效 BST。
3、给 root 节点穷举所有左右子树的组合。
class Solution {
/* 主函数 */
public List<TreeNode> generateTrees(int n) {
if (n == 0) return new LinkedList<>();
// 构造闭区间 [1, n] 组成的 BST
return build(1, n);
}
/* 构造闭区间 [lo, hi] 组成的 BST */
List<TreeNode> build(int lo, int hi) {
List<TreeNode> res = new LinkedList<>();
// base case
if (lo > hi) {
// 这里需要装一个 null 元素,这样才能让下面的两个内层 for 循环都能进入,正确地创建出叶子节点
// 举例来说吧,什么时候会进到这个 if 语句?当你创建叶子节点的时候,对吧。
// 那么如果你这里不加 null,直接返回空列表,那么下面的内层两个 for 循环都无法进入
// 你的那个叶子节点就没有创建出来,看到了吗?所以这里要加一个 null,确保下面能把叶子节点做出来
res.add(null);
return res;
}
// 1、穷举 root 节点的所有可能。
for (int i = lo; i <= hi; i++) {
// 2、递归构造出左右子树的所有有效 BST。
List<TreeNode> leftTree = build(lo, i - 1);
List<TreeNode> rightTree = build(i + 1, hi);
// 3、给 root 节点穷举所有左右子树的组合。
for (TreeNode left : leftTree) {
for (TreeNode right : rightTree) {
// i 作为根节点 root 的值
TreeNode root = new TreeNode(i);
root.left = left;
root.right = right;
res.add(root);
}
}
}
return res;
}
}
二叉树公共祖先
labuladong一文秒杀5道最近公共祖先问题
一个节点能够在它的左右子树中分别找到 p 和 q,则该节点为 二叉树公共祖先(LCA) 节点。
p和q一定存在于二叉树
模板:寻找值为 val1 或 val2 的节点
// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {
// base case
if (root == null) {
return null;
}
// 前序位置,看看 root 是不是目标值
if (root.val == val1 || root.val == val2) {
return root;
}
// 去左右子树寻找
TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);
// 后序位置,已经知道左右子树是否存在目标值
// ******寻找公共祖先添加代码的位置*******
return left != null ? left : right;
}
236. 二叉树的最近公共祖先
找p 和 q 的最近公共祖先,且p 和 q 均存在于给定的二叉树中。
找p和q的公共祖先,就是遍历二叉树看当前root的左右子树是否能找到p和q
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root==null){
return null;
}
// 前序
if (root.val==p.val || root.val==q.val){
// 情况二:祖先是自己
return root;
}
// 从左右子树找
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 后序 从左右子树找到了
if (left!=null && right!=null){
// 情况一:在左右子树分别找到了p和q
return root;
}
return left!=null?left:right;
}
}
情况一:左右子树分别找到了p和q
情况二:找到一个值为 val1 或 val2 的节点则直接返回,则祖先是自己。因为p和q一定存在于二叉树中,所以root找到直接返回
1676. 二叉树的最近公共祖先
给你输入一棵不含重复值的二叉树,但这次不是给你输入 p 和 q 两个节点了,而是给你输入一个包含若干节点的列表 nodes(这些节点都存在于二叉树中),让你算这些节点的最近公共祖先。
和找p和q一样
TreeNode lowestCommonAncestor(TreeNode root, TreeNode[] nodes) {
// 将列表转化成哈希集合,便于判断元素是否存在
HashSet<Integer> values = new HashSet<>();
for (TreeNode node : nodes) {
values.add(node.val);
}
return find(root, values);
}
// 在二叉树中寻找 values 的最近公共祖先节点
TreeNode find(TreeNode root, HashSet<Integer> values) {
if (root == null) {
return null;
}
// 前序位置
if (values.contains(root.val)){
return root;
}
TreeNode left = find(root.left, values);
TreeNode right = find(root.right, values);
// 后序位置,已经知道左右子树是否存在目标值
if (left != null && right != null) {
// 当前节点是 LCA 节点
return root;
}
return left != null ? left : right;
}
p和q不一定存在于二叉树
p 和 q 不一定存在于树中,所以你不能遇到一个目标值就直接返回,而应该对二叉树进行完全搜索(遍历每一个节点),如果发现 p 或 q 不存在于树中,那么是不存在 LCA 的。
对二叉树进行完全搜素的模板:
TreeNode find(TreeNode root, int val) {
if (root == null) {
return null;
}
// 先去左右子树寻找
TreeNode left = find(root.left, val);
TreeNode right = find(root.right, val);
// 后序位置,判断 root 是不是目标节点
if (root.val == val) {
return root;
}
// root 不是目标节点,再去看看哪边的子树找到了
return left != null ? left : right;
}
1644 题「二叉树的最近公共祖先 II」
给你输入一棵不含重复值的二叉树的,以及两个节点 p 和 q,如果 p 或 q 不存在于树中,则返回空指针,否则的话返回 p 和 q 的最近公共祖先节点。
class Solution {
// 用于记录 p 和 q 是否存在于二叉树中
boolean foundP = false, foundQ = false;
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode res = find(root, p.val, q.val);
if (!foundP || !foundQ) {
return null;
}
// p 和 q 都存在二叉树中,才有公共祖先
return res;
}
// 在二叉树中寻找 val1 和 val2 的最近公共祖先节点
TreeNode find(TreeNode root, int val1, int val2) {
if (root == null) {
return null;
}
TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);
// 后序位置,判断当前节点是不是 LCA 节点
if (left != null && right != null) {
return root;
}
// 后序位置,判断当前节点是不是目标值
if (root.val == val1 || root.val == val2) {
// 找到了,记录一下
if (root.val == val1) foundP = true;
if (root.val == val2) foundQ = true;
return root;
}
return left != null ? left : right;
}
}
序列化与反序列化
当二叉树中节点的值不存在重复时:
- 如果你的序列化结果中不包含空指针的信息,且你只给出一种遍历顺序,那么你无法还原出唯一的一棵二叉树。
- 如果你的序列化结果中不包含空指针的信息,且你会给出两种遍历顺序
- 如果你给出的是前序和中序,或者后序和中序,那么你可以还原出唯一的一棵二叉树。
- 如果你给出前序和后序,那么你无法还原出唯一的一棵二叉树。
- 如果你的序列化结果中包含空指针的信息,且你只给出一种遍历顺序
- 如果你给出的是前序或者后序,那么你可以还原出唯一的一棵二叉树。
- 中序不可以还原,因为不知道根节点的位置
297 .二叉树的序列化与反序列化
public class Codec {
String SEP = ",";
String NULL = "#";
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
traversal(root,sb);
return sb.toString();
}
private void traversal(TreeNode root, StringBuilder sb){
if (root==null){
sb.append(NULL).append(SEP);
return ;
}
sb.append(root.val).append(SEP);
traversal(root.left, sb);
traversal(root.right, sb);
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
List<String> nodeList = new LinkedList<>();
for (String s : data.split(SEP)){
nodeList.add(s);
}
return deserialize(nodeList);
}
public TreeNode deserialize(List<String> nodeList){
if (nodeList.isEmpty()){
return null;
}
String first = nodeList.removeFirst();
if (first.equals(NULL)){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(first));
root.left = deserialize(nodeList);
root.right = deserialize(nodeList);
return root;
}
}
331. 验证二叉树的前序序列化
序列化二叉树的一种方法是使用 前序遍历 。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
例如,上面的二叉树可以被序列化为字符串 “9,3,4,#,#,1,#,#,2,#,6,#,#”,其中 # 代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
保证 每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。
你可以认为输入格式总是有效的
例如它永远不会包含两个连续的逗号,比如 “1,3” 。
注意:不允许重建树。
示例 1:
输入: preorder = “9,3,4,#,#,1,#,#,2,#,6,#,#”
输出: true
示例 2:
输入: preorder = “1,#”
输出: false
示例 3:
输入: preorder = “9,#,#,1”
输出: false
提示:
1 <= preorder.length <= 104
preorder 由以逗号 “,” 分隔的 [0,100] 范围内的整数和 “#” 组成
方法一:反序列化
在反序列化的过程中,只用关注当前node ,然后判断左右子树即可
最主要的是要知道当前node是空的时候要返回false,因为每个非空node产生的边都是2条,如果出现了空说明非空node产生的边不对
class Solution {
public boolean isValidSerialization(String preorder) {
LinkedList<String> nodes = new LinkedList<>();
for (String node : preorder.split(",")){
nodes.addLast(node);
}
return isValid(nodes) && nodes.isEmpty();
}
public boolean isValid(LinkedList<String> nodes){
if (nodes.isEmpty()){
return false;
}
// 列表最左侧是根节点
String first = nodes.removeFirst();
if (first.equals("#")){
return true;
}
return isValid(nodes) && isValid(nodes);
}
}
方法二:
一个非空node会消耗一条边,产生两条边。 一个空节点会消耗一条边。所以只用判断边的数量即可
class Solution {
public boolean isValidSerialization(String preorder) {
int edge = 1;
for(String node : preorder.split(",")){
if (node.equals("#")){
// 每个空节点 消耗一条边
edge=edge-1;
if (edge<0){
return false;
}
}else{
// 每个非空节点 消耗一条边,产生两条边
edge=edge-1;
if (edge<0){
return false;
}
edge=edge+2;
}
}
return edge==0;
}
}
二叉树的知识
遍历
别人总结归纳的遍历二叉树的方法:二叉树遍历总结
94. 二叉树的中序遍历.cpp
99.恢复二叉搜索树
144. 二叉树的前序遍历.cpp 145. 二叉树的后序遍历.cpp
102. 二叉树的层序遍历.cpp
递归(前序、中序、后序)
时间复杂度:O(n),n为节点数,访问每个节点恰好一次。
空间复杂度:空间复杂度:O(h),h为树的高度。最坏情况下需要空间O(n),平均情况为O(logn)
- 前序遍历PreOrder
var preorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
res.push(root.val) // 访问根节点的值
traversal(root.left) // 递归遍历左子树
traversal(root.right) // 递归遍历右子树
}
}
traversal(root)
return res
}
- 中序遍历 Inorder
中序遍历是先遍历左节点,再根节点,再右节点
ar inorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
traversal(root.left)
res.push(root.val)
traversal(root.right)
}
}
traversal(root)
return res
}
如果先右节点,再根节点,再左结点 就是逆中序遍历
ar inorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
traversal(root.right)
res.push(root.val)
traversal(root.left)
}
}
traversal(root)
return res
}
538. 把二叉搜索树转换为累加树
3. 后序遍历
var postorderTraversal = function(root) {
const res = []
function traversal (root) {
if (root !== null) {
traversal(root.left)
traversal(root.right)
res.push(root.val)
}
}
traversal(root)
return res
}
迭代 (层序遍历)
相关题目:
-
- 二叉树的层序遍历。给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历
bfs一下 把每层记录到List<List>中,然后翻转一下
- 二叉树的层序遍历。给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历
空间复杂度O(n) 时间复杂度O(n)
最常用版本(常用主要包括前序和层序,即【DFS和BFS】)、【前中后】序遍历通用版本(一个栈的空间)、【前中后层】序通用版本(双倍栈(队列)的空间)
迭代套路:
用压栈的方法来进行迭代 ,最关键的是用nullptr标志来区分每个递归调用栈,不同遍历顺序只用改变压栈顺序即可(先序遍历:右节点->左结点->根节点 //中序遍历: 右节点->根节点->左结点 //后序遍历: 根节点->右节点->左结点)
时间复杂度:O(n),n为节点数,访问每个节点恰好一次。
空间复杂度:O(h),h为树的高度。取决于树的结构,最坏情况存储整棵树,即O(n)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode*>stack;
if(root!=nullptr) stack.push(root);
while(!stack.empty()){
TreeNode* t = stack.top();
stack.pop();//访问过的节点弹出
if(t!=nullptr){
//先序遍历:右节点->左结点->根节点 //中序遍历: 右节点->根节点->左结点 //后序遍历: 根节点->右节点->左结点
//右节点
if(t->right!=nullptr) stack.push(t->right);//右节点先压栈,最后处理
//左结点
if(t->left!=nullptr) stack.push(t->left);
//根节点
stack.push(t);//当前节点重新压栈(留着以后处理),因为先序遍历所以最后压栈
stack.push(nullptr);//在当前节点之前加入一个空节点表示已经访问过了
}
else{
ans.push_back(stack.top()->val);//call.top()是nullptr之前压栈的一个节点,也就是上面call.push(t)中的那个t
stack.pop();//处理完了,第二次弹出节点(彻底从栈中移除)
}
}
return ans;
}
};
队列模板:
void levelTraverse(TreeNode root) {
if (root == null) return;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// 从上到下遍历二叉树的每一层
while (!q.isEmpty()) {
int sz = q.size();
// 从左到右遍历每一层的每个节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
// 将下一层节点放入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
}
117. 填充每个节点的下一个右侧节点指针
可以用层序遍历
class Solution {
public Node connect(Node root) {
bfs(root);
return root;
}
private void bfs(Node root){
if (root==null){
return ;
}
Queue<Node> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()){
int sz = q.size();
for (int i=0;i<sz;i++){
Node cur = q.poll();
if ((i+1)!=sz){
cur.next = q.peek();
}
if (cur.left!=null){
q.offer(cur.left);
}
if (cur.right!=null){
q.offer(cur.right);
}
}
}
}
}
莫里斯遍历:利用线索二叉树的特性进行遍历
空间复杂度O(1)
时间复杂度O(n) n为节点数
Morris遍历是利用线索二叉树,把空间复杂度降到了O(1)。 其实就是利用了叶子节点的左右空指针来存储某种遍历前驱节点或者后继节点。
遍历的时候,在每个节点上我们需要决定下一个遍历的方向:是遍历左子树还是遍历右子树。Morris的思想就是在节点和他的直接前驱之间设一个临时的链接predecessor->right = root.
所以就是遍历到root的时候 我们先找到他的直接前驱predecessor,(先root-left 然后一直->right直到右子树为0或为root就找到直接前驱了)
如果没有链接即predecessor->rightNULL,则建立链接,走向root->left【这是第一次遍历root结点,因此前序遍历的时候是在这里操作】
如果有链接,即predecessor->rightroot,则断开连接,走向root->right 【这是第二次遍历root结点,因此中序遍历的时候是在这里操作】
线索二叉树: 原本为空的右子节点指向了中序遍历顺序之后的那个节点,把所有原本为空的左子节点都指向了中序遍历之前的那个节点
莫里斯遍历的算法流程:
假设当前节点为cur,并且开始时赋值为根节点root。
- 判断cur节点是否为空
- 如果不为空
1)如果cur没有左孩子,cur向右更新,即(cur = cur.right)
2)如果cur有左孩子,则从左子树找到最右侧节点pre
①如果pre的右孩子为空,则将右孩子指向cur。pre.right = cur 【前序遍历在此res.push_back(cur->val)】
②如果pre的右孩子为cur,则将其指向为空。pre.right = null。(还原树结构【中序遍历在此res.push_back(cur->val)】 - cur为空时,停止遍历
后序遍历:
N叉树的前序遍历
完全二叉树
完全二叉树的定义:它是一棵空树或者它的叶子节点只出在最后两层,若最后一层不满则叶子节点只在最左侧。
计算满二叉的节点个数:如果满二叉树的层数为h,则总节点数为:2^h - 1.(如何计算2^left,最快的方法是移位计算)
222. 完全二叉树的节点个数.cpp
常用方法
用栈来维护从根节点到当前节点的所有节点
还原先序遍历的二叉树
1028. 从先序遍历还原二叉树
617.合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点
。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
Related Topics 树 深度优先搜索 广度优先搜索 二叉树
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1==null){
return root2;
}
if(root2==null){
return root1;
}
TreeNode merged = new TreeNode(root1.val+root2.val);
merged.left = mergeTrees(root1.left, root2.left);
merged.right = mergeTrees(root1.right, root2.right);
return merged;
}
}