各种二叉树的介绍汇总:
二叉树:最多有两棵子树的树被称为二叉树
满二叉树:二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上
完全二叉树:如果一个二叉树与满二叉树前m个节点的结构相同,这样的二叉树被称为完全二叉树
也就是说,如果把满二叉树从右至左、从下往上删除一些节点,剩余的结构就构成完全二叉树
二叉搜索树 (Ordered Binary Tree )
也称二叉排序树,这个是我们接触的最多的一种结构,它要求节点的左子树小于该节点本身,右子树大于该节点,每个节点都符合这样的规则,对二叉搜索树进行中序遍历就得得到一个有序的序列,二叉搜索树中序遍历是有序的。
平衡二叉树 (Balanced Binary Tree:)
严格平衡二叉树指的是一个节点的左右子树的高度差值不能大于1,均衡二叉树一般都是在二叉搜索树的基础之上添加自动维持平衡的性质,这种树的插入,搜索,删除的综合效率比较高为O(logN),比如前面介绍的AVL树(严格平衡的二叉树)和红黑树(非严格平衡的二叉树)。
94. 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回它的
中序
遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[2,1]
示例 5:
输入:root = [1,null,2]
输出:[1,2]
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
递归写法:
总体过程:先递归遍历左子树,再遍历当前节点,最后递归遍历右子树。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> ans; //定义答案数组。
vector<int> inorderTraversal(TreeNode* root) {
dfs(root); //!!!注意:从根节点开始递归,这样就可以得到一个中序遍历。
return ans; //递归结束,返回答案数组。
}
void dfs(TreeNode* root){
if(!root) return; //如果当前节点为空,我们就可以结束本层递归。
dfs(root->left); //先递归遍历左子树,直到为空,即将根节点的左子树全部遍历完毕为止。
ans.push_back(root->val); //将左子树全部递归遍历结束,就到达了根节点,我们直接把根节点插入到答案数组中去,
dfs(root->right); //再递归遍历右子树。
}
};
递归时间复杂度:O(n),n为节点个数,因为每个节点只会被遍历一次,所以时间复杂度为O(n)
空间复杂度:O(h),h为最大递归层数。
迭代写法:
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res; //定义答案数组。
stack<TreeNode*> stk; //定义栈,下面要用到。
while(root||stk.size()){ //当当前节点非空,或者栈非空时。
while(root){ //当当前节点不空。
stk.push(root); //把当前节点压入栈中。
root=root->left; //当前节点走到它的左孩子节点。
}
//while循环结束,则当前节点(根节点)一定是空,我们现在要找的是后继节点,也就是栈顶元素。
if(stk.size()>0){ //如果栈顶元素非空,
root=stk.top(); //取出栈顶元素
stk.pop(); //弹出栈顶元素
res.push_back(root->val); //取出栈顶元素的值。
root=root->right; //之后访问根节点的右子树。
}
}
return res;
}
};
2021年8月6日15:02:16:
解法一:递归写法
递归写法:
/**
* 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<Integer> res=new ArrayList<>(); //定义全局答案列表
public List<Integer> inorderTraversal(TreeNode root) {
dfs(root); //从根节点开始递归遍历
return res; //等全部递归全部完成,返回答案列表即可
}
public void dfs(TreeNode root){
if(root==null) return; //递归结束条件,即遍历到空节点,直接return结束即可,注意dfs的返回值是void,所以这里直接写的是return;而不是return null;
//先递归遍历左子树
if(root.left!=null) dfs(root.left); //注意这里if条件写不写都行,不写的话,进入dfs函数里面也会判断其左子树是否为空
//再遍历根节点,即把当前点加到全局答案数组里
res.add(root.val);
//最后再递归遍历其右子树
if(root.right!=null) dfs(root.right); //同上,这里if条件写不写均可
}
}
迭代:
/**
y总的牛皮迭代写法:
/**
* 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> stk=new Stack<>(); //定义迭代用到的栈,注意栈中存放的是树的节点。
while(root!=null||stk.size()!=0){ //当当前节点非空或者栈非空的时候,我们就要执行下面的操作
while(root!=null){ //当当前节点非空的时候,我们就将当前节点插到栈中,并将当前节点走到其左孩子位置
stk.push(root); //先将当前节点插入到栈中
root=root.left; //再走到它的左孩子的位置
//经过上面的while循环,就可以把当前节点的所有“直线左子树”上的节点加到栈中,之后就要执行弹栈操作,
//注意注意注意:这里root=root.left可千万不要写if(root.left!=null)这个条件千万不要加上,如果加上的话,只要当节点有左儿子才往这个节点走,比如图片上的7就无法达到了
//同理下面的root=root.right也不要写判断条件。
}
//之后执行弹栈操作,并且每弹出一个节点就要把这个节点的右孩子加到栈中,之后也要执行上面的将这个节点的所有“直线左子树”上的节点加到栈中的操作
if(stk.size()!=0){ //为了防止出现空指针异常,在弹出元素之前先对栈判空
root=stk.peek(); //在弹出栈顶元素之前,先记录下来栈顶元素,注意为了重用这段代码,我们用的是root记录栈顶元素
stk.pop(); //弹出栈顶元素
res.add(root.val); //将栈顶元素的值加到答案列表中,代表遍历这个节点
root=root.right; //再走到栈顶元素的右孩子节点,这里就可以体现出来使用root的好处:使用root来记录这个节点,
//这里千万不要写判断条件
//我们就可以不用写再加“直线左子树”节点到栈中的代码,就达到了复用代码的目的
}
}
return res; //最后返回答案列表
}
}
114. 二叉树展开为链表
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
- 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
提示:
树中结点数在范围 [0, 2000] 内
-100 <= Node.val <= 100
以前序遍历的规律展开为链表
- 1、存在左子树,将左子树的右链插入到当前点的右边(相当于把整棵左子树移动到当前点与右子树的中间)
- 2、否则,则遍历到右子树
详细操作
1、第一个图,顺着左子树的右链一直找,找到4结点,让4结点指向5结点,再让当前1结点的右孩子指向2,则相当于将左子树插入到1到5之间
2、第二个图同理,让3结点插入到2和4之间
空间复杂度:由于没有开辟新的数组或者其他额外空间,所以空间复杂度O(1);
时间复杂度 O(n),n表示总节点个数:时间复杂度取决于内层循环的执行次数,虽然代码很像有双层循环,
但是对于内层循环,我们每次查找的是右链,一层一层右链进行查找,每次是将子树的右链遍历一遍,并没有重复遍历节点,所以内层循环最多执行n次,所以时间复杂度是O(n)。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
while(root){ //用根节点来遍历
auto p=root->left; //每次取出根节点的左孩子,
if(p){ //如果根节点的左子树是存在的,我们就找出左孩子的右链(包括左子树本身)
while(p->right) {p=p->right;} //只要右链存在,就一直往右走,找出来整个右链为止。
//我们现在要将整个右链插入到root和root->right之间。
p->right=root->right; //将右链最后一个节点插到根节点的右孩子。
root->right=root->left; //将右链第一个节点插到根节点的右孩子,看图,一目了然。
root->left=nullptr; //一定要记得将根节点的左孩子清空,因为根节点的左孩子已经被清空了;
}
//上面if条件执行完,则根节点一定没有左孩子了,我们将根节点移动到根节点的右孩子继续接着判断其是否有左孩子,如图中2有左孩子3,我们现在要将3插入到2和4之间。
root=root->right; //继续接着判断根节点的右孩子。
}
}
};
2021年8月10日17:38:11:
迭代:
/**
* 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;
* }
* }
*/
//其实就是对二叉树进行前序遍历,即NLR,前序遍历是先遍历根节点,并构造成单链表的形式,即是:1.如果存在左子树,则将左子树的右链插到当前点的右边,
//2.否则,则遍历至右儿子。之后继续上面的步骤,直到遍历到叶子节点,最后返回原根节点即可,
//那么我们如何编码实现上面的过程呐?我们从根节点(记为cur)开始,用p开始遍历cur的左子树,p找到cur的左子树的整个右链,p最后走到cur的左子树的最右下
//之后让p指向cur的右儿子,cur的右儿子指针指向cur的左儿子,之后cur往下走一步,继续上面的过程,可以看图理解,
//这个题目最后没有要求我们返回链表,即是说我们在代码对树做出修改,实现即可,不用返回链表,而且root节点最后也不用返回,所以我们直接复用root节点,开cur节点了
//空间:o(1),时间:外层循环o(n),内层循环找右链最多o(n),且包含在内层循环中,所以时间复杂度是o(n)
class Solution {
public void flatten(TreeNode root) {
if(root==null) return; //根节点为空,直接结束
while(root!=null){ //当根节点不空,我们就继续上面的操作
TreeNode p=root.left; //p找当前节点的左子树右链上的最后一个节点
if(p!=null){ //当当前节点的左儿子不空
while(p.right!=null) p=p.right; //当左儿子有右链,就一直往右链走,直到走到右链的最后一个节点,如样例1中的节点4
p.right=root.right; //先将4指向5, //注意下面的三行代码最后写在if语句中,因为这里有p.right,所以可能为空,放到if中就可以避免空指针异常了
root.right=root.left; //再将1指向2
//要记得把左子树置空哈哈
root.left=null; //一定要将左指针置为null,否则会报错
}
root=root.right; //往下一层走,继续往下进行上面的操作,注意是往root.right走不是往root.left走啊!!!
}
//这个题目不需要任何返回值,所以这里什么都不需要写
}
}
2021年11月12日19:32:57:
//先序遍历这棵树的节点,1.如果当前节点存在左子树,我们就将左子树的右链插入到当前点和当前点的右儿子之间,如样例1中的当我们遍历到根节点1的时候
//其是有左子树的,我们将其左子树的右链,即将2,4节点插入到节点1,5之间,当然了,节点2是有左子树3的,我们也要跟着放过去(注意类的本质是引用)
//2.如果不存在左子树(其实原本其如果有左子树在经过1的操作之后也是没有左子树的了),我们就走到其右儿子,再如样例1中此时节点1的经过1的操作是没有左子树了
//我们就走到其右子树,注意此时节点1的右子树是节点2(不再是节点5了)
//之后继续上面两步的操作,即此时我们走到了2号节点,2号节点是有左子树的,我们将其左子树的右链插入到2和4之间,即将节点3插入到2,4之间
//此时节点2没有左子树了,就走到其右子树3,之后继续上面两步操作,我们发现节点3没有左子树,我们走到其右子树4,4没有左子树,走到其右子树5,5没有左子树
//走到其右子树6,6没有左子树,并且6没有右子树,我们就走到了空节点,而上面算法的流程就是直到节点为空为止,我们同样是复用了节点root
//我们的算法中保证了每一个节点都没有左子树,而只有右子树,所以最后就是一条链
//注意所有的类的本质都是引用,这里我们之所以需要找右链,是因为我们需要把右链的right指向当前节点的右儿子,即比如样例中,我们需要找到右链
//当我们找到节点右链的最后一个节点4之后,我们需要将其right指针指向5,还要将当前节点即1的right指向其左子树,即先将4.right指向1.right,再将1.right指向2
//这样我们就以把2,3,4插到1和5之间了,插完之后,1再往下走
class Solution {
public void flatten(TreeNode root) {
if(root==null) return;
while(root!=null){ //只要节点不空,我们就要进行上面的算法的流程
TreeNode p=root.left; //节点p用于找节点root的左子树的右链,即p最后会到达其右链的最后一个节点的位置
//注意这里我们是先取到节点的左儿子,在使用之前再判断的节点是否存在
if(p!=null){ //如果节点root的左子树,即节点p是存在的,我们就要找其右链
while(p.right!=null) p=p.right; //while迭代循环找其右链
//这样p就走到了其左子树的右链的最后一个节点,之后我们需要将这条右链插入到root和root->right之间,即将2,3,4插入到1和5之间
//注意要先将4指向root.right,再将root.right指向1,否则如果顺序反的话,我们就找不到root.right了
p.right=root.right; //先将4指向5
root.right=root.left; //再将1指向2
root.left=null; //注意最后一定要记得将root.left指向null,否则由于是引用,1的左儿子其实现在还在指向2,但是我们要形成单链表,这是不应该有的
}
root=root.right; //经过上面的操作,当前节点一定是没有左儿子的,我们就复用root,将root走到其右子树的位置
}
//这个题目的返回值是void,我们无需返回
}
}
104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
算法分析:递归解决:
/**
* 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; //如果根节点为空,返回0,递归结束条件,即递归到了空节点
return Math.max(maxDepth(root.left),maxDepth(root.right))+1; //否则就往下递归,从左右子树中选最大深度加上本次深度1
//注意这里不需要特判左右子树是否为空,因为每调用一次maxDepth函数,就会进入函数即会执行函数中的if判断。
}
}
144. 二叉树的前序遍历
给你二叉树的根节点 root ,返回它节点值的
前序
遍历。
提示:
树中节点数目在范围 [0, 100] 内
-100 <= Node.val <= 100
进阶:递归算法很简单,你可以通过迭代算法完成吗?
递归:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> ans;
void dfs(TreeNode* root){
if(!root) return;
ans.push_back(root->val);
dfs(root->left);
dfs(root->right);
}
vector<int> preorderTraversal(TreeNode* root) {
dfs(root);
return ans;
}
};
迭代:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res; //记录答案数组。
stack<TreeNode*> stk;
while(root||stk.size()){ //当根节点不空或者栈不空的时候.
while(root){ //当 当前节点不空。
res.push_back(root->val); //先遍历自己,再遍历左子树,先将自己的值存入答案数组中。
stk.push(root); //把当前节点压入栈中,
root=root->left; //访问遍历其左子树。
}
//当root到达空,我们要回溯到其后继节点,此时栈顶元素就是其后继节点。
if(stk.size()){ //如果说栈不空。
root=stk.top(); //根节点回溯到其后继节点,即栈顶元素。
root=root->right; //前序遍历,遍历完左子树,直接遍历其右子树。不需要再遍历根节点。
stk.pop(); //弹出栈顶元素。
}
}
return res; //返回答案数组。
}
};
2021年8月6日21:17:56:
迭代:
/**
* 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;
* }
* }
*/
//迭代:类似于94题中序遍历的迭代写法,前序遍历:NLR,这个题目和94题很像,中序遍历是要先记录下来所有的左儿子,
//而前序遍历是先遍历记录根节点,在遍历左儿子,
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res=new ArrayList<>(); //定义答案列表
Stack<TreeNode> stk=new Stack<>(); //定义栈
while(root!=null||stk.size()!=0){
while(root!=null){
//在走到左子树之前,先遍历当前点,再走到左儿子,而中序遍历是先走到左儿子再遍历这个节点
res.add(root.val); //先遍历当前节点,
stk.push(root); //再将当前节点加到栈中
root=root.left; //走到左儿子
}
//上面的while结束就把跟节点的所有左儿子都遍历完了,之后需要回溯,回溯的时候需要找到最后一个节点的后继节点,即栈顶节点
if(stk.size()!=0){
root=stk.peek(); //取出栈顶节点
stk.pop(); //取出栈顶节点
root=root.right; //往右子树走
}
}
return res; //最后返回答案
}
}
145. 二叉树的后序遍历
给定一个二叉树,返回它的
后序
遍历。
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
递归写法:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> ans;
void dfs(TreeNode* root){
if(!root) return;
dfs(root->left);
dfs(root->right);
ans.push_back(root->val);
}
vector<int> postorderTraversal(TreeNode* root) {
dfs(root);
return ans;
}
};
递归时空间复杂度:复杂度分析
时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
迭代写法:
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
while(root||stk.size()){
while(root){
res.push_back(root->val);
stk.push(root);
root=root->right;
}
root=stk.top();
root=root->left;
stk.pop();
}
reverse(res.begin(),res.end());
return res;
}
};
2021年8月7日10:30:08:
迭代:
利用前序遍历求后序遍历:
/**
* 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> stk=new Stack<>();
while(root!=null||stk.size()!=0){
while(root!=null){ //和上一题的前序遍历类似,只不过遍历顺序是:根右左,最后调用Collections.reverse(res)进行列表反转
res.add(root.val); //先遍历当前节点
stk.push(root); //将当前节点加到栈中
root=root.right; //往所有的右子树走
}
if(stk.size()!=0){ //栈不空就要做下面的操作
root=stk.peek(); //取出栈顶节点
stk.pop(); //弹出栈顶节点
root=root.left; //往左走
}
}
Collections.reverse(res); //答案列表反转
return res; //返回答案
}
}
102. 二叉树的层序遍历
给你一个二叉树,请你返回其按
层序遍历
得到的节点值。 (即逐层地,从左到右访问所有节点)。
示例:
二叉树:[3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层序遍历结果:
[
[3],
[9,20],
[15,7]
]
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res; //定义答案数组。
queue<TreeNode*> q; //宽度优先搜索需要借助于队列实现,队列中存的是节点,所以这里队列类型为TreeNode。
if(root) q.push(root); //当根节点不空,我们将根节点加入到队列中。
while(q.size()!=0){ //当队列不空的时候,
vector<int> level; //每次我们要遍历一层节点,将这一层节点放到数组level中去。
int len=q.size(); //len记录当前队列的长度。
while(len--){ //把这一层节点全部遍历一遍,这里我们要做这些事:取出队头节点并记录下来,再弹出队头元素,
//将当前节点加入到数组中,再看当前节点有无左孩子节点,若有则把左孩子节点加入到队列中,
//再看有无右孩子节点,若有则把右孩子节点加入到队列中。这样一直循环,
//直到到达叶子节点,下次循环不满足while(q.size()!=0),层序遍历也随之结束。
auto t=q.front(); //记录队头结点
q.pop(); //弹出队头元素(已经扩展过对头元素了,而且后面还要用对头元素后面的元素,所以我们需要将队头元素删除)
level.push_back(t->val); //把当前节点值加入到一维答案数组中。
if(t->left) q.push(t->left); //如果当前节点的左孩子节点非空,我们将左孩子插入到队列
if(t->right) q.push(t->right); //如果当前节点的右孩子节点非空,我们将右孩子插入到队列
}
//while循环结束,记得将这一层答案数组插入到二维答案数组中去。
res.push_back(level);
}
return res; //最后返回二维答案数组。
}
};
2021年8月7日13:52:42:
宽搜迭代版本:
/**
* 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) {
List<List<Integer>> res=new ArrayList<>(); //定义答案二重列表
Queue<TreeNode> q=new LinkedList<>(); //定义宽搜的队列,注意java中的队列是用LinkedList实现的
if(root!=null){
q.offer(root);
}
while(q.size()!=0){ //我们遍历的时候每次遍历的是当前这一层节点,每次遍历的是一层节点,而队列中每次存储的就是当前这一层节点,这一点很重要
List<Integer> level=new ArrayList<>(); //每一层的答案列表用level表示,即将这一层的节点放到level中
int len=q.size(); //len记录当前这一层节点的个数,我们每次while循环遍历的节点个数就是len个
while(len--!=0){ //遍历len个节点,即当前这一层节点的数量,这里求出len,我们在这里就可以直接使用len--来遍历len个节点了
root=q.peek(); //取出队头元素,注意这里因为while(q.size()!=0)中没有用到root节点,所以我们可以复用root节点
//也可以重新申请节点:即:TreeNode t=q.peek();
q.poll(); //弹出队头元素
level.add(root.val); //将队头元素加到level列表中
if(root.left!=null) q.offer(root.left); //当前节点有左子树,就把左子树加到队列中
if(root.right!=null) q.offer(root.right); //当前节点有右子树,就把右子树加到队列中
}
res.add(new ArrayList<>(level)); //把当前层遍历的结果放到答案中
}
return res; //返回答案
}
}
107. 二叉树的层序遍历 II
给定一个二叉树,返回其节点值自底向上的层序遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其自底向上的层序遍历为:
[
[15,7],
[9,20],
[3]
]
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> res;
queue<TreeNode*> q;
if(root) q.push(root);
while(q.size()>0){
vector<int> level;
int len=q.size();
while(len--){
auto t=q.front();
q.pop();
level.push_back(t->val);
if(t->left)q.push(t->left);
if(t->right)q.push(t->right);
}
res.push_back(level);
}
reverse(res.begin(),res.end()); //和102题一模一样,除了注意这里要将一维数组整体翻转。
//注意这里的翻转操作会把二维列表中的一维列表顺序倒序,而不会改变一维列表中元素的顺序
return res;
}
};
2021年8月7日14:19:16:
将102题的答案翻转一下即可:
宽度优先遍历,先从上到下一层一层来做,最后将结果翻转。即:
- 将根节点插入队列中;
- 创建一个新队列,用来按顺序保存下一层的所有子节点;
- 对于当前队列中的所有节点,按顺序依次将儿子加入新队列,并将当前节点的值记录在答案中;
- 重复步骤2-3,直到队列为空为止。
- 将记录的结果翻转。
时间复杂度分析:树中每个节点仅会进队出队一次,所以时间复杂度是O(n)
。
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> res=new ArrayList<>();
Queue<TreeNode> q=new LinkedList<>();
if(root!=null) q.offer(root);
while(q.size()!=0){
int n=q.size();
List<Integer> level=new ArrayList<>();
while(n--!=0){
root=q.peek();
q.poll();
level.add(root.val);
if(root.left!=null) q.offer(root.left);
if(root.right!=null) q.offer(root.right);
}
res.add(level);
}
Collections.reverse(res);
return res;
}
}
这样做是不是不讲码德:
在ArrayList中可以指定位置插入,所以我们可以使用res.add(0,level)每次在头部插入后遍历的层节点,
树的层次遍历可以使用广度优先搜索实现。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。
如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> levelOrder = new LinkedList<List<Integer>>();
if (root == null) {
return levelOrder;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
TreeNode left = node.left, right = node.right;
if (left != null) {
queue.offer(left);
}
if (right != null) {
queue.offer(right);
}
}
levelOrder.add(0, level); //达到了后遍历的层插到前面的目的。
}
return levelOrder;
}
}
103. 二叉树的锯齿形层序遍历
给定一个二叉树,返回其节点值的锯齿形层序遍历。(即
先从左往右,再从右往左进行下一层遍历,以此类推
,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回锯齿形层序遍历如下:
[
[3],
[20,9],
[15,7]
]
这个题目和102题目几乎一模一样,我们只需要额外定义一个参数cnt,用来记录当前遍历的是奇数层还是偶数层即可(首层是第一层),如果是奇数层,没变化,偶数层就对其进行翻转即可。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>> res; //定义二维答案数组
queue<TreeNode*> q; //定义队列,后面要用到
if(root) q.push(root); //根节点不空,就将根节点插入到队列中。
int cnt=0; //定义当前是奇数层还是偶数层。之所以设置为0,是因为后面我们每遍历完一层就要对cnt++操作,如果不设定为0,则第一次cnt++操作就会变成2了。
//也可以把cnt初始化为1,只需要把cnt++放到if(cnt%2==0)reverse(level.begin(),level.end());这一句后面即可。
while(q.size()){ //只要队列中有元素。我们就要遍历
vector<int> level; //定义一层答案数组
int len=q.size(); //定义这一层的节点数len
while(len--){ //因为有len个节点,所以我们需要遍历len次。
auto t=q.front(); //每次我们先取出队头元素。
q.pop(); //删除队头元素
level.push_back(t->val); //将队头元素的值插入到这一层答案数组中。
if(t->left) q.push(t->left); //如果当前这个点有左孩子,将其左孩子插入到队列里面。
if(t->right) q.push(t->right); //如果当前这个点有右孩子,将右孩子插入到队列里面。
}
cnt++; //每遍历完一层(即每执行完一整个while(len--)循环,即意味着我们执行完一层)
if(cnt%2==0)reverse(level.begin(),level.end()); //如果说当前层是偶数层,将其进行翻转
res.push_back(level); //把当前这一层答案插入最终答案数组里面。
}
return res; //将答案数组返回即可。
}
};
2021年8月7日15:28:27:
//注意这个题目细节是很多的,比如n初始化值,即n++应该在的位置都要注意。
/**
* 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;
* }
* }
*/
//这个题目和102题层序遍历类似,不同的是,这个题目是用"之"字形进行遍历,即第一层从左到右遍历,第二层从右往左遍历,第三层从左往右遍历
//所以这个题目我们在层序遍历的时候再记录一个变量表示层数,当层数是偶数的时候,我们在把一维列表level,加到二维答案列表res时翻转一下level
//
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res=new ArrayList<>(); //定义答案二维数组列表
Queue<TreeNode> q=new LinkedList<>(); //定义队列,用于存储层序遍历的结果
int n=0; //n表示层数,从0开始的
if(root!=null) q.offer(root); //注意这一步不起眼但是很重要,一定不要忘记写
while(q.size()!=0){
List<Integer> level=new ArrayList<>(); //定义本层答案列表
int len=q.size();
while(len--!=0){
root=q.peek();
q.poll();
level.add(root.val);
if(root.left!=null) q.offer(root.left);
if(root.right!=null) q.offer(root.right);
//注意n++不要写在这里了,这里的话n被加的次数就不对了,放在这里的话就表示根节点有几个孩子节点n就加几,显然不对
}
n++; //n++应该放在while循环外面,表示遍历完这一层层数加一
if(n%2==0) Collections.reverse(level); //注意n是从0开始的,所以这里是偶数层才翻转,
res.add(new ArrayList<>(level));
}
return res;
}
}
429. N 叉树的层序遍历
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
示例 2:
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
提示:
树的高度不会超过 1000
树的节点总数在 [0, 10^4] 之间
这个题目和102题几乎一模一样,除了遍历孩子节点。
记住:枚举节点的孩子节点:for(auto c:t->children) 再将子节点插入到队列:q.push(c);
即:
for(auto c:t->children){
q.push(c);
}
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
vector<vector<int>> res;
if(!root) return res;
queue<Node*> q;
if(root) q.push(root);
while(q.size()){
vector<int> level;
int len=q.size();
while(len--){
auto t=q.front();
q.pop();
level.push_back(t->val);
for(auto c:t->children) q.push(c); //遍历所有孩子节点。
}
res.push_back(level);
}
return res;
}
};
2021年8月11日20:20:47:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res=new ArrayList<>(); //定义答案
if(root==null) return res; //树是空的,直接返回空
//和二叉树的层序遍历类似,先定义一个存放层序节点的队列
Queue<Node> q=new LinkedList<>(); //定义层序遍历的要用的队列,
q.add(root); //将根节点插到队列中
//和二叉树类似,编写whle迭代循环
while(q.size()!=0){
int len=q.size(); //遍历一层,求出这一层的节点个数,每次是把一层加到队列中
List<Integer> line=new ArrayList<>(); //line是用于存放当前这一层层序遍历结果的数组列表
while(len--!=0){
Node t=q.peek(); //每次取出队头节点
q.poll(); //弹出队头节点
line.add(t.val); //将当前节点的值加到line中
for(Node c:t.children){ //注意只有这一点和二叉树不同,二叉树只有两个节点,而n叉树的节点是用一个数组给出的,所以我们只需要遍历这个数组即可
//children是节点的一个属性,所以是t.children
q.add(c); //将所有的子节点加到队列中
}
}
res.add(new ArrayList<>(line)); //添加到答案里
}
return res; //最后将答案返回
}
}
559. N 叉树的最大深度
给定一个 N 叉树,找到其最大深度。
最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。
N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:3
示例 2:
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出:5
提示:
树的深度不会超过 1000 。
树的节点数目位于 [0, 10^4] 之间。
用递归的思想:我们要求的是当前节点的最大深度,即是子节点的最大深度+1,递归一遍即可。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
//用递归的思想:我们要求的是当前节点的最大深度,即是子节点的最大深度+1,递归一遍即可。
class Solution {
public:
int maxDepth(Node* root) {
if(!root) return 0; //如果当前节点为空,就返回0;
int res=0; //用res表示每个子节点深度的最大值。
for(auto c:root->children)res=max(res,maxDepth(c)); //遍历当前节点的所有孩子节点,用res更新记录最大深度。
return res+1; //答案要加上自己本身的深度1(res即当前节点所有子树的最大深度)。
}
};
2021年8月11日20:56:57:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
//直接递归做即可,求当前节点的最大深度就是求一下每个子节点的最大深度加一,
class Solution {
public int maxDepth(Node root) {
if(root==null) return 0;
int res=0; //res表示每个节点的最大深度,每次递归都会新建一个res,记录当前节点的子节点的最大深度
for(Node c:root.children) res=Math.max(res,maxDepth(c)); //遍历当前节点的所有儿子节点,递归计算每个节点的所有儿子节点的最大深度
return res+1; //返回值是子节点的最大深度加一
}
}
589. N叉树的前序遍历
给定一个 N 叉树,返回其节点值的前序遍历。
例如,给定一个 3叉树 :
返回其前序遍历: [1,3,5,6,2,4]。
N叉树的前序遍历即先遍历根节点,再从左到右遍历其所有节点,所以我们从根节点开始递归遍历即可。
递归代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
// N叉树的前序遍历递归做法:我们先遍历根节点,再顺次遍历其所有孩子节点即可。
class Solution {
public:
vector<int> res; //res记录答案,最后返回res
void dfs(Node* root){
if(!root) return; //如果根节点为空,直接返回即可。
if(root) res.push_back(root->val); //根节点不空,先遍历根节点。
for(auto c:root->children) dfs(c); //顺次遍历根节点所有孩子节点。
}
vector<int> preorder(Node* root) {
dfs(root); //递归遍历,从根节点开始
return res; //遍历结束,返回答案数组。
}
};
2021年8月11日20:36:57:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
List<Integer> res=new ArrayList<>(); //定义答案
public List<Integer> preorder(Node root) {
if(root==null) return res;
dfs(root);
return res;
}
public void dfs(Node root){
if(root==null) return; //如果节点为空,直接返回,这也是递归结束条件
res.add(root.val); //先遍历根节点
//之后按顺序遍历n叉树的所有儿子
for(Node c:root.children){ //顺序遍历节点的所有儿子节点
dfs(c);
}
}
}
590. N叉树的后序遍历
给定一个 N 叉树,返回其节点值的后序遍历。
例如,给定一个 3叉树 :
返回其后序遍历: [5,6,3,2,4,1].
和上一题一样,只不过是要从左到右先遍历其所有孩子,再遍历根节点。
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
//N叉树的后序遍历递归写法,先遍历其所有孩子,再遍历其根节点。
class Solution {
public:
vector<int> res;
void dfs(Node* root){
if(!root) return;
for(auto c:root->children) dfs(c); //先遍历其所有孩子节点
res.push_back(root->val); //再遍历其根节点
}
vector<int> postorder(Node* root) {
dfs(root);
return res;
}
};
2021年8月11日20:37:12:
//和上个题目一样,只是把遍历当前节点放到了递归子树的后面
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
List<Integer> res=new ArrayList<>();
public List<Integer> postorder(Node root) {
if(root==null) return res;
dfs(root);
return res;
}
public void dfs(Node root){
if(root==null) return;
for(Node c:root.children){
dfs(c);
}
res.add(root.val);
}
}
404. 左叶子之和
计算给定二叉树的
所有左叶子之和
。
示例:
3
/ \
9 20
/ \
15 7
在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
思路:我们递归的判断一下当前节点的左孩子是否存在,如果存在,我们再判断一下左孩子是不是叶节点,如果是叶节点的话,我们就把左孩子的权值加到答案上。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
//我们递归的判断一下当前节点的左孩子是否存在,如果存在,我们再判断一下左孩子是不是叶节点,如果是叶节点的话,我们就把左孩子的权值加到答案上。
class Solution {
public:
int res=0;
int sumOfLeftLeaves(TreeNode* root) {
dfs(root); //从根节点开始递归。
return res; //返回答案。
}
void dfs(TreeNode* root){
if(!root) return; //如果递归过程中,节点为空,直接结束这一层递归即可。回溯到上一层递归。
if(root->left){ //否则root不空,如果root有左孩子节点; 这两句if是为了判断是否到达了叶子节点。
if(!root->left->left&&!root->left->right){
//递归到了叶子节点,即如果左孩子为叶子节点。
res+=root->left->val; //将这个叶子节点的值加到最终结果上。
}
}
dfs(root->left); //注意这里还需要对左右孩子进行递归遍历。和上一句不是if---else的关系。
dfs(root->right);
}
};
2021年8月11日17:24:38:
/**
* 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;
* }
* }
*/
//注意是左儿子的叶子结点,即同时满足是左儿子还是叶子节点,注意看样例不是左子树上叶子节点,而是所有的,包括左右子树的左叶子之和
//我们需要做的即是判断一下当前节点是否有左儿子,如果有,再判断左儿子是否是叶子节点,如果还是的话就加到全局答案里
//我们可以使用dfs,深搜从根节点开始,先判断当前节点是否有左儿子,如果有左儿子再判断左儿子是否是叶子结点,之后再往左右子树上递归,继续判断
class Solution {
int res=0; //res是全局答案之和
public int sumOfLeftLeaves(TreeNode root) {
dfs(root); //从根节点开始递归
return res; //最后返回的就是递归完相加之后的res
}
public void dfs(TreeNode root){
//唯一的递归结束条件,不可不写
if(root==null) return; //递归到了空节点,直接返回空
if(root.left!=null){ //如果当前节点有左儿子
if(root.left.left==null&&root.left.right==null){ //如果左儿子是叶子节点
res+=root.left.val; //就在答案里加上当前节点左儿子的值
}
}
//判断完当前节点,之后继续递归判断当前节点的左子树和右子树
dfs(root.left);
dfs(root.right);
}
}
更简单的递归写法:
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root == null) return 0;
int res = 0;
//判断节点是否是左叶子节点,如果是则将它的和累计起来
if(root.left != null && root.left.left == null && root.left.right == null){
res += root.left.val;
}
return sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right) + res;
}
}
105. 从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<int,int> pos; //用哈希表记录中序遍历中从数值到中序遍历位置的映射。哈希表查找时间复杂度O(1)
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
for(int i=0;i<inorder.size();i++) pos[inorder[i]]=i; //记录中序遍历数值到位置的映射。
return build(preorder,inorder,0,preorder.size()-1,0,preorder.size()-1); //递归的建立整棵树,需要把前序遍历和中序遍历传下去,参数一开始都是从0到最大长度-1。
}
TreeNode* build(vector<int>& preorder, vector<int>& inorder,int pl,int pr,int il,int ir){ //实现建树函数,需要返回根节点
if(pl>pr) return NULL; //如果pl>pr说明已经没有数了,返回空即可。
auto root=new TreeNode(preorder[pl]); //创建根节点,根节点为前序的最左端点
int k=pos[root->val]; //k就是根节点在中序遍历里面的位置
root->left=build(preorder,inorder,pl+1,pl+k-il,il,k-1); //对中左和前左进行递归建树
root->right=build(preorder,inorder,pl+k-il+1,pr,k+1,ir); //对中右和前右进行递归建树。
return root; //递归完返回根节点
}
};
2021年8月10日21:51:50:
递归建树:
递归建立整棵二叉树:先递归创建左右子树,然后创建根节点,并让指针指向两棵子树。
具体步骤如下:
- 先利用前序遍历找根节点:前序遍历的第一个数,就是根节点的值;
- 在中序遍历中找到根节点的位置
k
,则k
左边是左子树的中序遍历,右边是右子树的中序遍历; - 假设左子树的中序遍历的长度是
l
,则在前序遍历中,根节点后面的l
个数,是左子树的前序遍历,剩下的数是右子树的前序遍历; - 有了左右子树的前序遍历和中序遍历,我们可以先递归创建出左右子树,然后再创建根节点;
时间复杂度分析:我们在初始化时,用哈希表(unordered_map<int,int>)记录每个值在中序遍历中的位置,这样我们在递归到每个节点时,在中序遍历中查找根节点位置的操作,只需要 O(1)的时间。此时,创建每个节点需要的时间是 O(1),所以总时间复杂度是 O(n)。
如图所示,[pl,pr]
和[il,ir]
分别以前序和中序的方式维护一棵树,前序遍历的第一个元素则是当前树的根结点的值x
,找到x
值在中序遍历的位置y
,可知[pl + 1,pl + len]
和[il,y - 1]
维护的是同一棵左子树,[pl + len + 1,pr]
和[y + 1,ir]
维护的是同一颗右子树
从前序遍历里能找到区间根节点(第一个点),然后在中序遍历里找到根结点,可以分出左子树和右子树两部分,再递归的去做。
一般化:
要快速的在中序遍历中用结点的值找到所在的位置,可以用哈希表来存。
要画图并且要计算一下子问题区间的下标起止位置。
/**
* 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;
* }
* }
*/
//递归构造这棵树,1.先在前序遍历中找到根节点,再根据根节点位置把中序序列分成左右两部分,并且可以找到根节点前的节点就是左子树的节点,后的节点就是右子树节点
//2.之后再对应到前序遍历序列中,就可以找到左子树节点在前序遍历的位置和右子树节点在前序遍历中的位置,之后递归继续构造这棵二叉树,
//3.即是根据左前和左中递归一下就可以知道左子树,根据右前和右中递归一下就可以知道右子树
//由于我们需要前序遍历中的根节点找到其在中序遍历序列中位置,我们可以通过哈希表来完成,即用一个个哈希表记录中序序列中值与位置的对应,题目已经保证无重复元素
//这个题目已经保证有解,所以不用考虑没有定义的情况
class Solution {
Map<Integer,Integer> map=new HashMap<>(); //哈希表map记录中序序列中值与位置的对应关系
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0;i<inorder.length;i++) map.put(inorder[i],i); //遍历中序序列,记录下来中序序列中值与位置的对应关系
return dfs(preorder,inorder,0,preorder.length-1,0,inorder.length-1); //使用dfs函数递归建树,初始参数包括两个序列和序列的左右端点,初始时都是0和n-1
//上面的dfs结束之后就会返回递归建好的树的根节点
}
public TreeNode dfs(int[] preorder,int[] inorder,int pl,int pr,int il,int ir){ //编写dfs函数,传入的参数包括两个序列和两个序列的左右下标
//递归结束条件,一定不要忘了写
//pl == pr时表示叶子节点,pl > pr表示空节点
if(pl>pr||il>ir) return null; //递归结束条件,即如果左超过了有,就说明已经建好,返回null
TreeNode root=new TreeNode(preorder[pl]); //先构造出来根节点,注意注意注意不是preorder[0],不能写死了,而应该是pl
int k=map.get(root.val); //找到根节点在中序序列中的位置,即根据值找到对应的位置
//下面就开始递归建树了:
root.left=dfs(preorder,inorder,pl+1,pl+1+k-1-il,il,k-1); //自己在纸上列式子求出这几个范围,这个是左子树,传入的参数包括左子树前序序列的左右边界,和左子树中序遍历的左右边界
root.right=dfs(preorder,inorder,pl+1+k-1-il+1,pr,k+1,ir); //一定要分清,这个是右子树,传入的参数包括右子树前序序列的左右边界,和右子树中序遍历的左右边界
return root; //最后返回root节点即可
}
}
106. 从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<int,int> pos; //建立答案数组。
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
for(int i=0;i<inorder.size();i++) pos[inorder[i]]=i; //将值映射到位置。即通过值可以在中序遍历数组中找位置,
return build(inorder,postorder,0,inorder.size()-1,0,postorder.size()-1); //从初始开始递归建树。
}
TreeNode* build(vector<int>& inorder,vector<int>& postorder,int il,int ir,int pl,int pr){
if(il>ir) return nullptr; //!!!非常重要,递归结束条件。
auto root=new TreeNode(postorder[pr]); //存下头结点,头结点在后序遍历的最后一个位置。
int k=pos[postorder[pr]]; //k记录根节点在中序遍历的位置,postorder[pr]即头结点的值,因为值是唯一的,所以我们根据这个值在中序遍历中查找它的位置。
root->left=build(inorder,postorder,il,k-1,pl,pl+k-1-il); //递归遍历其根节点左子树
root->right=build(inorder,postorder,k+1,ir,pl+k-il,pr-1); //递归遍历其根节点右子树
return root; //注意返回根节点。
}
};
2021年8月11日11:06:19:
/**
* 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;
* }
* }
*/
//和上一题基本上一模一样,计算范围边界一定要列式计算出来,并且注意,root.left里写的都是左子树的范围边界,root.right里写的都是右子树的范围边界
//注意注意注意:哈希表中记录的是中序遍历数组的值与位置的对应关系,而不是后序遍历,因为我们要做的是根据后序遍历的最后一个位置(直接知道)的数找到其在中序遍历中的位置
class Solution {
Map<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 dfs(inorder,postorder,0,inorder.length-1,0,postorder.length-1);
}
public TreeNode dfs(int[] inorder,int[] postorder,int il,int ir,int pl,int pr){
if(il>ir||pl>pr) return null; //递归结束条件
TreeNode root=new TreeNode(postorder[pr]);
int k=map.get(root.val);
root.left=dfs(inorder,postorder,il,k-1,pl,k+pl-il-1); //root.left写的都是左子树的边界范围
root.right=dfs(inorder,postorder,k+1,ir,k+pl-il,pr-1); //右子树写的都是右子树的边界范围
return root; //将建立好的树返回
}
}
100. 相同的树
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:
输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:
输入:p = [1,2,1], q = [1,1,2]
输出:false
提示:
两棵树上的节点数目都在范围 [0, 100] 内
-10^4 <= Node.val <= 10^4
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
//我们使用递归,我们看一下输入的格式(输入:p = [1,2], q = [1,null,2]),我们只要遍历一下两棵树,看一下两个数上的节点值是否相同即可,只要有一处不同,即返回false,如果全部相同,则返回true。遍历的时候只需要保证两棵树遍历的时候按照相同的顺序即可。注意递归结束条件别忘了写。注意!p是空,p是非空。
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if(!p&&!q) return true; //如果两棵树全部为空则是相同的。
//上面第一种情况两棵树全部为空已经判断完毕,进入到下面说明至少一棵树不空。
if(!p||!q||p->val!=q->val) return false; //如果说p树为空(则q一定非空),如果q树为空(则p树一定非空),或者是两棵树都非空,但是值不相同,都返回false,即两颗树不同。
//可以到达这里的代码说明两棵树都非空且值也都一样,我们只需要递归看一下其孩子节点是否仍然相同即可。
return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right); //我们这里是要递归看一下两棵树的左子树和(&&)右子树是否仍然全部相同,如果全部相同,则返回true,只要左子树或者右子树有一个不相同即返回false,&&运算必须全部为true,才返回true。
}
};
2021年8月7日11:43:49:
递归方法:
递归很巧妙:
/**
* 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条件必不可少,是递归结束的条件
if(p==null&&q==null) return true; //两棵树都是空,就说明是相同的树,返回true
if(p==null||q==null||p.val!=q.val) return false; //经过上面的判断已经说明两棵树不可能同时为空,
//如果这个if条件成立就说明有一颗树是空的,或者是两棵树的根节点值不同,都说明这两棵树是不同的树,我们返回false
//上面的if没有返回false,说明两棵树均非空且根节点的值也相同,我们就需要递归判断一下两棵树的左右儿子是否相同,
return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right); //否则我们就需要递归判断两棵子树的左儿子,右儿子是否同时相同,
//只有同时相同时(&&),才说明两棵子树是相同的树,所以这里我们使用的是&&,
}
}
101. 对称二叉树(迭代写法也要会)
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(!root) return true; //如果树为空,则默认为左右对称的。
return dfs(root->left,root->right); //否则树不为空,我们递归的看一下根节点的左右两棵子树是否左右对称。
}
bool dfs(TreeNode* p , TreeNode* q){ //参数p,q分别为根节点的左右两棵子树。
if(!p&&!q) return true; //如果左右两棵子树全部为空,则一定左右对称。
if(!p||!q||p->val!=q->val) return false; //如果说有一棵子树为空,另一棵子树不为空或者是左右两棵子树的值不相同,则一定不是左右对称的,我们返回false。
//代码到达这里,说明上面两个条件均不满足,即两棵子树均非空且值一样,所以我们现在需要递归的判断一下左子树的右孩子和右子树的左孩子是否相等,且左子树的左孩子和右子树的右孩子是否相等。只有两者同时都为true,才返回true。只要有一个为false,即返回false,所以这里我们使用&&。
return dfs(p->left,q->right)&&dfs(p->right,q->left);
}
};
算法分析:
2021年8月7日13:22:37:
递归:
递归: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;
* }
* }
*/
//注意这个题目不是要求每一个节点的左右儿子值相同,而是要求整棵数是要左右对称的,看清样例,即在树中间放一面镜子,镜子左右两边完全对称才行
//所以我们就可以利用这个条件,枚举一下左右两边的所有点,看一下点是不是一样的即可,我们先看一下根节点的左右两边是否相同,
//相同之后再对称的遍历两边的左右子树,即是说当遍历左边的左子树要遍历看一下右边的右子树,遍历左边的右子树要遍历右边的左子树
//看图片上的图和注释,这个题目结合了100题,是100题的加强版
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null) return true; //如果根节点为空,肯定是一棵对称二叉树直接返回true
return dfs(root.left,root.right); //否则就要看一下根节点左右子树是否满足:左.左=右.右&&左.右=右.左,
//所以我们需要把根节点的左右子树传到我们自己编写的dfs函数中,dfs函数其实就是100题的改编
}
public boolean dfs(TreeNode p,TreeNode q){ //dfs函数类似于100题的isSameTree函数,但是判断的是:左.左=右.右&&左.右=右.左
if(p==null&&q==null) return true; //都为空,肯定是对称二叉树,返回true
if(p==null||q==null||p.val!=q.val) return false; //只有一个节点是空或者两个节点的值不同,都不是对称二叉树,返回false
return dfs(p.left,q.right)&&dfs(p.right,q.left); //上面的if条件不满足,就说明两个节点均不空并且值也相同,
//此时就需要递归判断:左.左=右.右&&左.右=右.左,注意是&&,即必须同时成立才可以,并且注意这里使用的函数是dfs,不是isSymmetric,
}
}
迭代:
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root.left);
queue.offer(root.right);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if (node1 == null && node2 == null) {
continue;
}
if (node1 == null || node2 == null || node1.val != node2.val) {
return false;
}
queue.offer(node1.left);
queue.offer(node2.right);
queue.offer(node1.right);
queue.offer(node2.left);
}
return true;
}
}
2021年11月11日15:28:21:
迭代写法:
//迭代写法也要会,迭代在保证对称的时候,是在加入的时候保证的,即我们先同时加入左儿子的左孩子和右儿子的右孩子,之后同时加入左儿子的右孩子和右儿子的左孩子
//之后再从队列中弹出元素的时候也是成对弹出的,即每次同时弹出和判断左儿子的左孩子和右儿子的右孩子,之后同时加入左儿子的右孩子和右儿子的左孩子
//这样之后我们在判断的时候就可以保证判断的两个对称的节点
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null) return true;
Queue<TreeNode> q=new LinkedList<>();
q.add(root.left); //把根节点的左右孩子加到队列中
q.add(root.right);
while(q.size()!=0){
TreeNode node1=q.poll();
TreeNode node2=q.poll();
if(node1==null&&node2==null) continue;
if(node1==null||node2==null||node1.val!=node2.val) return false;
//每次同时加入左儿子的左孩子和右儿子的右孩子
q.add(node1.left);
q.add(node2.right);
//之后同时加入左儿子的右孩子和右儿子的左孩子
q.add(node1.right);
q.add(node2.left);
}
return true; //最后都没有返回false的话,就说明是对称的,我们返回true
}
}
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
提示:
树中节点数的范围在 [0, 10^5] 内
-1000 <= Node.val <= 1000
空间复杂度与递归层数成正比。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int minDepth(TreeNode* root) {
if(!root) return 0; //如果“当前”节点为空,则深度为0;
if(!root->left&&!root->right) return 1; //如果当前节点为叶子节点(叶子节点的左右两棵子树均为空),则深度为1;
//代码到达这里说明当前节点即不为空也不是叶子节点,即至少有一个孩子节点(一个两个孩子节点均有可能)。
if(root->left&&root->right) return min(minDepth(root->left),minDepth(root->right))+1; //如果当前节点左右两棵子树均存在,我们就返回左右两棵子树的深度最小的那一个。
if(root->left) return minDepth(root->left)+1; //即如果左子树不空(结合上面我们知道右子树一定为空),
//!!!注意这里我们求的深度是到叶子节点的深度,如果右子树为空,则这里我们应该求的深度应该是到左子树的深度,即左子树深度+1(1是本身的深度1)。
//到达这里的情况一定是上面条件均不满足,即是左子树为空,右子树不空的情况,!!!注意这里不能写if条件语句,可以写else或者省略else,如果直接写if(root->right)会报无返回值错误。
else return minDepth(root->right)+1;
}
};
2021年8月7日16:15:40:
递归:
首先先搞清楚题意:叶子节点是指没有子节点的节点,比如对于[1,2]这个样例,最小深度是2,而不是1,这句话的意思是 1 不是叶子节点
题目问的是到叶子节点的最短距离,所以所有返回结果为 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;
* }
* }
*/
//树的题目一般都是递归做的,考虑每一个节点的局部信息,即还是“一个圈带两个三角形”那个图,根是u,左右子树分别是a,b
//则所有以u为根节点的树的最小深度记为f(u),我们需要考虑的就是f(u)和f(a),f(b)的关系,只要我们滤清了这个关系,我们就可以递归的求出每一个f(u)
//最后f(root)就是我们的答案,很多二叉树的题目都是这样考虑的
//那么我们该如何找出来这个关系呐,首先要搞清楚f(u),f(a),f(b)的定义,f(u)是以u为根节点的所有子树的深度最小值
//所以根据定义,分这样几种情况:1.如果u是叶子节点,则f(u)=1;2.如果u不是叶子节点,则分三种情况:1.左右子树a,b均不空,则f(u)=min(f(a),f(b))+1;
//2.a不空,b空,注意题目中的定义:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意一定要是到叶子节点的深度
//所以如果b空了,就一定不是叶子节点,所以这里我们应该是f(u)=f(a)+1; 同理3.a空,b不空,f(u)=f(b)+1
//所以我们就发现了f(u)的深度可以通过左右子树求出来,所以整棵二叉树的最小深度可以通过递归求出来
//时间:整个过程需要计算n个节点,且每一个节点的计算次数是常数次,所以整个算法的时间复杂度是O(n)的,
class Solution {
public int minDepth(TreeNode root) {
if(root==null) return 0; //空树,深度是0
if(root.left==null&&root.right==null) return 1; //如果是叶子节点,深度是1
if(root.left!=null&&root.right!=null) return Math.min(minDepth(root.left),minDepth(root.right))+1; //左右子树均不空,返回两棵子树的最小值,别忘了加一
//注意左右子树均不空应该写在最前面,否则先写单个的写起来比较麻烦一点
//经过上面的if判断,如果没有执行则说明左右子树至少有一个非空,并且不是均非空,即一个空一个非空
if(root.left!=null) return minDepth(root.left)+1; //左子树不空就递归左子树,注意题目中的深度是根节点到叶子结点的深度,所以此时右子树是空的,没有叶子节点,也就没有深度可言;别忘了加一
else return minDepth(root.right)+1; //注意最后这里不能写if(root.right!=null),而应该写else或者省略else,否则会报无返回值的错误,别忘了加一
}
}
迭代:
即广度优先搜索
思路及解法
同样,我们可以想到使用广度优先搜索的方法,遍历整棵树。
当我们找到一个叶子节点时,直接返回这个叶子节点的深度。广度优先搜索的性质保证了最先搜索到的叶子节点的深度一定最小。
class Solution {
class QueueNode {
TreeNode node;
int depth;
public QueueNode(TreeNode node, int depth) {
this.node = node;
this.depth = depth;
}
}
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
Queue<QueueNode> queue = new LinkedList<QueueNode>();
queue.offer(new QueueNode(root, 1));
while (!queue.isEmpty()) {
QueueNode nodeDepth = queue.poll();
TreeNode node = nodeDepth.node;
int depth = nodeDepth.depth;
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
queue.offer(new QueueNode(node.left, depth + 1));
}
if (node.right != null) {
queue.offer(new QueueNode(node.right, depth + 1));
}
}
return 0;
}
}
复杂度分析
时间复杂度:O(N),其中 N 是树的节点数。对每个节点访问一次。
空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于队列的开销,队列中的元素个数不会超过树的节点数。
110. 平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
树中的节点数在范围 [0, 5000] 内
-10^4 <= Node.val <= 10^4
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool flag; //定义全局答案flag,记录是否是一棵平衡二叉树
bool isBalanced(TreeNode* root) {
flag=true; //初始化flag为true,即是一颗平衡二叉树。
dfs(root); //从当前节点开始递归。
return flag; //返回答案
}
int dfs(TreeNode* root){ //dfs返回的是当前节点的高度最大值,root是当前节点。
if(!root) return 0; //如果当前节点为空,则返回高度为0;
//代码到达这里说明当前节点不空,
int lh=dfs(root->left),rh=dfs(root->right); //我们用lh和rh记录当前节点的左右孩子的最大深度,递归的求出lh和rh。
if(abs(lh-rh)>1) flag=false; //如果左右两棵子树的高度之差大于1,将flag置为false,即判断到这里已经知道这棵树不是平衡二叉树。
//只要递归过程中,有一个节点满足了这个条件即左右子树之差大于了1,我们就置flag为false.(如果都不满足这个条件,这句话是不会执行的)。
return max(lh,rh)+1; //最后返回的是当前高度的最大值,即左右子树的最大值加上本身的高度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;
* }
* }
*/
//这个题目其实就是结合前面的一些题目,或者说你不仅要写出这个题目的算法,而又由于这个题目用到了其他算法(比如说104题求最大深度),
//所以也需要写出求最大深度的算法,所以这个题目我们需要写一个dfs函数,传入的是根节点,返回的是根节点的最大深度,
//我们需要判断的是每一个节点的左右子树的最大深度之差是否<=1,所以这个题目的dfs函数中需要做的不只是返回根节点的最大深度,
//还需要在dfs函数中判断每个节点的左右子树的深度之差(Math.abs(lh-rh))是否<=1,
class Solution {
boolean flag=true; //定义全局答案
public boolean isBalanced(TreeNode root) {
if(root==null) return true;
dfs(root);
return flag;
}
public int dfs(TreeNode root){ //dfs函数返回的是节点的最大深度
if(root==null) return 0; //叶子节点的深度为0
int lh=dfs(root.left),rh=dfs(root.right); //定义节点root的左右子树的最大深度分别为lh,rh
if(Math.abs(lh-rh)>1) flag=false; //如果节点的左右子树的最大深度之差>1,就将flag改为false,
//傻逼注意:这里不能直接返回false,因为dfs的返回值类型是int
return Math.max(lh,rh)+1; //否则返回当前节点最大深度
}
}
112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum 。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:false
示例 3:
输入:root = [1,2], targetSum = 0
输出:false
提示:
树中节点的数目在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if(!root) return false; //如果当前节点为空,返回false,说明从根节点到当前节点不存在路径,
sum-=root->val; //每次递归减去路径上节点的值,最后判断一下到叶子节点,sum值是否已经变成0了,如果变成0了说明存在路径。
if(!root->left&&!root->right) return sum==0; //如果递归过程中当前节点是叶子节点了,我们看一下sum是否等于0,(return sum==0:如果sum等于0,返回true,否则返回false)
return (root->left&&hasPathSum(root->left,sum))||(root->right&&hasPathSum(root->right,sum));
//递归判断如果节点有左子树并且左子树存在一条路径总和等于sum,或者节点有右子树并且右子树有一条路径总和等于sum。
//只要这两种情况有一个成立即可。
}
};
2021年8月7日17:12:56:
解法一:
/**
* 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;
* }
* }
*/
//先看清楚题目:题目要求一定要是:从 根节点到叶子节点 的路径,而不能是任意的两个节点之间的节点之和
//还是考虑"一个圈u带两个三角形a,b那个图",我们定义f(u)表示从根节点走到u的权值之和,则f(a)=f(u)+a.val,f(b)=f(u)+b.val,
//我们有了f(u)之后该怎么求这个题目的答案呐?我们只需要判断一下u这个点是不是叶节点即可,如果u这个点是叶节点,并且f(u)=sum的话,就说明存在一条路径,返回true
//如果遍历完所有的叶子节点,都不存在一条路径使得f(u)=sum的话,我们就返回false,当然我们也可以将f(u)看做是sum-u.val的值,最后是0就说明存在
//这样就可以少一个变量,当然也可以就定义为权值之和,注意题目要求的是返回true或者false
class Solution {
int n=0;
public boolean hasPathSum(TreeNode root, int sum) {
if(root==null) return false; //如果树是空的,就说明不存在路径,返回false
sum-=root.val; //先减去节点的值,当节点为叶子节点并且节点值为0,就返回true,一定要注意先减去节点的值
if((root.left==null)&&(root.right==null)&&(sum==0)) return true; //如果是叶子节点,并且节点值已经被减到0,返回true
//否则当前节点就不是叶子结点,我们就要分别往左右两棵子树看一下是否存在路径,左右子树只要有一边存在即可,所以是||
return ((root.left!=null)&&(hasPathSum(root.left,sum)))||((root.right!=null)&&(hasPathSum(root.right,sum)));
}
}
解法二:用和写,不是用差来记录:
/**
* 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;
* }
* }
*/
//使用加法来运算,这里是常规的dfs递归的写法,几把还是不会,诶!
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null) return false; //根节点为空,返回false
return dfs(root,targetSum,0); //否则就执行dfs函数,传入的参数包括根节点,总和,已经当前和,当前和是0
}
public boolean dfs(TreeNode root,int targetSum,int cur_sum){
if(root==null) return false; //遍历到了空节点提前结束递归,返回false
cur_sum+=root.val; //否则root就不是空节点,加上当前节点的值
if(root.left==null&&root.right==null) { //如果是叶子节点就判断一下当前和是否和sum相同,相同就找到了一条路径,返回true,
//如果不同,返回false,注意这里不是整个算法就会返回了false,而是下面的if判断中的递归条件为了false,结束的是下面的if中的递归,不是整个递归
//这一点很重要,
return targetSum==cur_sum;
}
if(dfs(root.left,targetSum,cur_sum)==true) return true; //往左子树递归
if(dfs(root.right,targetSum,cur_sum)==true) return true; //往右子树递归
return false; //这里的return false才是最后的会返回给最后答案的false,能执行到这里说明上面的两条if语句均没有满足
}
}
迭代:
113. 路径总和 II
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
//这个题目和上一题唯一的一点不同:上一题找到一条路径即返回,而这个题目我们即使找到了一条也不能停,要把整棵树全部递归一遍。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> ans; //ans存答案数组。
vector<int> path; //path存储当前路径。
vector<vector<int>> pathSum(TreeNode* root, int sum) {
if(root) dfs(root,sum); //如果根节点存在,从根节点开始递归查找路径,
return ans;
}
void dfs(TreeNode* root,int sum){
path.push_back(root->val); //把当前节点加入到路径上。
sum -=root->val; //每次递归减去路径上节点的值,最后判断一下到叶子节点,sum值是否已经变成0了,如果变成0了说明存在路径。
if(!root->left&&!root->right){ //如果递归到了叶子节点,我们此时需要判断一下sum是否为0,如果为0,说明这条路径合法,加入到ans中去。
if(sum==0) ans.push_back(path);
}else{ //即当前节点不是叶子节点,则一定至少有左子树或者右子树,或者左右子树都有。我们再对其左右子树递归遍历
if(root->left) dfs(root->left,sum);
if(root->right) dfs(root->right,sum);
}
path.pop_back(); //弹出叶子节点。
}
};
2021年8月7日19:38:29:
递归:
/**
* 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;
* }
* }
*/
//上一题是判断是否存在从根节点到叶子节点的路径使得路径总和等于sum,这个题目是让我们找到所有这样的路径输出
//上个题目如果左子树中已经存在答案了,就不会再搜索右子树了,而这个题目不管左子树中是否存在,都要搜索右子树,
//这个题目还是用差值计算,即最后是叶子结点并且sum等于0说明是一条合法的路径
class Solution {
List<List<Integer>> res=new ArrayList<>(); //定义全局答案
List<Integer> path=new ArrayList<>(); //path存储当前路径,因为path是全局共享的,所以记得要回溯清空
//注意答案要求的是root.val,即是值,而不是节点
public List<List<Integer>> pathSum(TreeNode root, int sum) {
if(root==null) return res; //只要当根节点不空的时候才执行下面的dfs语句
dfs(root,sum); //执行dfs,说明dfs非空,我们从根节点开始查找,当前差值还是sum,sum存储的其实就是差值f(u)
return res; //结束说明的dfs之后,返回答案列表
}
public void dfs(TreeNode root,int sum){
//能进来的点一定不空,所以这里可以不用判断
path.add(root.val); //先把当前点加到当前路径答案中
sum-=root.val; //先减去当前节点的值,注意别忘了
if(root.left==null&&root.right==null){ //当前节点是叶子结点,
if(sum==0) res.add(new ArrayList<>(path)); //就判断一下sum是否为0,如果是0的话,就找到了一条满足条件的路径就加到答案列表里
}else{ //否则当前节点就不是叶子节点,可能同时有左右子树或者左右子树中的一个,所以下面写了两个判断
if(root.left!=null) dfs(root.left,sum); //当左子树不空,就往左子树递归遍历
if(root.right!=null) dfs(root.right,sum); //当右子树不空,就往右子树递归遍历
//写在一个else里面,这样写就可以同时处理当前节点同时有左右子树或者只有左右子树中的一个
}
path.remove(path.size()-1); //记得回溯,和path.add(root);配对使用
}
}
力扣官方题解答案:
class Solution {
List<List<Integer>> ret = new LinkedList<List<Integer>>(); //使用LinkedList来接收,可以调用LinkedList中的api,
Deque<Integer> path = new LinkedList<Integer>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
dfs(root, targetSum);
return ret;
}
public void dfs(TreeNode root, int targetSum) {
if (root == null) {
return;
}
path.offerLast(root.val);
targetSum -= root.val;
if (root.left == null && root.right == null && targetSum == 0) {
ret.add(new LinkedList<Integer>(path));
}
dfs(root.left, targetSum);
dfs(root.right, targetSum);
path.pollLast();
}
}
复杂度分析:
124. 二叉树中的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点
root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
提示:
树中节点数目范围是 [1, 3 * 10^4]
-1000 <= Node.val <= 1000
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int ans; //定义全局答案。
int maxPathSum(TreeNode* root) {
ans=INT_MIN; //答案初始化为最小值
dfs(root); //从根节点开始递归。当递归回溯的时候最后一次计算即是整个树中的最大路径和。
return ans; //递归结束,将答案返回。
}
int dfs(TreeNode* u){
if(!u) return 0; //到达空节点,结果为0;
int leftmax=max(0,dfs(u->left)), rightmax=max(0,dfs(u->right)); //递归计算左边的路径最大值,如果最大值小于0,就不要再加左边的值了,这里我们用和0取max实现这个操作,右边同理。
ans=max(ans,u->val+leftmax+rightmax); //更新记录整个树中的最大路径和。
return u->val+max(leftmax,rightmax); //这里返回的是根节点的值加上左子树或者右子树的最大路径。
}
};
2021年8月7日21:19:44:
递归,很难的一道题目,y总太强了!!!
递归函数:
这个题目并不是直接递归传递的原函数,而是重新编写了dfs函数,并且dfs返回值是以某一个节点为最高点的单侧最大值。
/**
* 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;
* }
* }
*/
//这个题目中的路径并没有要求是从根节点到叶节点,而是树中的任意一条路径,起点有n个,终点有n个,路径数量级是N^2,
//在枚举路径的时候,我们枚举的是路径的最高点,即是LCA(最近公共祖先),我们在考虑的时候,考虑的是以每一个节点为最高节点的所有路径中的最大路径和,
//我们在考虑的时候,还是要想到"一圈加两个三角形"那个模型,以一个点为最高点的所有路径里面有两部分:一部分是往左子树走,一部分是往右子树走,
//而往左右子树的两部分长度是互不影响的,相互独立的(没有任何公共点公共边),所以如果我们想让这个路径和最大,由于完全独立,所以我们最需要让左右两部分均取最大值即可
//最后让跟节点的值加上左右两边子树的最大值,三者之和即是所有路径中的最大值,那么对于根节点u来说,f(u)表示从u往下走的最大值(不能转弯的那种)
//则f(u)取值有三种情况:1.f(a),f(b)均<=0,则f(u)=Vu,2.往左走:f(u)=Vu+f(a),3.往右走:f(u)=Vu+f(b),
//则以u为最高点的路径最大值为:Vu+max(0,f(a))+max(0,f(b)),注意和0取最大值,即如果<0,就要及时止损,u其实是树中的每一个节点
//而这个题目求的是所有节点中的最大路径和,所以我们需要定义一个全局答案res,用于更新记录以每一个节点为最高点的最大路径和,
//这个题目每个节点只会被遍历一次,所以时间复杂度是O(n)
class Solution {
int res=Integer.MIN_VALUE; //定义全局答案,初始化为最小值
public int maxPathSum(TreeNode root) {
dfs(root); //因为树非空,所以可以不用特判空树的情况,从根节点开始往下递归
return res; //递归结束,返回全部路径中的最大路径和
}
public int dfs(TreeNode u){ //dfs函数返回的是以节点u为最高点的单侧最大值,即往左走或者往右走
if(u==null) return 0; //遍历到空节点,返回0
int ld=Math.max(0,dfs(u.left)); //往左边遍历的最大值,注意这里要和0取最大值,如果ld<0,相当于直接放弃往左走
int rd=Math.max(0,dfs(u.right)); //往右边遍历的最大值,也要和0取最大值
//下面更新res
res=Math.max(res,u.val+ld+rd); //更新res。res是取的是本节点的值加左右两边的最大值,三者之和
return u.val+Math.max(ld,rd); //注意dfs函数返回的是以节点u为最高点的单侧最大值,别光顾着更新res,忘了返回dfs函数的返回值了
}
}
2021年10月22日14:02:19:
/**
* 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;
* }
* }
*/
//首先路径的起点有n个,终点有n个,所以路径的总条数的级别是O(n^2)的,我们要把所有的路径都枚举出来才会得到最大值,那么我们该如何才能枚举出来所有的路径呐?
//在树的题目中有一个很常用的枚举所有路径的方式就是将每一个点都作为最高点枚举一下,即枚举每一个点作为最高点,其实一个路径的最高点就是路径两个端点的LCA
//以每一个节点为最高点的路径有很多,那么我们该如何计算出来以这个点为最高点的最大路径和呐?我们还是考虑二叉树的经典模型:一个灯挂俩灯的模型
//因为以一个节点为最高点的路径分为两部分:一部分往左子树,一部分是往右子树,并且左子树和右子树是没有公共点和公共边的,即相互之间互不影响
//左边求一个最大值,右边求一个最大值,再加上根节点的值就是以这个根节点为最高点的最大路径和,
//所以我们就可以让我们的dfs的返回值是以某一个点往下走的最大路径和,如:局部信息的根节点是u,其左右子树是a,b,我们用f[a]表示从a往下走的最大路径和
//用f[b]表示从b往下走的最大路径和,则f[u]即从u往下走的最大路径和就有三种情况:1.走到u就步往下走了,即f[u]=V[u],V[u]是u节点的权值,
//2.往左子树走,则f[u]=V[u]+f[a],3.往右子树走f[u]=V[u]+f[b],那么我们求出了这几项有什么用呐?因为我们要求的是以u为最高点的最大路径和
//我们可以发现其值就是V[u]+max(0,f[a])+max(0,f[b]),注意如果往一边走是负值的话,我们就要及时止损,即不能再往下走了,所以要和0取max
//这个题目我们每一个节点都只被遍历了一次所以时间复杂度就是O(n),因为递归返回的时候我们就保证了所有点的f[u]都被算且只被计算一次
class Solution {
int res=-0x3f3f3f3f; //维护一个全局最大值
public int maxPathSum(TreeNode root) {
dfs(root); //从根节点开始递归
return res; //递归结束返回res
}
public int dfs(TreeNode u){ //返回以u为最高点的往下的最大路径和
if(u==null) return 0; //如果u是空节点,我们就返回0
//否则我们就要求出以u为最高点的往下的单边最大路径和
//因为我们在求一个点为最高点的最大路径和的时候需要用到其左右子树的情况,所以我们要先往其左右子树递归先求出左右子树的f[a],f[b].
//这一点还是和归并排序的递归写法是相同的,即先往其左右两部分递归,递归结束的时候开始归并排序,
int fa=dfs(u.left); //先往u节点的左右子树上递归求出其左右子树的单边最大路径和fa,fb,因为我们在更新fu的时候需要用到fa,fb
int fb=dfs(u.right);
//在返回dfs函数的返回值之前先更新一下res
res=Math.max(res,u.val+Math.max(0,fa)+Math.max(0,fb)); //更新以u节点为最高点的最大路径和,首先u.val必须有,
//然后如果左右两边是负数的话,我们就不能用,所以我们在将fa,fb加到u.val之前要先和0取max
return Math.max(u.val+Math.max(fa,0),u.val+Math.max(fb,0)); //注意dfs的返回值是以u为最高点的单边最大路径和,也要注意u节点必须选,
//而左右子树因为可能是负值所以要和0取max,返回的就是两边的最大值,我们可以发现这样写很麻烦,我们可以在fa递归结束赋值给fa的时候直接让0和dfs(u.left)取max再赋值给fa, fb也是同理
即上面的代码我们可以改为:
int fa=Math.max(0,dfs(root.left));
int fb=Math.max(0,dfs(root.right));
res=Math.max(res,fa+fb+u.val);
return Math.max(fa,fb)+u.val;
}
}
543. 二叉树的直径(思路和124一模一样)
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边
的数目表示。
算法分析:
(递归遍历) O(n)
- 递归函数的返回值定义为从当前结点到叶子结点的最大长度,当前结点为空返回
-1
。(注意这里当节点为空时要返回-1,
因为两节点之间的路径长度是以题目之间的**边
**的数目的表示的,当是叶子节点的时候才是0, 当是空节点的时候就是-1,如果是空节点返回0就是错误的) - 递归时,分别得到左右子树递归的返回值,则可以更新答案
ans = max(ans, d1 + d2 + 2)
;然后返回max(d1, d2) + 1
。
时间复杂度
每个结点最多仅被遍历一次,故时间复杂度为 O(n)。
- 二叉树的当前根的直径 = 左子树的深度 + 右子树的深度 (注意这里我们说的是深度,如果写的是左右子树的直径则这里要加2,但是写的是深度)
- 两点直接最长路径不一定经过 root ,需要对所有子树根的结果取 MAX
- dfs 顺序:当前节点深度 = max(left, right)
- dfs 状态:目标节点
注意答案不一定经过根节点,如:
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int res=0; //res记录最大直径,即答案,
int diameterOfBinaryTree(TreeNode* root) {
//树的直径是根加上左右子树的最大值,故而枚举每一个树的根。计算其最大值,时间复杂度是O(n),因为每个节点遍历一次
dfs(root); //从根节点开始递归
return res; //递归结束返回答案
}
int dfs(TreeNode* root){ //递归函数的返回值定义为从当前结点到叶子结点的最大长度,当前结点为空返回 -1。所以如果为叶子节点则深度为0,空则深度-1
if(!root) return -1; 如果为叶子节点则深度为0,空则深度-1,因为叶子节点有两个空节点,故而叶子的深度为-1 + -1 + 2
int left=dfs(root->left),right=dfs(root->right); //求出左右两边的最大值
// 递归时,分别得到左右子树递归的返回值,则可以更新答案 ans = max(ans, left + right + 2);
//左右子树加上根到两子树的两条边
res=max(res,left+right+2); //左右子树加上根到两子树的两条边
// 然后返回 max(left, right) + 1,即是左右两棵子树的最大值再加上跟到这个节点的距离1。
return max(left,right)+1;
}
};
2021年8月11日19:14:39:
/**
* 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 res=Integer.MIN_VALUE; //全局答案,初始化为最小值,全局答案,这里没有什么回溯操作,是递归一点一点更新的,或者是相互之间没有影响
public int diameterOfBinaryTree(TreeNode root) {
dfs(root); //dfs函数可以求出以当前节点为最高点单侧的最长路径,
return res; //递归完之后返回最终答案
}
public int dfs(TreeNode root){ //dfs函数可以求出以当前点为最高单侧的最长路径长度
if(root==null) return 0; //节点为空,长度为0,递归奇数条件不可不写
int left=dfs(root.left),right=dfs(root.right); //left,right分别表示当前节点左儿子为最高节点的单侧的最长路径长度,right是当前结点的右子树为最高节点的单侧的最长路径长度
res=Math.max(res,left+right); //更新全局答案,注意这里就是left+right,不要加2,因为left或者right在定义的时候就包含了从根节点到左/右 儿子的长度了
return Math.max(left,right)+1; //返回的是以当前节点为最高点的单侧路径长度,加一是不是从子树到当前节点的长度要加一
}
}
124.二叉树中的最大路径和, 543 二叉树的直径,687. 最长同值路径 这三个题目的算法类似
2021年10月22日15:13:47:
/**
* 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;
* }
* }
*/
//思路和124题求最大路径和的思路基本上完全相同
//直径是最长的任意两个节点之间的距离,我们还是需要枚举出来所有的路径,这样才可以求出每一个路径的长度,从而更新最长路径长度,
//所以我们还是按照124题求最大路径和那个题目一样,枚举每一个点作为最高点,然后我们求一下穿过这个点的最长直径的长度,
//我们需要求出来从这个点往左边走 最多可以走多远,往右边走 最多可以走多远,两边都取最大值再相加就是整个以这个点u为最高点的最长直径的长度,
//因为左右两边是独立的,我们要想让整个最大,所以我们就应该让左边最大并且右边最大,即还是二叉树的经典模型:一个灯挂俩个灯
//所以这里我们让dfs函数的返回值定义为单边最长路径的长度的最大值,当我们递归求出来之后左右两边相加就是穿过根节点的最长直径,
//我们将每一个点都求一个最大距离,求一个max就是最大值,要注意的一点是路径长度指的是边的数目,不是点的数目
class Solution {
int res=0; //定义全局答案
public int diameterOfBinaryTree(TreeNode root) {
dfs(root); //从根节点开始递归
return res; //最后返回res
}
public int dfs(TreeNode u){ //dfs的返回值是以节点u为最高点的单边最大路径长度,注意不是最大直径
//dfs的返回值是以root节点为最高点的单边最长路径长度,所以l就是往左边走的最大长度,而r是往右边走的最大长度,所以l+r就是以root为最高点的最大路径长度
if(u==null) return 0; //节点是空返回0
int l=dfs(u.left),r=dfs(u.right); //还是同归并排序那里相同,因为我们如果要更新以u为最高点的代表最大路径长度,我们需要用到其左右子树的情况
//在返回dfs的返回值之前先更新res
res=Math.max(res,l+r); //注意res就是l+r,注意不要加2,因为看我们的dfs的定义,我们在计算l,r的时候已经算上根节点到左右子树的距离了
return Math.max(l,r)+1; //注意返回值是以节点u为最高点的单边最大路径长度,就是左边或者最右的最大值再加上到达左右子树的边的长度1
}
}
687. 最长同值路径
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
注意:两个节点之间的路径长度由它们之间的边数表示。
示例 1:
输入:
5
/ \
4 5
/ \ \
1 1 5
输出:
2
示例 2:
输入:
1
/ \
4 5
/ \ \
4 4 5
输出:
2
注意: 给定的二叉树不超过10000个结点。 树的高度不超过1000。
2021年10月22日15:28:12:
/**
* 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;
* }
* }
*/
//首先注意路径是用的边的数目的表示的,不是点的数目,这个题目还是和124,543题目相同,需要枚举出来全部的路径,所以我们需要枚举每一个点作为最高点
//还是需要用到经典模型:一个灯挂俩灯的模型,左右两边是完全独立的,我们要想让整个最长,我们就需要看一下往左边最长有多长,往右边有多长,
//两个相加就是以root为最高点的同值路径最长长度
//注意这个题目是让我们求同值路径最长路径,所以我们一定要保证路径上的点的值是相同的,所以那边的长度就是0,这一点要注意
//我们的dfs的返回值是以节点u为最高点的最长同值单边路径的长度,
class Solution {
int res=0; //定义全局答案
public int longestUnivaluePath(TreeNode root) {
dfs(root);
return res; //递归结束返回res
}
public int dfs(TreeNode u){ //dfs的返回值是以节点u为最高点的单边最大同值路径长度
if(u==null) return 0; //节点是空,我们返回0
//否则节点不空,我们还是先往左右两边递归,我们在更新和返回的时候都需要先用到左右两边的值
int l=dfs(u.left);
int r=dfs(u.right);
//先递归求出左右两个儿子的单边最大同值路径的长度(y总说:左右两边一定要先递归,因为最高点不一定在最高点,有可能是在左右两棵子树里面)
//在返回dfs函数的返回值之前先更新一下res,注意这里要求是同值的路径,所以我们不能直接用l和r更新res,我们应该先来判断一下
if(u.left==null||u.left.val!=u.val) l=0; //如果节点u的左子树不存在,则左边的最长同值路径就是0,或者左子树的值不等于u的值,左边的同值路径也是0
if(u.right==null||u.right.val!=u.val) r=0; //同理右边也是一样,当右子树不存在或者右子树的值不和根节点u相同,右边的最长路径长度r就是0
//这样经过上面的判断之后l,r的值才算最终确定下来,我们就可以更新res了
res=Math.max(res,l+r); //res更新为左右两边的最长同值路径长度之和,注意因为我们在计算l,r的时候已经加上到达左右子树的长度1了,所以这里不要再加2了
return Math.max(l,r)+1; //最后dfs的返回值是左右两边的最长同值路径的长度+1,加一是到达其左右儿子的长度1
}
}
129. 求根到叶子节点数字之和
给定一个二叉树,它的每个结点都存放一个 0-9 的数字,每条从根到叶子节点的路径都代表一个数字。
例如,从根到叶子节点路径 1->2->3 代表数字 123。
计算从根到叶子节点生成的所有数字之和。
说明: 叶子节点是指没有子节点的节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3
输出: 25
解释:
从根到叶子节点路径 1->2 代表数字 12.
从根到叶子节点路径 1->3 代表数字 13.
因此,数字总和 = 12 + 13 = 25.
示例 2:
输入: [4,9,0,5,1]
4
/ \
9 0
/ \
5 1
输出: 1026
解释:
从根到叶子节点路径 4->9->5 代表数字 495.
从根到叶子节点路径 4->9->1 代表数字 491.
从根到叶子节点路径 4->0 代表数字 40.
因此,数字总和 = 495 + 491 + 40 = 1026.
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int ans=0; //定义答案,初始化为0;
int sumNumbers(TreeNode* root) {
if(root) dfs(root,0); //如果根节点不空,我们从根节点开始递归。
return ans; //最后记得将答案返回。
}
void dfs(TreeNode* root,int number){ //从当前节点开始遍历,number为到达当前节点之前记录的值。
number =number*10+root->val; //遍历到当前节点的时候先更新number的值,即让number乘以10再加上当前节点的值。
if(!root->left&&!root->right) ans+=number; //如果当前节点是叶子节点,就在最终答案上加上number
if(root->left) dfs(root->left,number); //如果不是叶子节点且有左子树,我们就对左子树进行递归操作。,左右子树之间没有任何关系,所以我们要分开分别往左右子树递归
if(root->right) dfs(root->right,number); //如果不是叶子节点且有右子树,我们就对右子树进行递归操作。
}
};
2021年8月7日21:55:21:
递归:
深度优先搜索是很直观的做法。从根节点开始,遍历每个节点,如果遇到叶子节点,则将叶子节点对应的数字加到数字之和。如果当前节点不是叶子节点,则计算其子节点对应的数字,然后对子节点递归遍历。
递归:
/**
* 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;
* }
* }
*/
//首先先搞清楚这个题目的题意:每条从根节点到叶节点的路径都代表一个数字,路径 1 -> 2 -> 3 表示数字 123,
//考虑经典的"一圈带两个三角形"的结构,我们这样分析:从根节点到节点u路径数字之和是number,则加上u节点之后就变成了number*10+k(k是u节点的值),
//我们可以这样求出每一个节点的值,如果当前节点是叶子结点的话,就在全局答案res中加上这个number*10+k,
//递归dfs求解这个问题,编写dfs函数,在dfs中计算每一条路径上的数,定义全局答案,将计算出来的每一条路径上的数都加到全局答案上。
class Solution {
int res=0; //定义全局答案,初始化为0,
public int sumNumbers(TreeNode root) {
if(root!=null)dfs(root,0); //从根节点开始递归,注意需要确保根节点非空再去遍历,注意第二个参数是当前路径上的节点之和,
return res; //递归全部结束,将最终的答案进行返回即可
}
public void dfs(TreeNode root,int number){ //第二个参数是当前路径上节点之和,注意number不是全局共享的,所以不用对number进行回溯操作
number=number*10+root.val; //当前节点上的数加到当前路径表示的话上
if(root.left==null&&root.right==null) res+=number; //如果当前节点是叶子节点的话,就把这个数值加到答案上
if(root.left!=null) dfs(root.left,number); //左子树不空,就递归遍历左子树,
if(root.right!=null) dfs(root.right,number); //右子树不空,就递归遍历右子树
}
}
257. 二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
输入:
1
/ \
2 3
\
5
输出: ["1->2->5", "1->3"]
解释: 所有根节点到叶子节点的路径为: 1->2->5, 1->3
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
//很简单的深搜问题:我们从根节点开始遍历,遍历的时候维护一下从根节点到当前节点的路径,如果当前节点是叶子节点,我们就在答案数组中存下来这条路径。
class Solution {
public:
vector<string> ans; //定义答案数组。看输出格式,是字符串格式。
vector<int> path; //path存储从根节点到叶子节点的路径。
vector<string> binaryTreePaths(TreeNode* root) {
if(root) dfs(root); //如果根节点不空,我们从根节点开始递归遍历从根节点到叶子节点的路径。
return ans; //返回答案数组。
}
void dfs(TreeNode* root){ //定义递归函数
path.push_back(root->val); //先把当前点插入到路径数组中。
//需要我们自己组合出来输出格式。
if(!root->left&&!root->right){ //如果当前节点是叶子节点,即找到了一条从根节点到叶子节点的路径。
string line=to_string(path[0]); //现在我们自己组合出输出格式,我们观察输出格式,除了path[0],其他path[i]前都有"->"
for(int i=1;i<path.size();i++){
line+="->"+to_string(path[i]); //组合出来答案格式。
}
ans.push_back(line); //将组合出来的答案插入到答案数组中去。
}else{ //如果当前节点不是叶子节点,则至少有左子树或者右子树中的一个,或者左右子树都有。
if(root->left) dfs(root->left); //如果左子树不空,我们递归判断左子树。
if(root->right) dfs(root->right); //如果右子树不空,我们递归判断右子树
}
path.pop_back(); //回溯之前记得将叶子节点删除。 !!!一定不要忘记写上。
}
};
2021年8月10日10:10:18:
递归 方法一:
/**
* 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;
* }
* }
*/
//"超级简单的dfs问题",我们从根节点开始遍历,遍历的时候维护一下从根节点到当前点的路径,如果当前点是叶子结点就把当前路径存储下来,
//时间复杂度:遍历o(n),存储的时间复杂度是o(n),总的时间复杂度是o(n^2),注意看清楚这个题目的输出格式,这个格式是需要自己弄出来的
class Solution {
List<String> res=new ArrayList<>(); //res记录拼凑出来的路径答案
List<Integer> path=new ArrayList<>(); //path存储当前路径,注意是全局的路径,所以我们需要回溯删除,这一点很重要,不要忘了,注意存储的是值
public List<String> binaryTreePaths(TreeNode root) {
if(root!=null) dfs(root); //首先要确保根节点不空才可以往下递归,如果根节点是空的,则直接不执行if,会返回空res,符合答案
return res; //递归结束,返回存储之后的res答案
}
public void dfs(TreeNode root){ //编写dfs递归函数,注意传入的是当前节点
path.add(root.val); //先把当前节点加到路径中,注意最后记得回溯清空这个节点值,刚进入dfs函数或者说判断是否是叶子节点之前就要把当前节点的值加到path中
//递归结束条件,这样就保证了之后的递归最多递归到叶子节点,递归到叶子结点之后就不会再往下递归
if(root.left==null&&root.right==null){ //如果遍历到了叶子结点,就说明我们找到了一条路径,我们需要自己把路径组合出来
//我们此时已经在path数组列表中已经把当前值加到了path中,我们只需要遍历数组组合出来一个字符串加到res数组答案列表中,注意“”是字符串自己带的,我们不用管,我们要凑出来的是: ->
String line=path.get(0)+""; //line代表当前路径,我们需要自己把答案路径字符串组合出来,line初始化为字符串的第一个数值字符串
for(int i=1;i<path.size();i++){ //注意要从下标1开始拼凑,因为下标0的数值已经被我们初始化的时候使用过了,
line+="->"+path.get(i); //注意加一个+>再拼凑下一个节点值
}
res.add(line); //经过上面的for循环,就可以把当前路径的字符串拼凑出来,我们加到答案数组列表里,注意是line,不是path啊
}else{ //否则当前节点不是叶子节点,我们就需要往下递归遍历,即往这个节点的左右儿子上dfs
if(root.left!=null) dfs(root.left); //如果有左儿子就往左儿子继续往下递归
if(root.right!=null) dfs(root.right); //如果有右儿子就往右儿子继续往下递归
//注意左右儿子可能都有,所以分开写两个if
}
path.remove(path.size()-1); //记得在最后,或者说dfs结束之前要记得回溯删除节点,刚加进来的节点即最后一个节点
}
}
递归二:
class Solution {
List<String> ans = new ArrayList<String>();
void dfs(TreeNode root, String s)
{
if(root.left == null && root.right == null)
{
ans.add(s);
return ;
}
if(root.left != null) dfs(root.left, s + "->" + root.left.val);
if(root.right != null) dfs(root.right, s + "->" + root.right.val);
}
public List<String> binaryTreePaths(TreeNode root) {
if(root == null) return ans;
dfs(root, "" + root.val);
return ans;
}
}
116. 填充每个节点的下一个右侧节点指针
给定一个 完美二叉树(满二叉树) ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
示例:
输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
提示:
树中节点的数量少于 4096
-1000 <= node.val <= 1000
注意: 这个题目首先是一棵满二叉树,其次初始状态下,所有 next 指针都被设置为
NULL
。所以我们只需要改变那些需要改变的next节点即可
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/
//注意本题是满二叉树,每个节点(除了叶子节点)都有左右两个孩子。
//这个题目需要我们对树做出改变,最后返回改变之后的树的根节点root,
class Solution {
public:
Node* connect(Node* root) {
if(!root) return root; //如果是一棵空树,直接结束返回。
auto source_root=root; //用source_root记录下来原头结点,因为下面root节点改变了。
while(root->left){ //如果当前节点有左孩子,即不是叶子节点(这棵树是满二叉树)。
for(auto p=root;p;p=p->next){ //从根节点开始宽搜,结合while条件,利用next指针,宽搜完一层。
p->left->next=p->right; //将其左孩子的next指向其右孩子。
if(p->next) p->right->next=p->next->left; //如果p不是最右边的节点,我们就将p的右孩子的next指向p的next的左孩子节点。
}
root=root->left; //遍历完一层,从下一层开始遍历,将root指向其左孩子节点即可。
//!!!当root在上一层的时候已经把本层的指针全部设置完毕,
//所以对于叶子节点这一层,当root在最后一层非叶子节点的时候,其next指针已经全部设置完毕。
}
return source_root; //返回原根节点。
}
};
2021年8月10日13:32:23:
在每一层的时候把下一层的next连好。 等到了下一层,由于上一层的next已经连好,所以可以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;
}
};
*/
//
//这个题目是完美二叉树即是满二叉树,而下一个题目是普通二叉树,注意题目的空用#表示,这个题目本质是宽搜,
//我们只需要改变指向即可,不是让你写出#,我们改变完指向,返回根节点,最后的#是系统自动加上的
//注意这个题目的二叉树的定义中节点是有next指针的,并且初始状态下,所有节点的next指针都被设置为了null,我们现在要做的是将不应该为null的改一下‘
//我们在宽搜的过程中,给每一个节点赋一下next指针的值,宽搜的时候本来是需要一个队列的,但是由于这个题目有next指针的存在,所以这个题目的队列也是可以省略的
//第一层只有一个节点,之后再遍历每一层的时候,我们可以根据next指针来遍历,先遍历这一层的第一个点,再根据next指针遍历下一个节点,以此类推
//那么我们如何在宽搜的过程中,初始化next指针的值呐?由于这是一棵满二叉树,所以如果一个节点有左儿子节点,则左儿子节点的next指针一定是指向这个节点的右儿子,
//即节点2的左儿子4指向右儿子5,即2.left.next=2.right
//那么这个节点的右儿子的next应该执行哪里呐?应该执行这个节点的下一个节点的左儿子,即2的右儿子5指向3的左儿子6,即2.right.next=2.next.left
//我们需要特判一下是不是最后一层节点,如果是最后一层节点,其是没有左右儿子的,
class Solution {
public Node connect(Node root) {
if(root==null) return root; //如果根节点为空,直接返回空即可
//否则root不空,注意root的next可以不用显示为root.next=null,因为在此时是root的next即是null
Node source=root; //因为下面root节点会发生变化,所以我们在改变指向之前先存下来root节点
while(root.left!=null){ //如果当前节点是有左儿子的,说明当前节点root是有左右儿子的,我们需要更新一下root左右儿子的next指针的指向,
//注意这是横向改变,是不影响下一层的
//我们要从当前节点的第一个节点开始改变下一层的next指针的指向,即root.left这个节点开始
for(Node p=root;p!=null;p=p.next){ //改变的是这一层的下一层的next指针的最小,所以我们要从这一层的第一个节点root开始改变next指向
p.left.next=p.right; //节点p的左儿子的next等于其右儿子
if(p.next!=null){ //再改变p的右儿子的next指针指向之前,要判断一下p是否有next节点,如果有就是指向p.next.left,如果没有就说明p是最右侧结点,其右儿子的next应该指向空
p.right.next=p.next.left;
}else{ //否则就说明p是最后一个节点,将其右儿子的next指向null
p.right.next=null; //这一句可写可不写,因为初始化的时候这个节点就是指向null
}
}
root=root.left; //这样root就走到了下一层的第一个节点,开始下一层的节点的next指针的改变,注意是走到root.left,而不是next注意,往left节点走才是走到下一层,这里也是复用了root节点
}
return source; //最后全部改变完所有节点的next指向,返回根节点即可,注意返回的是source,不是root了
}
}
递归:
递归写真的很简单,
class Solution {
public Node connect(Node root) {
if(root == null || root.left == null)
return root;
root.left.next = root.right;
if(root.next != null){
root.right.next = root.next.left;
}
connect(root.left);
connect(root.right);
return root;
}
}
2021年11月12日16:27:33:
//完美二叉树就是满二叉树,这个题目的本质就是bfs,我们需要在bfs的过程中对每一个节点的next指针赋一下值即可,宽搜的时候我们原本是需要一个队列的
//而这个题目中由于有next指针的存在,而我们可以利用next指针来替换掉队列,我们在遍历节点的时候可以利用next指针来遍历,先遍历第一个点,再利用next指针遍历第二个点,再利用next指针遍历第三个点,...以此类推
//而且我们可以发现每一个节点的左儿子的next就是其右儿子(如节点2的左儿子4的next指向5),即2.left.next=2.right
//而每一个节点的右儿子的next应该指向其next节点的左儿子(如节点2的右儿子5应该指向其next节点即节点3的左儿子6),即2.right.next=2.next.left
//这两点是很重要的两点, 而且每一个节点初始时其next都是null,所以那些之后其next指针应该是null的那些节点我们可以不用管
//注意这个题目是满二叉树
class Solution {
public Node connect(Node root) {
if(root==null) return null; //如果节点为空,我们直接返回null
//注意在下面我们复用了节点root,root被我们复用作为每一层的第一次节点,即样例1中的节点1,2,4
//否则我们就需要对这些节点的next指向做出改变,
Node res=root; //因为下面root节点会被我们复用改变,而我们最后还需要返回root节点,所以我们先用节点存储下来root
while(root.left!=null){ //只要当前节点有左儿子就说明还有下一层,我们就要利用当前这一层更新下一层的情况,即这里是从第二层开始更新的
for(Node p=root;p!=null;p=p.next){ //从本层开始的第一个节点即root节点开始遍历
p.left.next=p.right; //先将其左儿子的next指向其右儿子
//注意在改变其右儿子的next的指向之前,应该先判断一下这个节点的next是否存在,如果不存在的话,比如3的右儿子7,就说明其右儿子是这一层的最后一个节点,其next本来就是null,我们不需要做改变
if(p.next!=null) p.right.next=p.next.left; //当这个节点的next是存在的时候,我们才改变其右儿子的next的指向
}
root=root.left; //当遍历完这一层,root节点就被我们复用为下一层的头结点,即root=root.left(即样例1中的从1走到2,从2走到4)
}
return res; //最后返回的是我们之前存储下来的节点res
}
}
2021年11月12日18:27:59:
bfs即开队列的写法,空间复杂度是O(n)
//这个题目我们也可以开一个队列在bfs的过程中完成next指针指向的改变,用队列将当前层的节点都存到队列中,并记录有len个节点属于当前层
//我们先弹出节点t,再将t指向q.peek()(这一点很重要),最后再将节点t的左右儿子加到队列中,为下一层做准备,注意每一层的最后一个节点需要特殊处理,
//因为每一层的最后一个节点没有q.peek()结点让其可以指向,所以我们只需要最后将最后一个节点从队列中弹出,并将其左右儿子加到队列中即可
//这样做就是我们是一层一层进行处理的,不是为其下一层做准备那样处理的,空间复杂度是O(n)
class Solution {
public Node connect(Node root) {
if(root==null) return null;
Queue<Node> q=new LinkedList<>();
q.add(root);
while(q.size()!=0){
int len=q.size();
for(int i=0;i<len-1;i++){ //注意这里只能到len-2,因为我们要保留每一层的最后一个节点需要特殊处理
Node t=q.poll(); //先取出队头元素
t.next=q.peek(); //将t.next指向q.peek(),这样就实现了将节点指向其下一个节点的目的,这一点处理很巧妙
if(t.left!=null) q.add(t.left); //将节点t的左右儿子加到队列中
if(t.right!=null) q.add(t.right);
}
//特殊处理每一层的最后一个节点
Node t=q.poll();
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
return root;
}
}
117. 填充每个节点的下一个右侧节点指针 II
给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
进阶:
你只能使用常量级额外空间。 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
示例:
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。
提示:
树中的节点数小于 6000
-100 <= node.val <= 100
代码:
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
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) return root; //如果树是空的,直接结束返回即可。
auto cur=root; //最后要返回root,我们这里用cur取代root来遍历一层。从根节点开始,对下一层进行连接操作。
while(cur){ //当当前节点非空
auto head=new Node(-1); //创建下一层的头结点
auto tail=head; //创建下一层的尾结点
for(auto p=cur;p;p=p->next){ //从当前节点开始遍历这一层。第一次是根节点,其for循环进行的操作是将第二层节点连接起来。
if(p->left) {tail->next=p->left;tail=tail->next;} //如果当前节点有左孩子,则将其左孩子插入到链表中,且将tail移动到末尾。
if(p->right){tail->next=p->right;tail=tail->next;} //如果当前节点有右孩子,则将其右孩子插入到链表中,且将tail移动到末尾。
}
cur=head->next; //遍历完这一层,将cur移动到下一层的第一个节点继续对下下一层进行连接操作。下一层的第一个节点就是head->next,我们是通过下一层对下下一层做的操作。
}
return root; //最后将root返回。
}
};
2021年8月10日14:40:38:
/*
// 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;
}
};
*/
//这个题目是普通二叉树,就无法保证非叶子结点的每个节点都有左右儿子节点了,注意:初始状态下,所有 next 指针都被设置为 NULL,所以每一层的最后一个节点的next可以不用管它
//我们每一层建立一个链表,使用头结点和尾结点连接这个链表,即我们在遍历当前层的时候,我们为下一层建立好单链表,
//为下一层建立单链表,需要单链表的头结点和用于往后建立链表的尾结点,这个思路很牛皮,不愧是y总,和前面的单链表算法联系到一块。
class Solution {
public Node connect(Node root) {
if(root==null||(root.left==null&&root.right==null)) return root; //根节点为空或者只有根节点直接返回根节点
Node source=root; //由于最后要返回根节点,并且根节点会被改变,所以我们先记录下来根节点
while(root!=null){ //为当前层的下一层建立单链表
Node head=new Node(-1); //下一层的虚拟头结点
Node tail=head; //下一层的尾结点,用于往后建立单链表
for(Node p=root;p!=null;p=p.next){ //从当前点开始往后遍历,为当前节点这一层的所有节点的所有儿子(即整个下一层)建立链表
//这里是为本层的下一层建立单链表,所以这里要从当前这一个节点开始,即p初始化为root
if(p.left!=null) tail=tail.next=p.left; //如果当前节点有左儿子,把左儿子连接到下一层的单链表上
if(p.right!=null) tail=tail.next=p.right; //同理如果节点有右儿子,把右儿子连接到下一层的单链表上
}
root=head.next; //建好下一层的单链表之后,继续为下一层的下一层建立单链表,下一层的第一个节点是head.next
}
return source; //最后返回原树的根节点,是source,不是root,root已经被我们修改过了
}
}
迭代:
class Solution {
public Node connect_1(Node root) {
if (root == null) {
return null;
}
// 借助队列实现层次遍历
LinkedList<Node> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
while (size-- > 0) {
Node node = queue.remove();
if (size > 0) {
node.next = queue.peek();
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return root;
}
递归:
public Node connect(Node root) {
if (root == null) {
return null;
}
if (root.left != null) {
if (root.right != null) {
// 若右子树不为空,则左子树的 next 即为右子树
root.left.next = root.right;
} else {
// 若右子树为空,则右子树的 next 由根节点的 next 得出
root.left.next = nextNode(root.next);
}
}
if (root.right != null) {
// 右子树的 next 由根节点的 next 得出
root.right.next = nextNode(root.next);
}
// 先确保 root.right 下的节点的已完全连接,因 root.left 下的节点的连接
// 需要 root.left.next 下的节点的信息,若 root.right 下的节点未完全连
// 接(即先对 root.left 递归),则 root.left.next 下的信息链不完整,将
// 返回错误的信息。可能出现的错误情况如下图所示。此时,底层最左边节点将无
// 法获得正确的 next 信息:
// o root
// / \
// root.left o —— o root.right
// / / \
// o —— o o
// / / \
// o o o
connect(root.right);
connect(root.left);
return root;
}
private Node nextNode(Node node) {
while (node != null) {
if (node.left != null) {
return node.left;
}
if (node.right != null) {
return node.right;
}
node = node.next;
}
return null;
}
}
2021年11月12日16:51:59:
//这个题目是一棵普通的二叉树,所以我们就不能按照上一个题目的做法来做,
//我们可以在遍历前一层的时候把下一层的next建立出来,我们需要自己维护一下下一层的单链表,我们先构建下一层的单链表需要知道下一层的头结点,还需要维护建立单链表的尾结点cur节点,
class Solution {
public Node connect(Node root) {
if(root==null) return null; //特判空
Node res=root; //先存下来root节点
while(root!=null){ //当节点不空的时候,我们就需要遍历
Node dummy=new Node(-1); //下一层的虚拟头结点
Node cur=dummy; //cur节点用于往后建立下一层的单链表
for(Node p=root;p!=null;p=p.next){ //遍历这一层,完成下一层节点的next指针的赋值
if(p.left!=null) cur=cur.next=p.left; //如果当前节点有左儿子,我们就要把其左儿子加到下一层单链表的后面
if(p.right!=null) cur=cur.next=p.right; //同理,如果当前节点有右儿子,我们就把其右儿子加到下一层单链表的后面
}
//这样当for循环结束之后,下一层的单链表就建立好了,我们需要到下一层继续建立下下一层的单链表
root=dummy.next; //此时我们就复用root节点,让root去到其下一层的头结点的位置,注意dummy是下一层的虚拟头结点的位置,而dummy.next才是真实的位置
}
return res; //最后要返回原头结点,我们使用res存储了下来
}
}
2021年11月12日18:35:18:
//bfs的写法,即开队列来完成,这里就和上一个题目的写法一模一样了,完全相同
class Solution {
public Node connect(Node root) {
if(root==null) return null;
Queue<Node> q=new LinkedList<>();
q.add(root);
while(q.size()!=0){
int len=q.size();
for(int i=0;i<len-1;i++){
Node t=q.poll();
t.next=q.peek();
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
Node t=q.poll();
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
return root;
}
}
226. 翻转二叉树
翻转一棵二叉树。
示例:
输入:
4
/ \
2 7
/ \ / \
1 3 6 9
输出:
4
/ \
7 2
/ \ / \
9 6 3 1
思路:首先根节点不变,我们交换根节点的两个子节点,交换完子节点之后再分别递归的对根节点的两棵子树进行交换,左右子树之间没有什么关系,分开进行递归。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
//首先根节点不变,我们交换根节点的两个子节点,交换完子节点之后再递归的对根节点的两棵子树进行交换。
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(!root) return NULL; //如果当前节点为空,什么都不用做,
swap(root->left,root->right); //代码到达这里当前节点一定不为空,我们交换左右两个子节点。
invertTree(root->left);
invertTree(root->right);
//全部递归结束,我们要返回根节点
return root;
}
};
2021年8月9日16:25:17:
递归:
其实就是交换一下左右节点,然后再递归的交换左节点,右节点
根据动画图我们可以总结出递归的两个条件如下:
终止条件:当前节点为 null 时返回
交换当前节点的左右节点,再递归的交换当前节点的左节点,递归的交换当前节点的右节点
时间复杂度:每个元素都必须访问一次,所以是 O(n)
时间复杂度分析:每个节点仅被遍历一次,所以时间复杂度是 O(n)。
/**
* 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) {
if(root==null) return null; //根节点为空,返回null
// swap(root.left,root.right); //编写函数交换根节点的两个儿子节点,注意java是值传递,这样写是无法完成两个节点交换的
//可以在下面直接交换两个节点
//交换左右子树,注意注意写外部函数是错误的
TreeNode t=root.left; //交换根节点的两个儿子节点
root.left=root.right;
root.right=t;
invertTree(root.left); //递归对左儿子子树进行翻转
invertTree(root.right); //递归对右儿子子树进行翻转
return root; //最后返回递归交换完的根节点
}
// public void swap(TreeNode p,TreeNode q){
// TreeNode t=p;
// p=q;
// q=t;
// }
}
或者这样写:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(!root) return root;
TreeNode *left = invertTree(root->left);
TreeNode *right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
};
这样理解:
如果当前遍历到的节点root
的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root
为根节点的整棵子树的翻转。
dfs的写法在树很大的时候会栈溢出。
算法2
(栈/队列) O(n)
:
- 和递归一样的思路,用栈和队列来BFS,遍历到的节点交换其左右节点,并把这个节点的左右孩子都加入栈/队列中
- 在这里,先把孩子节点加入栈/队列中和先交换左右孩子的效果是一样的
Java 代码
栈
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
Stack<TreeNode> stk = new Stack<>();
stk.push(root);
while(!stk.isEmpty()){
TreeNode node = stk.pop();
if(node.left != null) stk.push(node.left);
if(node.right != null) stk.push(node.right);
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
}
return root;
}
}
队列
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); //根节点已经特判不空,我们先把根节点加到队列中。
while(!queue.isEmpty()){ //队列不空
TreeNode node = queue.poll(); //取出队头节点
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
TreeNode tmp = node.left; //交换当前节点的左右儿子,这样while结束就可以把所有节点的左右儿子进行交换
node.left = node.right;
node.right = tmp;
}
return root;
}
}
199. 二叉树的右视图
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:
1 <---
/ \
2 3 <---
\ \
5 4 <---
这个题目用宽搜搜一下每一层,把每一层最右边的数记下来,返回即可。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> q; //定义宽搜队列。
vector<int> res; //定义一个答案数组,存的是值。
if(!root) return res; //如果根节点为空,返回空。
q.push(root); //如果根节点不空,我们将根节点插入到队列里面。
while(q.size()){ //当队列不空,
int len=q.size(); //求一下这一层的元素个数。
for(int i=0;i<len;i++){ //每次枚举一下这一层的所有元素。等把len个元素全部枚举完毕就会宽搜下一层。
auto t=q.front(); //取出当前第i个元素(第一次为队头元素)
q.pop(); //将队头元素弹出,以便下一次for循环取下一个元素。
if(t->left!==NULL) q.push(t->left); //如果当前元素有左孩子,将左孩子加入到队列中。
if(t->right!==NULL) q.push(t->right); //如果当前元素有右孩子,将右孩子加入到队列中。
if(i==len-1) res.push_back(t->val); //如果当前节点是这一层最后一个元素(即是右视图这一层的元素)我们将这个值加入到队列中。
}
}
return res; //将答案返回。
}
};
2021年8月10日11:45:45:
宽搜:
/**
* 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;
* }
* }
*/
//注意注意注意不是让我们求二叉树的右链,而是每一次的最右边的那个数,比如样例1中如果没有结点4,则应该是1,3,5,而如果没有4,并且5是2的左子树,则也是1,3,5
//所以这个题目我们只需要进行宽搜,先搜一下每一层,然后把宽搜的每一层的最后一个数记录下来,最后返回即可
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Queue<TreeNode> q=new LinkedList<>(); //定义宽搜的队列,
List<Integer> res=new ArrayList<>(); //定义答案数组
if(root==null) return res; //如果树是空的,我们直接返回空的res数组列表即可
//下面进行经典的宽搜的过程即可
q.offer(root); //否则根节点就是不空的,我们把根节点加到队列中去
while(q.size()!=0){
int len=q.size(); //每次搜的时候先求一下当前这一层元素的个数,因为每次我们要搜一层
while((len--)!=0){ //下面枚举一下当前这一层
TreeNode t=q.peek(); //先取出队头元素
q.poll(); //弹出队头元素
if(t.left!=null) q.offer(t.left); //当前节点有左儿子就把左儿子节点加到队列中
if(t.right!=null) q.offer(t.right); //当前节点有右儿子就把右儿子加到队列中
if(len==0) res.add(t.val); //如果宽搜到了这一层的最后一个节点,我们就把这个节点值加到答案数组列表中,
//注意这里是len==0,判断的时候是len,判断完之后len--,变成了0,这一点一定要注意,可以改为for循环for(int i=0;i<len;i++)最后判断的是i==len-1
}
}
return res; //最后返回答案数组列表即可
}
}
算法二、DFS:
思路: 我们按照 「根结点 -> 右子树 -> 左子树」
的顺序访问,就可以保证每层都是最先访问最右边的节点的。
(与先序遍历 「根结点 -> 左子树 -> 右子树」 正好相反,先序遍历每层最先访问的是最左边的节点)
时间复杂度: O(N),每个节点都访问了 1 次。
空间复杂度: O(N),因为这不是一棵平衡二叉树,二叉树的深度最少是 logN, 最坏的情况下会退化成一条链表,深度就是 N,因此递归时使用的栈空间是 O(N)的。
class Solution {
List<Integer> res = new ArrayList<>();
public List<Integer> rightSideView(TreeNode root) {
dfs(root, 0); // 从根节点开始访问,根节点深度是0
return res;
}
private void dfs(TreeNode root, int depth) {
if (root == null) {
return;
}
// 先访问 当前节点,再递归地访问 右子树 和 左子树。
if (depth == res.size()) { // 如果当前节点所在深度还没有出现在res里,说明在该深度下当前节点是第一个被访问的节点,因此将当前节点加入res中。
res.add(root.val);
}
depth++;
dfs(root.right, depth);
dfs(root.left, depth);
}
}
222. 完全二叉树的节点个数
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h 个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
树中节点的数目范围是[0, 5 * 10^4]
0 <= Node.val <= 5 * 10^4
题目数据保证输入的树是 完全二叉树
算法分析
如图所示,当给定一棵满二叉树时(沿着左边一直走的深度是L
,沿着右边一直走的深度是R
,当L == R
时是一棵满二叉树),可以直接求出满二叉树的节点个数2^L - 1
个节点。
操作:
当当前树是一棵满二叉树时,则直接返回2^L - 1
,否则需要递归求出左儿子的节点个数a
,以及右儿子的节点个数b
,返回总个数a + 1 + b
时间复杂度 O(logn∗logn)
每次判断左右深度的时间复杂度是O(logn),需要判断O(logn)次
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int countNodes(TreeNode* root) {
if(!root) return 0; //如果根节点为空,则返回0
auto l=root->left,r=root->right; //分别对左右子树进行递归。
int x=1,y=1; //看一下左右两边分别有多少层,表示往左走的次数,y表示往右走的次数。
while(l) l=l->left,x++; //记录左边一共多少层
while(r) r=r->right,y++; //记录右边一共多少层
//while循环结束,如果x==y则说明这是一棵满二叉树。
if(x==y) return (1<<x)-1; //(1<<x)代表2的x次方
else return countNodes(root->left)+1+countNodes(root->right); //如果不是一棵满二叉树,则递归地计算左右两棵子树,还要加上根节点本身数量1.
}
};
2021年8月11日14:25:30:
(递归) O(logn∗logn)
二叉树一般都可以考虑成递归问题
,对于每个节点,计算一直向左和一直向右的高度,如果相等则说明是满二叉树,那么该树节点个数为2^h−1
(h是二叉树的高度);如果不相等则说明不是满二叉树,则需要递归地向下求解,该棵树的节点个数等于1+左子树节点数+右子树节点数
。其实这和一般的二叉树计算节点个数的思路是一样的,不同点在于这是完全二叉树,所以如果一直向左和一直向右的高度相等的话就可以马上计算出节点个数而不用继续向下递归了。
这道题相当于二分查找最后一层最后一个节点的位置,每次查找的复杂度是O(logn)
,一共需要查找O(logn)
次,所以复杂度为O(logn∗logn)
。
/**
* 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;
* }
* }
*/
//如果是满二叉树的话,如果有n层则节点个数就是2^n-1个节点,而完全二叉树由于我们不知道最后一层节点有多少个所以比较麻烦
//首先对这棵树做一下宽搜就可以求出来节点的个数了,时间:O(n), 但是我们可以使用二分法求出最后一层节点的个数,时间降到O(logn),
//我们先判断一下左链有多高,右链有多高,只有一样高的时候才说明这是一棵满二叉树,直接用公式算,否则就是一棵完全二叉树,但是我们可以继续对根节点的两个儿子继续递归
//对左儿子递归,看一下左儿子的左链和右链是否一样高,如果一样高也可以直接应公式算,右儿子同理也一样可以这样判断,即左右儿子均是满二叉树
//如果有一个儿子不是满二叉树(注意最多有一个儿子不是满二叉树),就继续往一边子树递归,时间复杂度是O(logn)
class Solution {
public int countNodes(TreeNode root) {
if(root==null) return 0; //如果根节点为空,节点个数就是0,这也是递归结束条件,
TreeNode l=root.left,r=root.right; //分别记录下来根节点的左右儿子,
// 如果初始化 int x = 0, y = 0; 后面对应的就是 (2 << x) - 1
int x=1,y=1; //分别记录左右链的长度,注意这里初始化为1,而不是0,
while(l!=null) { //如果左子树不空,就一直往左链走
l=l.left;
x++; //每往下走一次,长度加一
}
while(r!=null){ //如果右子树不空,就一直往右链走
r=r.right;
y++; //每往下走一次,右链长度加一
}
if(x==y) return(1<<x)-1; //如果两边长度相同,节点个数就是2^n-1个,// 注意要加(), 加减号 的 优先级 大于 左移右移运算符
//否则就是左右有一棵子树不是满二叉树,则我们就需要递归到左右子树进行计算,
return countNodes(root.left)+1+countNodes(root.right); //分别加上递归计算出来的左子树的节点个数加上节点的个数1再加递归计算出来的右子树的节点个数
//还要注意传递的参数不是l和r,因为经过上面的while循环,l和r均已经被改变了,我们传递的应该是没有改变的root.left和root.right
}
}
每次递归 只有 不是满二叉树的子树 会继续递归下去,就相当于 二分,每次 只有一半会继续递归。
不是满二叉树的那边,在整个递归过程中,一直是 一个子树是满二叉树,一个子树不是满二叉树,然后不是满二叉树的子树继续递归,
最后 递归结束的情况 是 左孩子是 只有一个结点的满二叉树,右孩子是空树
2021年11月12日15:29:21:
/**
- 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 {
//注意我们可以直接遍历树来统计节点,随便使用前中后序遍历中的一种即可,但是这样时间是O(n)的,题目要求我们时间更快一些
//注意题目中是完全二叉树,不是满二叉树,对于满二叉树,如果有n层,其节点个数等于2^n-1,对于完全二叉树,其最后一层节点都集中于左侧
//其实我们需要找到最后一层的分界点的位置,而且我们知道分界点左侧树的高度为n,分界点右侧树的高度为n-1
//这个题目需要用到满二叉树的性质,那么什么情况下一棵树是一棵满二叉树呐?只要其最左侧结点和最右侧结点的深度是相同的其就是一棵满二叉树
//我们先判断一下当前树是不是一棵满二叉树,如果是的话直接返回(因为此时我们可以直接使用满二叉树的节点计算公式:2^n-1计算出来节点的个数)
//如果不是的话,就有三种情况:1.左右两边都是一棵满二叉树,只不过左边高度比右边多一,此时我们也可以直接返回(因为左右两边都分别使用满二叉树的计算公式进行计算相加即可)
//2.左边是满二叉树,而右边不是满二叉树(如样例1,此时分界点就在右子树),此时左边可以直接算,而此时我们只需要再递归一下右边即可(直到找到分界点,此时也是一棵满二叉树,只有一个节点也是满二叉树)
//3.同理,左边不是满二叉树,而右边是满二叉树(即分界点在左边),此时我们再递归计算左边即可
//整个过程其实类似于一个二分的过程,因为每次我们都大约少计算一半的节点(只往一边递归),
//我们最多和二分logn次,而每次二分在判断的时候,都要往其左右两边走,一直走到底,所以每次在递归判断的时候都会递归logn次,总时间就是logn*logn
//2^n-1就是(1<<n)-1,
public int countNodes(TreeNode root) {
if(root==null) return 0; //节点为空,节点数量是0
TreeNode l=root.left,r=root.right; //l,r分别是根节点的左右子树
int x=1,y=1; //x,y分别统计左右两边分别递归多少层,根节点就是第一层,所以x,y都初始化为1
while(l!=null) { //当左子树不空的时候,我们就往左子树走,并且左边的层数加一
l=l.left; //一直往左子树走
x++; //左子树的层数加一
}
while(r!=null){ //同理统计右边数的层数
r=r.right; //往右边走
y++; //右子树的层数加一
}
if(x==y) return (1<<x)-1; //如果左右两边的层数相同,就说明当前树是一棵满二叉树,我们返回其节点的数量(2^n-1)
//否则(即x!=y)至少有一边不是满二叉树,我们就需要递归到左右两边取计算
return countNodes(root.left)+countNodes(root.right)+1; //此时我们就需要分别往左右两边递归计算出来左子树的数量和右子树的数量再加上根节点的数量1
//最后的总的,三者之和的结果就是当前树的节点的总数量我们返回
}
}
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
树中节点数目在范围 [2, 10^5] 内。
-10^9 <= Node.val <= 10^9
所有 Node.val 互不相同 。
p != q
p 和 q 均存在于给定的二叉树中。
算法一:
和求二叉搜索树的思路类似:
直接拿本身这个函数进行递归,本身这个函数的含义是在root这棵树找到p和q的最近公共祖先
1、若当前节点
root == p
,则q
点一定在root
的左右子树其中一处,则最近的公共结点肯定是root
2、若当前节点root ==q
,则表示p
点一定在root
的左右子树其中一处,则最近的公共结点肯定是root
3、若1和2情况都不是,则p和q的最近公共祖先要么在root的左子树,要么在root的右子树
,则直接递归到root.left和root.right进行搜索
,若递归完后,左子树返回null表示没找到,那答案肯定是在右子树,同理,右子树返回null表示没找到,那答案肯定是在左子树
4、若3情况中左右子树都找不到p和q的最近公共祖先,则表示p点和q点分别在不同的左右子树,则root就是他们的最近公共祖先
时间复杂度O(n)
代码:
/**
* 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||root==p||root==q) return root;
TreeNode l=lowestCommonAncestor(root.left,p,q);
TreeNode r=lowestCommonAncestor(root.right,p,q);
if(l==null) return r;
if(r==null) return l;
else return root;
}
}
算法二:
这个题目和二叉搜索树的不同在于我们不能通过比较和根节点权值的关系判断出来点是在左子树还是右子树。而二叉搜索树可以这样做。
这样的话我们必须把左右两棵子树全部遍历一遍才可以知道那棵子树有p节点,那棵子树有q节点,这样我们我们都需要把左右两棵子树全部遍历一遍最坏情况下时间复杂度为O(n).
dfs(root, p, q
):表示从root根结点往下找是否找到p点和q点,state表示找到p点和q点的二进制状态
11
表示p点和q点都能找到
10
表示只能找到q点
01
表示只能找到p点
00
表示两个点都找不到
当前点root找到p点和q点的状态主要依赖如下几种情况1、
dfs(root.left, p, q)
2、dfs(root.right, p, q)
3、当前点root == p
4、当前点root == q
找到最近能同时找到p点和q点的结点,即最早出现state = 3
的结点时间复杂度
O(n)
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* ans=NULL; //定义一个全局遍历,一开始为空,表示还没有找到最近公共祖先。
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root,p,q); //从根节点开始递归搜索
return ans; //将答案返回;
}
int dfs(TreeNode* root,TreeNode* p,TreeNode* q){
if(!root) return 0; //如果根节点为空,则表示p,q都没有在这个子树上,返回0即可,
int state=dfs(root->left,p,q); //state记录状态,只有当state=11即3的时候才表示找到了p,q的最近公共祖先。左右两边都需要递归,我们先从左边开始递归。
if(root==p)state|=1; //如果根节点是p,则将state个位赋值为1,
else if(root==q) state|=2; //如果根节点是q,则将state十位赋值为1,
state|=dfs(root->right,p,q); //再去右子树递归
if(state==3&&ans==NULL) ans=root; //如果state变为了3,且ans没有被赋值过,则说明当前子树是第一个包含p,q的子树,则当前根节点就是p,q的最近公共祖先。
return state; //每次递归将答案返回。
}
};
2021年8月9日19:15:27:
算法分析:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//注意这个题目是一棵普通的树,就不能利用值来判断到底是在左子树还是右子树,所以没有办法,我们只能往当前节点的左右两边子树都找一下,看有没有p和q,
//如果我们同时第一次在当前节点的左右子树里同时找到了p,q,那么当前节点就是p,q的LCA,而如果不是第一次搜到就说明当前节点不是LCA,而只是一个祖先,不是最近的
//看上面小呆呆大佬的算法分析:直接拿本身这个函数递归,本身这个函数的定义就是在以root为根节点的树中找p,q的LCA,可以分为以下四种情况:
//1.若root=p,LCA是p,2.若root=q,LCA是q,3.若1,2两种情况均不是,则p和q的LCA要么在root的左子树,要么在root的右子树,则直接递归到root.left和root.right进行搜索,
//若递归完之后,左子树返回null则表示没有找到,那答案肯定是在右子树,同理,右子树返回null,表示没有找到,那答案肯定是在右子树,
//为什么说:左子树返回null则表示没有找到,那答案肯定是在右子树 呐?因为如果左子树left返回null,表示左子树p,q均不包含,那么右子树有以下几种情况:
//①.右边也都不包含p,q,则right=null,最终返回null,②.右边只包含p或q,则返回p或q,③.右边同时包含p和q,则right是LCA,我们最终也需要返回LCA,
//总和以上三种情况,如果左边返回结果null,则我们返回right,如果右边返回null,则我们返回left,否则左右两边均不空,说明p,q一个在左边,一个在右边,所以返回root
//4.若3种情况中左右子树都找不到p,q的LCA,则表示p,q点分别在不同的左右子树,则root就是他们的LCA,
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// LCA 问题
if (root == null) { //如果根节点为空,直接返回null, 空节点不可能是LCA
return root;
}
if (root == p || root == q) { //如果p,q中有一个是根节点,则LCA就是root
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q); //否则就分别往左右子树递归
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) { //即表示左右子树中均有LCA,则答案一定是root节点
return root;
} else if (left != null) { //如果左边不是空,就表示LCA在左边,返回左边的结果
return left;
} else if (right != null) { //如果右边递归结果不是空,就表示LCA在右边,返回右边的结果
return right;
}
return null;
}
}
437. 路径总和 III
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
示例:
root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8
10
/ \
5 -3
/ \ \
3 2 11
/ \ \
3 -2 1
返回 3。和等于 8 的路径有:
1. 5 -> 3
2. 5 -> 2 -> 1
3. -3 -> 11
共3条。
算法分析:
前缀和,类似于560题,560是一维的,这是把一维放到了一棵树上,
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
unordered_map<int,int> hash; //哈希表hash记录前缀和及其对应个数
int res=0; //res记录答案
int sum; //把sum作为全局变量,在dfs函数中就不用再传递sum了
int pathSum(TreeNode* root, int _sum) {
hash[0]=1;//最一开始的时候要有一个哨兵,s[0]=0,所以对于hash表就有hash[0]=1;
sum=_sum;
dfs(root,0); //从根节点开始遍历,第二个参数0,表示当前和是0
return res; //dfs结束之后返回答案
}
void dfs(TreeNode* root,int cur){
if(!root) return; //如果节点为空,直接返回空即可
//否则节点不为空
cur+=root->val; //当前值就要加上根节点的值
res+=hash[cur - sum]; //答案就要加上si-t的个数
hash[cur]++; //当前总和加到哈希表中
//dfs下一层,即向这个节点的左右孩子进行递归,因为不能出现折线,所以我们分别往左右子树进行递归
dfs(root->left,cur);
dfs(root->right,cur);
//递归结束,回溯的时候,将当前总和删掉。
hash[cur]--; //记得回溯
}
};
2021年8月28日10:13:15:
/**
* 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 {
Map<Integer,Integer> map=new HashMap<>(); //定义哈希表
int res=0; //res是全局答案,记录等于target的路径和有多少条
public int pathSum(TreeNode root, int target) {
map.put(0,1); //哨兵,用来处理一些特殊情况,或者说边界情况,少了就错了。
dfs(root,target,0); //从根节点开始往下递归,target即是我们的目标值,当前节点的路径前缀和sum是0
return res; //最后返回res
}
public void dfs(TreeNode root,int target,int sum){ //编写外部dfs函数,target是我们要找的目标值,sum是当前节点的前缀和
if(root==null) return; //如果根节点为空,直接结束,这也是递归结束条件,这里的return;不可少,否则递归将不会结束
sum+=root.val; //否则sum就要加上当前节点的值,即到达当前节点的前缀和sum=sum+当前节点的权值,这样更新之后的sum就是到达当前节点的前缀和
if(map.containsKey(sum-target)) res+=map.get(sum-target); //即从j到i的和正好是target, 我们就在res上加上对应前缀和的数量,注意java和C++对哈希表的操作不同
//注意先判空,因为map中可能没有前缀和等于sum-target,否则就会空指针异常
//首先我们要符合前缀和的定义,即是我们再加入当前前缀和之前要先判断是否有前缀和等于sum-target
map.put(sum,map.getOrDefault(sum,0)+1); //判断完之后将当前节点的前缀和加到哈希表中
dfs(root.left,target,sum); //之后分别往当前节点的左右子树进行递归,sum上面已经更新过了,所以这里在往下递归的时候,直接用就可以了,不用再更新sum了
dfs(root.right,target,sum); //分别往左右子树递归,这样就可以保证不会出现折线的情况,都是从上往下的路径
map.put(sum,map.getOrDefault(sum,0)-1); //往左右子树递归完之后,记得回溯,即是将当前节点的前缀和从哈希表中删除,即sum的个数减一
//这里之所以要在这里写回溯,而不是在两个dfs上面,是因为我们在往左右子树分别递归的时候,前缀和是sum,而不是回溯之后的sum,所以我们要将回溯放到最后
}
}
算法二:双重递归:
算法思想
- 搜索顺序:枚举每个起点,树中的每个点都可以作为根节点
- 终止条件:遍历到叶节点,在中间只要是出现sum为0的就记录
pathsum
枚举的起点,dfs
是对路径的搜索。pathsum
的本质是前序遍历- 在
dfs
中只要中间路径出现sum
为0
就需要记录,这是因为一条较长的路径可能包含一条较短的路径
class Solution {
int dfs(TreeNode root,int sum){
if(root==null) return 0;
if(root.left==null && root.right==null) return sum==root.val?1:0;
//中间sum为0的地方就记录下来
int cnt=sum==root.val?1:0;
return dfs(root.left,sum-root.val)+dfs(root.right,sum-root.val)+cnt;
}
public int pathSum(TreeNode root, int sum) {
if(root==null) return 0;
return pathSum(root.left,sum)+pathSum(root.right,sum)+dfs(root,sum);
}
}
2021年11月11日13:40:15:
/**
* 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;
* }
* }
*/
//题目要求我们不能出现折线,只能是从上往下的,不一定从根节点出发
//思路就是一维前缀和放到了树中,思路和560题相同,使用哈希表存储si-t的个数,再累加一下,在dfs的过程中维护前缀和,注意回溯
class Solution {
Map<Integer,Integer> map=new HashMap<>(); //哈希表map存储si-t值的个数
int res=0; //res记录总的答案数量
public int pathSum(TreeNode root, int t) {
if(root==null) return 0;
map.put(0,1); //边界情况,s[0]=1,这个哨兵很重要
dfs(root,t,0); //当前需要凑出来的总和是t,当前路径总和是0
return res;
}
public void dfs(TreeNode root,int t,int cur){ //编写dfs函数,当前需要凑的总和是t,当前总和是cur
if(root==null) return; //遍历到空节点,直接返回
cur+=root.val; ///同样要先遍历这个点再做判断,这里的遍历就是让cur加上节点的值
if(map.containsKey(cur-t)){ //之后再判断哈希表中是否已经有cur-t
int cnt=map.get(cur-t); //cur就是我们的s[i]
res+=cnt;
}
map.put(cur,map.getOrDefault(cur,0)+1); //将cur的个数加一
dfs(root.left,t,cur); //递归一下左右儿子
dfs(root.right,t,cur);
map.put(cur,map.getOrDefault(cur,0)-1); //递归结束回溯的时候,将cur的个数减一
}
}
297. 二叉树的序列化与反序列化(前序遍历)
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串****并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
示例 1:
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[1,2]
提示:
树中结点数在范围 [0, 10^4] 内
-1000 <= Node.val <= 1000
算法分析:
先序遍历
这题要实现两个函数,一个是把一个二叉树变成一个字符串,另一个是要把字符串变成一棵二叉树。
这里是用基于深搜的方式,用先序遍历来把二叉树变成字符串,对于空结点null存成#。
这里一定要把空结点也存下来,如果不存空结点的话,那么只有前序遍历是没有办法唯一确定一棵二叉树的(还要给出中序遍历才能唯一确定二叉树),也就没有办法实现反序列化。但是如果我们记录空节点的话,就可以通过空节点和前序遍历确定出来这棵二叉树。
- 我们按照先序遍历,唯一的序列化一棵二叉树。之前学过,要唯一确定一颗二叉树,光有先序是不够的,要么先序+中序确定,要么后续+中序。
- 本题为啥先序就可以确定一颗二叉树,因为题目给了空结点的位置顺序所以能确定。
- 例如样例中的二叉树可以表示为 1,2,#,#,3,4,#,#,5,#,#
- 反序列化,遍历序列化后的字符串,遇到#, 指针跳过#,两个,并且返回空节点。剩下的场景一定是数字,维护好数字区间,通过substring函数找出所有数字,做成一个root节点,dfs左右子树,最后return root。
时间复杂度:(先序遍历序列化) O(n)
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
public:
string path; //path记录二叉树序列化为字符串之后的结果,并且要记录空节点,并在两个节点之间加一个","。
// Encodes a tree to a single string.
string serialize(TreeNode* root) { //设计实现序列化过程
dfs_s(root); //从根节点开始序列化
return path; //最后过程全部结束返回path
}
void dfs_s(TreeNode* root){
if(!root) path+="#,"; //如果根节点为空节点,将这个点赋值为#,并记得在所有点之间记得加,
else{ //如果节点不空,就按照递归的方式先遍历根节点,再递归遍历左子树,再递归遍历右子树
// 先去遍历根结点
path+=to_string(root->val)+","; //先遍历根节点
// 遍历左右子树
dfs_s(root->left); //递归遍历左子树
dfs_s(root->right); //递归遍历右子树
}
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) { //设计实现反序列化,即将带有空节点("#")的先序遍历的字符串还原为原本的二叉树
int u=0; //u代表字符串的下标,从下标0开始遍历
// 从0这个位置开始遍历,这里u要传引用,因为在计算的过程中u是一直往前走的,递归到的位置对这一层来说还是有用
return dfs_des(data,u); //从下标0开始还原这棵二叉树
}
//& 在参数里是引用,可以看做全局变量
TreeNode* dfs_des(string& data,int& u){ //编写反序列化函数,注意这里都要传引用,第一个传引用是为了避免复制data这个数据,否则时间复杂度将是n^2,第二个是为了确保使用同一个u
// 如果当前位置是#号就代表之前序列化的时候这个点是空节点就返回空,并且注意在返回空之前,我们要先将u往后移动两位(即跳过#和,)。
//题目已经保证序列化的字符串就是一颗合法的树,按照规则反序列化不会出问题,即这里不需要写到达边界的判断。
if(data[u]=='#'){
u+=2;
return NULL;
}
// 如果当前位置不是空,因为我们序列化是通过先序遍历实现的,所以不是空的第一个位置就是根节点
else{
int k=u; //记录根节点的下标
while(data[u]!=',') u++; //让u走到,的位置
auto root=new TreeNode(stoi(data.substr(k,u-k)));
//求完根结点的值要把u往后移动一位跳过逗号
u++;
//递归计算左子树
//左子树计算完退栈到这一层的时候,u的位置就已经摆好了,这也是我们之所以对u传引用的原因
root->left=dfs_des(data,u);
// 计算右子树
root->right=dfs_des(data,u);
// 最后返回这个子树
return root; //最后返回根节点即可。
}
}
};
// Your Codec object will be instantiated and called as such:
// Codec ser, deser;
// TreeNode* ans = deser.deserialize(ser.serialize(root));
2021年8月11日16:22:11:
递归:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//这个题目序列化的时候我们使用先序遍历的方式(NLR),深搜遍历这棵树,注意当遍历到空节点的时候用#存下来空节点,并且所有的节点之间添加,号以示节点区分,
//比如样例1中的序列化的结果就是1,2,#,#,3,4,#,#,5,#,#
//反序列化:即复原这棵二叉树,我们如果只是知道先序遍历是无法复原这棵二叉树的,但是我们通过添加的#号就可以复原这棵二叉树了,
//比如通过:1,2,#,#,3,4,#,#,5,#,#,我们就可以复原出来样例1的二叉树,过程:首先1是根节点,之后遍历根节点的左子树发现是2,所以2是1的左子树
//之后遍历2的左子树,发现是#,即2的左子树为空,这样就就该遍历2的右子树,发现也是#,即空,2遍历完了,回到1,该遍历1的右子树发现是3,所以3是1的右子树
//之后遍历3的左子树,是4,遍历4的左子树,发现是#,即是空,遍历4的右子树,也是#,所以4遍历完了,回到3,遍历3的右子树是5,再遍历5的左子树,是#
//遍历5的右子树,是#,所以5遍历完了,回到3,3也遍历完了,回到1,1也遍历完了,整个树也复原即反序列化成功了
public class Codec {
String path=""; //定义序列化即前序遍历的答案字符串
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
dfs_s(root); //从根节点开始前序递归遍历
return path; //遍历完返回答案字符串
}
public void dfs_s(TreeNode root){ //编写序列化的递归函数,即前序遍历的序列,不单单是简单的前序序列,注意要加上#或者,
if(root==null) path+="#,"; //如果是空节点,路径就加上#,即是用#替代空
else{ //否则就表示空节点,我们在两个节点之间是用,分隔的,所以我们要在节点值之后加上,
path+=root.val+","; //先遍历当前节点,即在答案字符串上加上节点值和用于区分的,别忘了加根节点值或者忘记加上,号
dfs_s(root.left); //先往左子树递归
dfs_s(root.right); //再往右子树递归
}
}
int u; //u表示当前反序列化或者说还原到了哪一位,u是共用一个,所以要定义为全局的
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
u=0; //u初始化为0
return dfs_d(data); //开始递归反序列化还原二叉树
}
public TreeNode dfs_d(String data){ //编写反序列化递归函数,注意在dfs函数中一定不要传递全局变量u,否则u就只是局部的,不是全局共用的
if(data.charAt(u)=='#'){ //如果当前位置是#,表示当前位置是null,即空节点,返回null
u+=2; //在返回空之前,我们要让u往后跳两个字符,即跳过#和,跳到下一个数字位置
return null;
}
//否则的话,当前位置的数就是节点上的数,注意由于我们序列化得到的是一个字符串,所以当前节点的值可能不止一位,比如是39,我们要遍历字符串找出来这个数
else{
//使用双指针找出来当前节点的数值
int k=u;
while(data.charAt(u)!=',') u++; //最后u就到达了,号,并且[k,u)之间的数字就是节点值
int n=Integer.valueOf(data.substring(k,u)); //求出节点的值
TreeNode root=new TreeNode(n); //构造出这个节点
u++; //注意u要往后跳一位,跳过,号
//递归计算左子树,因为先序遍历的顺序是NLR,所以当我们构造完根节点之后就要递归构造左子树,之后是右子树
root.left=dfs_d(data); //递归构造根节点的左子树
root.right=dfs_d(data); //递归构造根节点的右子树
return root; //将构造好之后的根节点进行返回
}
}
}
// Your Codec object will be instantiated and called as such:
// Codec ser = new Codec();
// Codec deser = new Codec();
// TreeNode ans = deser.deserialize(ser.serialize(root));
331. 验证二叉树的前序序列化
序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #
。
_9_
/ \
3 2
/ \ / \
4 1 # 6
/ \ / \ / \
# # # # # #
例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#"
,其中 #
代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
每个以逗号分隔的字符或为一个整数或为一个表示 null
指针的 '#'
。
你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3"
。
示例 1:
输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true
示例 2:
输入: "1,#"
输出: false
示例 3:
输入: "9,#,#,1"
输出: false
2021年8月11日17:03:09:
class Solution {
//这个题目的前序序列和297题我们写的序列化的方式相同,那么我们该如何判断这个序列是否正确呐?我们知道297题序列化的方式可以确定唯一的一个中序遍历字符串
//我们只需要模拟一遍,如果模拟过程中没有出现问题就说明是正确的,否则就是不正确的,
int k=0; //k是代表当前遍历到了字符串的哪一位
String s; //把字符串定义为全局的
public boolean isValidSerialization(String _s) {
s=_s+','; //把_s赋给全局字符串s,并且为了方便我们判断,我们规定每一个数都是以,结尾的,所以我们要给s最后补一个,号
if(dfs()==false) return false; //执行dfs函数,如果结果返回来false,就说明是不合法的,我们返回false
return k==s.length(); //否则也不一定合法,看一下字符串是否正好被用完
}
public boolean dfs(){
if(k==s.length()) return false; //k最大应该是n-1,结果这里达到了n,肯定是不合法的
if(s.charAt(k)=='#'){
k+=2;
return true;
}
//否则就需要找出来当前数字
while(s.charAt(k)!=',') k++; //找出来当前节点数字
k++; //跳过,号,遍历完根节点,没问题之后遍历左子树和右子树
return dfs()&& dfs(); //这样就可以递归判断左子树是否正确并且右子树是否正确,只有左右子树均正确才是正确的
}
}
2021年11月12日14:52:33:
class Solution {
//总体思路和反序列化一棵二叉树的过程是类似的,注意这个题目不要我们构造出这棵树,只需要假装构建即可,即模拟一下反序列化的过程,看是否有矛盾即可
//这个题目的细节很多,需要好好掌握理解
int u=0; //u是用于遍历字符串的全局下标
String s;
int n;
char[] c;
public boolean isValidSerialization(String preorder) {
s=preorder+","; //注意这个题目给我们的字符串的最后没有',',而我们判断的','作为每一个节点的结束,这里是为了统一处理(其实是最后一个'#')
n=s.length();
c=s.toCharArray();
if(!dfs()) return false; //如果在模拟构造的过程中出现了错误,就表示这不是一个合法的由二叉树前序遍历得到的字符串,我们返回false
//注意最后还要看一下字符串是否有多余的元素,如果没有多余的元素才说明是合法的
return u==n; //最后上面没有false,也不是上面就是正确的,因为可能字符串还没有被遍历结束,我们还要判断一下下标u是否到达了n,如果没有的话,就说明字符串太长了,也要返回false
}
public boolean dfs(){ //模拟反序列化即构成二叉树的过程,看中间过程中是否有矛盾
if(u==n) return false; //注意这里我们在递归往下构造节点的时候,字符串上应该是有东西的,但是这里u等于了n,就说明字符串上没有东西了,我们就返回false(即不是一个合法的字符串)
if(c[u]=='#'){ //和反序列化一棵二叉树那里的处理相同,这里如果字符是'#',说明是一个空节点,我们就跳过'#'和','
u+=2; //跳过'#'和','
return true; //注意这里还要返回true,表示当前这一边(左子树或者右子树)已经遍历构造完了,要切换到另一边了,所以这里的return true表示终结/结束,这属于是一个base case
}
//否则当前就是一个数字,我们要找出来这个数字
while(c[u]!=',') u++; //注意这里不是要真正的找出来这个数,而是只是模拟一下,其实就是把这个数找出来跳过即可
u++; //这一步的u++是为了跳过','
return dfs()&&dfs(); //之后再递归遍历构造左子树和右子树即可,两边必须要都没有问题才可以返回true,所以这里用了&&连接
//这里其实用到了前序遍历的特性:右子树的开始索引在左子树的结束索引的后面
}
}
513. 找树左下角的值
2021年10月29日19:27:31:
/**
* 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;
* }
* }
*/
//经典的bfs的题目,通过bfs依次将每一层的元素搜出来,对于每一层的节点来说,存在左儿子就将左儿子加到队列中,存在右儿子就将右儿子加到队列中
//
class Solution {
Queue<TreeNode> q=new LinkedList<>(); //定义宽搜用到的队列
int res; //res记录的是当前枚举到每一层的第一个元素,当bfs到最后一层的时候,res记录的就是最后一层的第一个元素
public int findBottomLeftValue(TreeNode root) {
bfs(root); //从根节点开始bfs
return res; //最后bfs结束的时候返回答案
}
public void bfs(TreeNode root){
q.add(root); //先将根节点加到队列中
while(q.size()!=0){
int length=q.size(); //length记录每一层的节点数量
res=q.peek().val; //res每次while迭代搜索每一行的时候记录的都是当前这一层的第一个元素的值,最后res就会被改为最后一层的第一个节点的值了
//下面就是经典的层序遍历二叉树的代码了
while(length--!=0){
TreeNode t=q.poll();
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
}
}
}
515. 在每个树行中找最大值
2021年10月29日19:28:31:
/**
* 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;
* }
* }
*/
//经典的bfs的题目:通过bfs搜索出来每一层的元素,在每一层中都比较出最大值即为当前层的最大值,同时将该层的所有节点的左右儿子都加到队列中,以遍历下一层
class Solution {
List<Integer> res=new ArrayList<>();
Queue<TreeNode> q=new LinkedList<>(); //bfs要用的队列
public List<Integer> largestValues(TreeNode root) {
if(root==null) return res; //判空
bfs(root); //否则就从根节点开始bfs
return res; //最后返回答案res
}
public void bfs(TreeNode root){
q.add(root); //先将root加到队列中
while(q.size()!=0){
int length=q.size(); //求出这一层的节点个数
int maxV=q.peek().val; //maxV记录每一层的最大值
while(length--!=0){
TreeNode t=q.poll();
maxV=Math.max(maxV,t.val); //更新当前层的最大值
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
res.add(maxV); //遍历完当前这一层之后,将这一层的最大值加到res中
}
}
}
559. N 叉树的最大深度
2022年1月2日16:22:37:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public int maxDepth(Node root) {
if(root==null) return 0;
int res=1;
for(Node n:root.children) res=Math.max(res,maxDepth(n)+1);
return res;
}
}
563. 二叉树的坡度
2022年1月2日16:33:53:
//和二叉树转链表那个题目很像,及其用到了124题二叉树的最大路径和那个题目
class Solution {
int res=0;
public int findTilt(TreeNode root) {
if(root==null) return 0;
dfs(root);
return res;
}
public int dfs(TreeNode root){
if(root==null) return 0;
if(root.left==null&&root.right==null) return root.val;
if(root.left!=null&&root.right!=null){
int l=dfs(root.left),r=dfs(root.right);
res+=Math.abs(r-l);
return l+r+root.val;
}
if(root.left!=null){
int l=dfs(root.left);
res+=Math.abs(l);
return l+root.val;
}
int r=dfs(root.right);
res+=Math.abs(r);
return r+root.val;
}
}
简易版:
//简易版题解
class Solution {
int res=0;
public int findTilt(TreeNode root) {
dfs(root);
return res;
}
public int dfs(TreeNode root){ //dfs的返回值是当前节点的权值之和(包括左子树,右子树和当前结点)
if(root==null) return 0;
int l=dfs(root.left),r=dfs(root.right);
res+=Math.abs(r-l);
return l+r+root.val;
}
}
617. 合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
输入:
Tree 1 Tree 2
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出:
合并后的树:
3
/ \
4 5
/ \ \
5 4 7
注意: 合并必须从两个树的根节点开始。
算法分析:
直接模拟一下即可
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if(t2) swap(t1,t2); //如果t2不空就交换t1和t2两个节点,这样做的好处是可以保证t1一定存在,一定不为空(除非两个节点均为空)
if(!t1&&!t2) return NULL; //如果t1和t2节点均为空,就直接返回空
if(t2) t1->val += t2->val; //如果t2节点不空,此时t1节点一定不空,我们就用t1记录更新新的节点值,即是用Tree1记录新的树
//递归一下计算左右两棵子树
t1->left=mergeTrees(t1->left,t2?t2->left:NULL); //递归的时候,即传下去t1的左孩子和t2的左孩子,注意t1一定不为空,而t2有可能有空,所以往下传的时候要注意判断t2是否为空
t1->right=mergeTrees(t1->right,t2?t2->right:NULL); //递归的时候,即传下去t1的右孩子和t2的右孩子,注意t1一定不为空,而t2有可能有空,所以往下传的时候要注意判断t2是否为空
//最后返回t1节点即可
return t1;
}
};
java代码:
- 创建新树
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null) return t2 ;
if (t2 == null) return t1 ;
TreeNode root = new TreeNode (t1.val + t2.val);
TreeNode l = mergeTrees(t1.left,t2.left) ;
TreeNode r = mergeTrees(t1.right,t2.right) ;
root.left = l ;
root.right = r ;
return root ;
}
}
- 使用原节点
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if(t1==null) return t2;
if(t2==null) return t1;
t1.val+=t2.val;
t1.left=mergeTrees(t1.left,t2.left);
t1.right=mergeTrees(t1.right,t2.right);
return t1;
}
}
2021年8月11日21:26:57:
/**
* 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;
* }
* }
*/
//模拟题,递归解决,就是将两个二叉树进行合并,如果两个对应点都存在的话,那么合并完之后点的权值应该是两个节点的权值之和,
//如果一个节点存在,一个节点不存在的话,则节点的权值就是存在的节点权值,如果都不存在返回null,
//我们每次递归两个树对应的节点,然后判断两个节点的关系,之后就变成了判断是上面的哪一种情况
//
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if(t2!=null) { //如果t2不为空,我们就交换t1,t2,这样做的好处就是可以保证t1一定不为空
TreeNode t=t1;
t1=t2;
t2=t;
}
if(t1==null) return null; //交换完之后我们再判断一下,如果t1还是null,就说明两棵树均为null,我们返回null
if(t2!=null) t1.val+=t2.val; //经过上面的判断,可以保证t1一定不为空,如果t2不空,我们就把值合并到t1上
//我们把t1当做最后要返回的合并完的树,t1的值上面已经确定了,下面就要确定t1的左子树和右子树了
t1.left=mergeTrees(t1.left,t2!=null?t2.left:null); //t1的左儿子就是合并t1,t2的左子树,t1的左子树根据上面的if判断可以知道一定是存在的,
//而t2的左儿子则不确定,所以我们要先判断一下t2是否为空,如果不空递归的就是t2.right,否则就是null
t1.right=mergeTrees(t1.right,t2!=null?t2.right:null);
//同理右子树也是这样的道理
return t1; //最后返回的是合并节点之后的t1
}
}
662. 二叉树最大宽度
2021年11月21日17:39:32:
//注意这个题目中的空节点也是要占位置的,在算最大宽度的时候,指的不是每一层的点数的最大值,要计算上中间的空节点null,
//这个题目的思路就是用数组存储堆(用树实现的堆),即根节点的编号是1,左子树的编号是父节点的编号*2,右子树的编号是父节点的编号*2+1
//我们在bfs每一层的时候要找到每一层最左边和最右边节点,编号之差就是那一层的最大宽度,使用每一层的宽度更新全局答案res
//为了方便,我们需要在这一层的队列中同时存放节点和下标,所以我们可以新建一个结构体/类
class Solution {
class Pair{ //定义结构体
TreeNode node; //结构体中包含的类型是节点和下标
int id;
Pair(TreeNode node,int id){
this.node=node;
this.id=id;
}
}
public int widthOfBinaryTree(TreeNode root) {
if(root==null) return 0; //特判空
Queue<Pair> q=new LinkedList<>(); //定义宽搜要用到的队列
q.add(new Pair(root,1)); //先把根节点和其下标1加到队列中
int res=1; //定义答案,即宽度的最大值,一开始有一个根节点,所以res要初始化为1
while(q.size()!=0){
int len=q.size(); //len是本层节点的数量
int left=q.peek().id; //左边界点的下标就是当前队列中的第一个点的id
int right=0; //而右边界点我们还不知道,在下面bfs的过程中进行赋值
while(len--!=0){
Pair t=q.poll();
TreeNode node=t.node;
int id=t.id;
right=id; //每次都用t.id更新right,最后本层bfs结束,right记录的就是最右边点的id值
if(node.left!=null) q.add(new Pair(node.left,id*2)); //左子树的下标就是根节点的下标*2,右子树的下标就是根节点的下标*2+1
if(node.right!=null) q.add(new Pair(node.right,id*2+1));
}
res=Math.max(res,right-left+1);
}
return res;
}
}
863. 二叉树中所有距离为 K 的结点
2022年2月28日16:13:27:
//1.这个题目先dfs遍历,将有向树(图)转化为无向树(图),使用邻接表存储边可以到的边:java中可以使用map来存,key:节点指针,value:相邻的节点列表(其父节点和其子节点)
//2.再以target为根遍历,找到所有距离为k的点,注意的是无向图(所以要防止反向遍历),需要判断遍历点的父节点是否和当前点相同,如果相同我们就忽略
//就是说以target节点为根遍历无向树,第二个参数为节点的父节点,刚开始时我们是以target为根节点的,所以其父节点是null,这一点一定要注意
class Solution {
List<Integer> res = new ArrayList<>(); //存储答案
Map<TreeNode, List<TreeNode>> map = new HashMap<>(); //哈希表存储每一个节点可以按照无向图可以直接到达的节点(即每一个节点的父节点和子节点)
public List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
dfs1(root); //dfs1函数的作用就是将有向图转变为无向图,即在哈希表存储下来每一个节点可以直接到达的节点
//经过上面的dfs1,整棵树就变成了无向图了
dfs2(target, null, k); //dfs2函数的作用是以target为根,遍历无向图,并且为了避免重复遍历,我们要传入第二个参数,即节点的父节点,第三个参数是距离为k,我们使用的是减法
return res; //最后别忘了将res答案数组返回啊
}
//dfs1的作用就是将树转变为无向图
public void dfs1(TreeNode root){
if(root == null) return; //特判,注意是递归的结束条件
if(root.left != null){ //当前节点有左子节点,就要添加节点到左子节点的边,以及左子节点到节点的反向边
map.computeIfAbsent(root, m -> new ArrayList<>()).add(root.left); //添加一条当前节点到左子节点的边
map.computeIfAbsent(root.left, m -> new ArrayList<>()).add(root); //添加一条从左子节点到当前节点的反向边
dfs1(root.left); //我们要继续往下递归,这里只需要写dfs1(root.left),不需要写dfs2(root.right)
}
if(root.right != null){ //当前节点存在右子节点,注意这里和上面不是else if的关系,是可能会同时成立的关系
map.computeIfAbsent(root, m -> new ArrayList<>()).add(root.right);
map.computeIfAbsent(root.right, m -> new ArrayList<>()).add(root);
dfs1(root.right); //这里和上面的流程的类似的,只需要类比这来写
}
}
//dfs2的作用就是在无向图中找距离target节点为k的节点
public void dfs2(TreeNode root, TreeNode parent, int k){
if(k == 0){ //找到了一个节点,就把当前节点加到res中
res.add(root.val);
return;
}
if(!map.containsKey(root)) return; //如果哈希表中没有包含root节点,我们就直接结束返回
for(TreeNode son : map.get(root)){ //否则我们就遍历所有当前节点可以到达的节点
if(son != parent){ //这里是为了保证子节点和父节点不重复,即为了保证不重复遍历节点
dfs2(son, root, k - 1); //我们就继续往下递归遍历,传入的次数包括son,son的父节点即root,注意要在这里将k-1
}
}
}
}
872. 叶子相似的树
请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个 叶值序列
。
举个例子,如上图所示,给定一棵叶值序列为 (6, 7, 4, 9, 8)
的树。
如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是 叶相似
的。
如果给定的两个根结点分别为 root1
和 root2
的树是叶相似的,则返回 true
;否则返回 false
。
示例 1:
输入:root1 = [3,5,1,6,2,9,8,null,null,7,4], root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8]
输出:true
示例 2:
输入:root1 = [1], root2 = [1]
输出:true
示例 3:
输入:root1 = [1], root2 = [2]
输出:false
示例 4:
输入:root1 = [1,2], root2 = [2,2]
输出:true
示例 5:
输入:root1 = [1,2,3], root2 = [1,3,2]
输出:false
提示:
给定的两棵树可能会有 1 到 200 个结点。
给定的两棵树上的值介于 0 到 200 之间。
算法分析:
2021年8月11日19:51:09:
/**
* 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<Integer> a=new ArrayList<>(); //申请两个数组列表分别用于存放两棵树的叶子节点的遍历序列
List<Integer> b=new ArrayList<>();
public boolean leafSimilar(TreeNode root1, TreeNode root2) {
dfs(root1,a); //对树1进行前序遍历搜索,将答案存到数组a中
dfs(root2,b); //对树2进行前序遍历搜索,将答案存到数组b中
return a.equals(b); //List支持equals比较,如果两个数组列表相等就会返回true,否则返回false,注意是用的是equasl,不是==
}
public void dfs(TreeNode root,List<Integer> a){
if(root==null) return; //如果树是空的,直接结束,递归结束条件
if(root.left==null&&root.right==null) a.add(root.val); //是叶子节点,就把叶子结点加到数组列表中
dfs(root.left,a); //继续递归左子树,并把节点也放到数组a中
dfs(root.right,a); //继续递归右子树,并把节点也要放到数组a中,注意也是数组a中,不是数组b,数组a只是代表要存放的数组列表
}
}
958. 二叉树的完全性检验
2021年12月21日12:50:19:
class Solution {
//使用完全二叉树的堆式存储来判断,即根节点的编号是x,其左儿子的编号是2x,右儿子是2x+1, 只要当某个节点的编号大于了100,就是非法的
int n=0,p=0; //n是树中节点的数量,p是所有节点中的最大编号
public boolean isCompleteTree(TreeNode root) {
if(!dfs(root,1)) return false; //dfs函数是判断当前点的编号是否超过100(题目中说最多有100个点)
return n==p;
}
public boolean dfs(TreeNode root,int k){ //k是当前节点的编号
if(root==null) return true; //如果节点是空,返回true
if(k>100) return false; //再判断一下当前节点的编号是否大于了100
n++;p=Math.max(p,k);
return dfs(root.left,2*k)&&dfs(root.right,2*k+1);
}
}
988. 从叶结点开始的最小字符串
给定一颗根结点为 root
的二叉树,树中的每一个结点都有一个从 0
到 25
的值,分别代表字母 'a'
到 'z'
:值 0
代表 'a'
,值 1
代表 'b'
,依此类推。
找出按字典序最小的字符串,该字符串从这棵树的一个叶结点开始,到根结点结束。
(小贴士:字符串中任何较短的前缀在字典序上都是较小的:例如,在字典序上 "ab"
比 "aba"
要小。叶结点是指没有子结点的结点。)
示例 1:
输入:[0,1,2,3,4,3,4]
输出:"dba"
示例 2:
输入:[25,1,3,1,3,0,2]
输出:"adz"
示例 3:
输入:[2,2,1,null,1,0,null,0]
输出:"abc"
提示:
给定树的结点数介于 1 和 8500 之间。
树中的每个结点都有一个介于 0 和 25 之间的值。
2021年10月22日11:59:02:
这个题目的思路还是和找路径那几个题目的思路相同,注意这个题目是自底向上的,而我们在之前递归的题目中都是自顶向下的,所以我们可以考虑反转,即暴力搜索到所有的路径,在将答案加到res中的时候先翻转一下,最后求的是字典序最小的一个,所以我们要对res进行排序,最后res[0]就是答案:
/**
* 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;
* }
* }
*/
//换汤不换药:这个题目的总体思路就是我们暴力搜索到自顶向下的所有路径,在将路径加到res之前的时候翻转一下,最后搜索到所有的路径加到res之后
//再加res排个序,最后最小的字符串就是res[0],这个题目中我们需要自己编写翻转字符串的函数,或者使用StringBuffer,其自带reverse函数
class Solution {
List<String> res=new ArrayList<>();
public String smallestFromLeaf(TreeNode root) {
if(root==null) return "";
dfs(root,""); //从根节点开始递归,当前路径是""
Collections.sort(res); //对集合数组res进行排序,Collections.sort()就是按照字典序从小到大排序的
return res.get(0); //最后res[0]就是答案
}
public void dfs(TreeNode root,String path){
if(root==null) return;
path+=((char)(root.val+'a')); //注意要将int转为char,注意因为char小,而int大,所以char会自动转为int,所以我们要强制类型转换为char
if(root.left==null&&root.right==null){ //当前节点是root节点
String t=reverse(path); //将path翻转之后加到res中。reverse的返回值是字符串
res.add(t); //注意是将翻转之后的新字符串t加到res中,不是字符串path
return;
}
dfs(root.left,path); //否则就说明当前节点不是叶子结点,我们之后就是接着往左子树和右子树上分别递归
dfs(root.right,path);
}
public String reverse(String path){ //翻转字符串path并得到新的字符串进行返回,所以返回值是String
char[] c=path.toCharArray();
int l=0,r=c.length-1;
while(l<r){
char t=c[l];
c[l]=c[r];
c[r]=t;
l++;r--;
}
return new String(c);
}
}
class Solution {
List<StringBuffer> res=new ArrayList<>();
public String smallestFromLeaf(TreeNode root) {
if(root==null) return "";
// StringBuffer sb=new StringBuffer("");
dfs(root,new StringBuffer("")); //注意当在dfs中需要传递参数的时候如果一开始参数不好传递的话,我们可以想这样先构造好,
构造好之后再传递到dfs函数中去。
Collections.sort(res);
return res.get(0).toString();
}
public void dfs(TreeNode root,StringBuffer path){
if(root==null) return;
path.append((char)(root.val+'a'));
if(root.left==null&&root.right==null){
path.reverse();
res.add(path);
return;
}
dfs(root.left,path);
dfs(root.right,path);
}
}
1609. 奇偶树
2021年12月31日17:50:01:
//bfs,在遍历的时候需要记录高度,并根据高度检查节点值的奇偶性和是否满足严格递增/递减
//我们可以使用一个布尔变量flag记录层序/高度是否是偶数,使用pre记录当前层上一节点的值(判断是否满足递增/递减)即可,pre可以根据数据范围设置为哨兵值
class Solution {
public boolean isEvenOddTree(TreeNode root) {
Queue<TreeNode> q=new LinkedList<>();
if(root.val%2==0) return false;
boolean flag=true; //规定true是偶数层,false是奇数层
q.add(root);
while(q.size()!=0){
int len=q.size();
int pre=flag?0:0x3f3f3f3f; //flag是true即是偶数层,应该是严格递增的,所以pre初始化为0,奇数层的时候pre应该是递减的,所以初始化为正无穷大
while(len--!=0){
TreeNode t=q.poll();
int cur=t.val;
if(flag&&(cur%2==0||cur<=pre)) return false; //如果是偶数层,但是是偶数或者没有递增,都是错误的
if(!flag&&(cur%2!=0||cur>=pre)) return false; //同理写奇数层的情况
pre=cur; //更新pre为cur
if(t.left!=null) q.add(t.left);
if(t.right!=null) q.add(t.right);
}
flag=!flag; //奇数层变成偶数层,偶数层变成奇数层
}
return true;
}
}