树一般涉及到递归,回溯,剪枝,深度优先/栈,广度优先/队列,三种遍历方式。树的递归都是一样的套路,首先判断 最下方叶节点 和 根节点为空 的情况,这两个的代码或者情景实际上是一样的,然后判断左右子树对应的某个结点,这两个结点各自的子树也有相同的关系。这样就实现了递归。
对递归的两种理解:
前序递归:每次先对当前结点操作一下,再往左子树慢慢下去,右子树慢慢下去;
中序遍历:先往左子树跑到头,然后依次返回各个结点,右子树慢慢下去;
后序遍历:先往左子树跑到头,然后依次返回各个结点,再跑右子树跑到头,然后依次返回各个结点。
或者每次看到递归,可以认为它把子树都处理完了。
深度相关:有用栈的,大部分用递归。
二叉搜索树相关:看到就用中序遍历!
回溯相关:回溯解决的是全排列问题。257题,注意,回溯很多题都要用一个辅助函数。回溯题专讲请见另一篇文章(6大步骤团灭回溯题)。
广度优先相关:队列
给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
//这个判断针对 叶节点的下一轮 或 光杆根节点
if(p==nullptr && q==nullptr) return true;
//判断当前结点
if(p&&q && p->val==q->val){ //<- p&&q这个判断一定要有,上一个条件不能排除空结点的情况
//如果 左子树 和 右子树 都相同
if(isSameTree(p->left,q->left) && isSameTree(p->right,q->right)){
return true;
}
}
return false;
}
};
这道题没什么好说的,一般递归都会有对 当前结点 和 左右子树 分别的操作,同时需要在开头考虑叶节点的情况。最近碰到好几次下面的报错:
runtime error: member access within null pointer of type ‘struct TreeNode’
需要加上代码中标红的判断。
对于递归的理解:
看到isSameTree(p->left,q->left)之后,我们不用去思考复杂的递归过程,而仅仅把它当成另一个封装完全有独立功能的函数。有这个语句之后,我就知道,它的子树,也满足这条件。
1
/
2 2
/ \ /
3 4 4 3
class Solution {
public:
bool isSymmetric(TreeNode* root) {
return isSymmetric(root,root);
}
private:
bool isSymmetric(TreeNode* left,TreeNode* right){
if(left==nullptr && right==nullptr) return true;
if(left==nullptr || right==nullptr) return false;
return (left->val==right->val) && isSymmetric(left->left,right->right) && isSymmetric(left->right,right->left);
}
};
哎,这道题卡了我很久,之前想了个层序遍历的不对。
这个递归的,思路是比较左子树和右子树,左子树的根节点和右子树的根节点的val相同,而且左子树的左侧与右子树法右侧相同,左子树的右侧和右子树的左侧相同。
class Solution {
public:
int maxDepth(TreeNode* root) {
int num(0);
if(!root) return num;
if(root->left) num = max(num, maxDepth(root->left));
if(root->right) num = max(num, maxDepth(root->right));
num++;
return num;
}
};
树的最大深度 = max(左子树深度,右子树深度) + 1;
还是一样的,先考虑 根节点为空 或 叶节点 的情况,在考虑左子树和右子树。
有一个化简的写法:
int maxDepth(TreeNode *node){
if(!node) return 0;
return max(maxDepth(node->left), maxDepth(node->right)) + 1;
}
class Solution {
public:
int maxDepth(Node* root) {
int maxi = 0;
if(!root) return 0;
//找出所有子树中最高的那个的长度
for(Node* it:root->children){
if(it) maxi = max(maxi,maxDepth(it));
}
//树的长度等于最高的子树的长度+1
return maxi+1;
}
};
和104题一样。
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> result;
if(!root) return result;
stack<vector<int>> temp;
int level = 0;
vector<int> item;
queue<pair<int,TreeNode *>> que;
que.push(make_pair(level,root));
while(!que.empty()){
int last_level = level;//当前的层数
level++; //下一层的层数
item.clear();
while(que.front().first==last_level){
TreeNode *node = que.front().second;
que.pop();
if(!node) continue;//这一句能提高性能
if(node) item.emplace_back(node->val);
if(node && node->left) que.push(make_pair(level,node->left));
if(node && node->right) que.push(make_pair(level,node->right));
}
temp.push(item);
}
while(!temp.empty()){
result.emplace_back(temp.top());
temp.pop();
}
return result;
}
};
既然是层序遍历,而且是从下往上输出,很快就能想到用栈。101那道题做不对,就是因为当时没有用last_level这个策略。要想一层一层的操作,那么肯定就需要make_pair (level,root) 来记录层数。在添加当前层的左右孩子进队列的时候,层数需要加一。不过可能有的有左孩子有的有右孩子有的都有有的都没有,如果层数在make_pair (level++,node->left))添加的话,会出现混乱,所以得在这个while外面添加。或者不用level++,在括号里用level+1.
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
if(nums.size()==0) return nullptr;
return sortedArrayToBST(0,nums.size()-1,nums);
}
private:
TreeNode* sortedArrayToBST(int start, int end, vector<int>& nums){
if(start>end) return nullptr;
int mid = (start + end) / 2; //用>>提升性能
TreeNode* node = new TreeNode(nums[mid]);
//这样可千万不行:TreeNode node(nums[mid])!!在栈上,出来就析构了。
TreeNode* left = sortedArrayToBST(start, mid-1, nums);
TreeNode* right = sortedArrayToBST(mid+1, end, nums);
node->left = left;
node->right = right;
return node;
}
};
犯了个老错误,开辟新空间的时候,在自由存储区new一个,不要在栈上开辟。离开作用域就析构了。这道题跟二叉排序很像。
错误的解答:
class Solution {
public:
bool isBalanced(TreeNode* root) {
if(!root) return true;
int l(0),r(0);
if(root->left) l = maxDepth(root->left);
if(root->right) r = maxDepth(root->right);
return abs(l-r)<2;
}
private:
int maxDepth(TreeNode *root){
int m = 0;
if(!root) return m;
if(root->left) m = max(m,maxDepth(root->left));
if(root->right) m = max(m,maxDepth(root->right));
return ++m;
}
};
注意,左右子树的最大深度之差小于1并不能保证这是一颗平衡二叉树。万一左右子树也都不是平衡的呢?
修改:
加上一个判断即可。
class Solution {
public:
bool isBalanced(TreeNode* root) {
if(!root) return true;
int l(0),r(0);
//if(!isBalanced(root->left)) return false;
//if(!isBalanced(root->left)) return false;
l = maxDepth(root->left);
r = maxDepth(root->right);
return (abs(l-r)<2) && isBalanced(root->left) && isBalanced(root->right);
}
private:
int maxDepth(TreeNode *node){
if(!node) return 0;
return max(maxDepth(node->left), maxDepth(node->right)) + 1;
}
};
花了好长时间啊阿啊阿啊阿啊!
增加判断:除了左右子树最大深度差小于2外,还有左右子树都是平衡二叉树的判断。
而且这道题用上了104题的代码。
一看就是层序遍历。
思路1:使用107题的套路。
class Solution {
public:
int minDepth(TreeNode* root) {
if(!root) return 0;
queue<pair<int,TreeNode*>> que;
int level(0),last_level(0);
que.push(make_pair(++level,root));
while(!que.empty()){
last_level = que.front().first;
level = last_level + 1;
//对当前层元素的操作
while(last_level==que.front().first){ <-实际上是多余的
TreeNode* nextNode = que.front().second;
que.pop();
if(!nextNode->left && !nextNode->right) return last_level;
if(nextNode->left) que.push(make_pair(level,nextNode->left));
if(nextNode->right) que.push(make_pair(level,nextNode->right));
}
}
return last_level;
}
};
改进:
其实不用2个while。
class Solution {
public:
int minDepth(TreeNode* root) {
if(!root) return 0;
queue<pair<int,TreeNode*>> que;
int level(0),last_level(0);
que.push(make_pair(++level,root));
while(!que.empty()){
level = que.front().first;
TreeNode* nextNode = que.front().second;
que.pop();
if(!nextNode->left && !nextNode->right) return level;
if(nextNode->left) que.push(make_pair(level+1,nextNode->left));
if(nextNode->right) que.push(make_pair(level+1,nextNode->right));
}
return last_level;
}
};
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
//针对输入为空 或者 叶节点的下一层(也就是nullptr)
if(!root) return false;
//针对 叶节点
if(!root->left && !root->right && sum==root->val) return true;
//针对 子路径,(可以发现顺序是自下而上的)
if(root->left && hasPathSum(root->left,sum-root->val)) return true;
if(root->right && hasPathSum(root->right,sum-root->val)) return true;
return false;
}
};
警惕!很多题判断sum的,都是采用减法而不是加法。如果采用加法,逻辑不好控制,还需要辅助函数。
并没有用到回溯。每经过一个结点,sum就减去一个值,如果遇到根节点了,剩余数值正好等于了叶节点的值,那么就true。
递归方式,
如果这个树存在一条路径路径等于sum,那么他就存在一条子路径等于sum-root->val。
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
TreeNode* newTree = new TreeNode(0);
return invertTree(newTree,root);
}
private:
TreeNode* invertTree(TreeNode* newTree,TreeNode* root){
//针对 空的root 和 叶节点指向的nullptr
if(!root) return nullptr;
//针对 叶节点
newTree->val = root->val;
//针对 右子树
if(root->right){
TreeNode* left = new TreeNode(0);
newTree->left = invertTree(left,root->right);
}
//针对 左子树
if(root->left){
TreeNode* right = new TreeNode(0);
newTree->right = invertTree(right,root->left);
}
return newTree;
}
};
造一颗新树。下面是答案,纯翻转。java
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode right = invertTree(root.right);
TreeNode left = invertTree(root.left);
root.left = right;
root.right = left;
return root;
}
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(p->val<root->val && q->val<root->val)
return lowestCommonAncestor(root->left, p, q);
if(p->val>root->val && q->val>root->val)
return lowestCommonAncestor(root->right, p, q);
else return root;
}
};
看清楚,这是一颗二叉搜索树!!要充分利用他的性质:
如果两个数都小于根节点,那么就在左子树上找;
如果两个数都大于根节点,那么就在右子树上找;
如果两个数一个大于一个小于根节点,那么结果就是根节点了;
class Solution {
public:
vector<string> result;
string item;
vector<string> binaryTreePaths(TreeNode* root) {
generate(result,item,root);
return result;
}
private:
void generate(vector<string>& result, string& item, TreeNode* root){
//对于 空输入 和 叶节点的下一轮, 直接停止递归
if(!root) return;//item = "", result.emplace_back(item);
//每次访问结点,都需要增加"->val",不过开头需要单独判断
//注意int转string的方法
item += (item.empty()? "":"->") + to_string(root->val);
//如果到达了叶节点,可以往result里加入一个结果
if(!root->left && !root->right) result.emplace_back(item);
//记录回溯前状态
int len = item.length();
//对左子树进行操作
if(root->left){
//递归
generate(result,item,root->left);
//回溯,清除本轮操作的状态,注意对string清除操作的用法
item.erase(item.begin()+len,item.end());
}
if(root->right){
//递归
generate(result,item,root->right);
//回溯,清除本轮操作的状态
item.erase(item.begin()+len,item.end());
}
return;
}
};
一看就是一道典型的全排列问题,用回溯。我知道,回溯就是清除当前操作,从而复原到之前的状态,以便在原先状态的基础上进行另外的操作;但是我之前总是担心会重复操作。实际上回溯不会重复操作,因为他用的是if,本次操作后就直接进行其它操作了。
如何理解辅助函数?一般它都是void类型,而且它的操作对象,主要还不是总容器vector result(用过引用的方式修改该对象的值),而是当前容器string item。辅助函数内部,首先是极限值判断;然后是递归操作。
在评论区发现了一种更紧凑的写法:
void backtrack(TreeNode* root) {
//对于 空输入 和 叶节点的下一轮, 直接停止递归
if (!root) return;
//记录回溯前状态
int len = path.size();
//每次访问结点,都需要增加"->val",不过开头需要单独判断
path += (path.empty() ? "" : "->") + to_string(root->val);
//如果到达了叶节点,可以往result里加入一个结果
if (!root->left && !root->right) res.push_back(path);
//递归
else backtrack(root->left), backtrack(root->right);
//回溯
path.erase(path.begin() + len, path.end());
}
计算给定二叉树的所有左叶子之和。
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
int sum(0);
//如果是 null的根节点 或 叶节点 的下一轮
if(!root) return 0;
//在叶节点的上一层判断:如果 有左结点 且 左结点是叶节点
if(root->left && !root->left->left &&!root->left->right) sum += root->left->val;
//加上左子树的值
if(root->left) sum += sumOfLeftLeaves(root->left);
//加上右子树的值
if(root->right) sum += sumOfLeftLeaves(root->right);
return sum;
}
};
找出路径和等于给定数值的路径总数。
class Solution {
public:
int pathSum(TreeNode* root, int sum) {
if(!root) return 0;
int count(0);
int add(0);
queue<TreeNode*> que;
que.push(root);
//层序遍历每一个结点
while(!que.empty()){
TreeNode* node = que.front();
//找 当前结点 有多少个路径
count += oneNodePathSum(node,sum,add);
que.pop();
if(node->left) que.push(node->left);
if(node->right) que.push(node->right);
}
return count;
}
private:
//int add = 0;
//int count = 0;
int oneNodePathSum(TreeNode* root, int sum, int add){//add必须采用参数传递的方式
if(!root) return 0;
//count可以在这里定义,因为它可以通过return传递
int count(0);
//int add(0); 注意,绝对不可以在这里定义add,否则每次回溯都会把add清0!!
//add增加当前的值
add += root->val;
//记录递归前状态
int temp = add;
//如果某一条路径等于sum了,count++,不能return,因为下面可能有负数的结点,可能还有路径
if(add==sum) ++count;
//找左子树,不能剪枝,因为可能有负数的结点
if(root->left){
count += oneNodePathSum(root->left, sum,add);
add = temp;//回溯
}
////找右子树,不能剪枝,因为可能有负数的结点
if(root->right){
count += oneNodePathSum(root->right, sum,add);
add = temp;//回溯
}
return count;
}
};
112和257的结合。采用层序遍历每一个结点,对于每一个结点,都采用回溯的方式看看它有几条满足条件的路径。注意,这个add和257题中的vector result一样,可以定义为全局变量,也可以定义在main中,但是必须采用参数传递的方式,否则一直会不变。
现在总结257和437关于回溯的规律:
评论区看到另一种做法,就是递归每一个结点,从这个结点往上找,因为从每一个结点往上找的路径是唯一的,记录从这个结点到根节点能有几条路径满足条件;这个思路和那个动态规划的120题很像;
class Solution {
public int pathSum(TreeNode root, int sum) {
return helper(root, sum, new int[1000], 0); //0表示根
}
//array数组存储某一次递归时所遍历结点的结果值,p表示当前节点的位置
public int helper(TreeNode root, int sum, int[] array, int p){
if(root == null) return 0;
array[p] = root.val;
int temp = 0;
int n = 0;
for(int i=p; i>=0; i--){
temp += array[i];
if(temp == sum) n ++;
}
int left = helper(root.left, sum, array, p+1);
int right = helper(root.right, sum, array, p+1);
return n+left+right;
}
}
每次递归都新建一个数组,记录从根节点到这个结点的所有结点的val;相同路径上数组延续根节点的;不同路径的数组是不一样的。
class Solution {
public:
vector<int> findMode(TreeNode* root) {
vector<int> result;
int max(0);
unordered_map<int,int> mp;
DFS(root,mp,max);
for(auto it=mp.begin();it!=mp.end();it++){ <-map中根据val找key的方法
if((*it).second==max) result.emplace_back((*it).first);
}
return result;
}
private:
void DFS(TreeNode* root,unordered_map<int,int>& mp,int& max){
if(!root) return;
mp[root->val]++;
max = (max>mp[root->val])?max:mp[root->val];
if(root->left) DFS(root->left,mp,max);
if(root->right) DFS(root->right,mp,max);
return;
}
};
把它当成一般的树做的。构造一个map,存放着不同val的频数,选取最大的放入vector。
给定一个所有节点为非负值的二叉搜索树,求树中任意两节点的差的绝对值的最小值。
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
//int类型4个字节,32位,范围-2^31+1~2^31-1
int mini(1<<31-1);
int last = -1; //注意这个flag的玩法
DFS(root,mini,last);
return mini;
}
private:
void DFS(TreeNode* root, int& mini,int& last){
if(!root) return;
//遍历左子树
DFS(root->left,mini,last);
//用当前值减去上一个数
if(last != -1) mini = min(mini,root->val - last);
//这个数变成上一个数
last = root->val; //注意这个错位的玩法
//遍历右子树
DFS(root->right,mini,last);
return;
}
};
注意,看到二叉搜索树就中序遍历!!
中序遍历的特点是先一口气跑到左子树的最下面,再依次返回来;
右子树是从上慢慢下去。
为什么第二次做就不会了???
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。
输入: 二叉搜索树:
5
/
2 13
输出: 转换为累加树:
18
/
20 13
class Solution {
int add = 0;
public:
TreeNode* convertBST(TreeNode* root) {
if(!root) return nullptr;
convertBST(root->right);
root->val += add;
add = root->val;
convertBST(root->left);
return root;
}
};
这个是中序遍历的变种。
给定二叉搜索树的根结点 root,返回 L 和 R(含)之间的所有结点的值的和。
class Solution {
public:
int rangeSumBST(TreeNode* root, int L, int R) {
int sum = 0;
int last = -1;
DFS(root, sum, L, R);
return sum;
}
void DFS(TreeNode* root, int& sum,const int L, const int R){
if(root==nullptr) return;
//左子树
if(root->left) DFS(root->left,sum,L,R);
//如果当前结点的值在L-R之间,就相加
if(root->val>=L && root->val<=R)sum += root->val;
//右子树
if(root->right) DFS(root->right,sum,L,R);
}
};
核心:if(root->val>=L && root->val<=R)sum += root->val;
跟最大深度相关
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
错误解答:
最大直径是左子树和右子树的最大深度之和,但是万一最大直径没有经过根节点呢?所以说对于树中的每一个结点,都要把它视为根节点,然后比较所有结点的左子树和右子树的最大深度之和,取其中的最大值。
class Solution {
public:
int diameterOfBinaryTree(TreeNode* root) {
if(!root) return 0;
int l = 0; int r = 0;
if(root->left) l = maxdepth(root->left);
if(root->right) r = maxdepth(root->right);
return l+r;
}
int maxdepth(TreeNode* root){
if(!root) return 0;
return max(maxdepth(root->left),maxdepth(root->right))+1;
}
};
更改:
class Solution {
int diameterOfBinaryTree(TreeNode* root) {
if(!root) return 0;
int maxi = 0;
//temp没有作用,只是接一下
int temp = maxdepth(root,maxi);
//maxi是通过参数引用实现值的修改
return maxi;
}
int maxdepth(TreeNode* root, int& maxi){
if(!root) return 0;
//获取左子树的最大深度
int l = maxdepth(root->left,maxi);
//获取右子树的最大深度
int r = maxdepth(root->right,maxi);
//获取以所有结点作为根节点时,各自的直径中的最大值
maxi = max(maxi, l+r);
//这个树的最大深度
return max(l,r)+1;
}
};
给定一个二叉树,计算整个树的坡度。一个树的节点的坡度定义即为,该节点左子树的结点之和和右子树结点之和的差的绝对值。空结点的的坡度是0。整个树的坡度就是其所有节点的坡度之和。
class Solution {
public:
int findTilt(TreeNode* root) {
if(!root) return 0;
//树的坡度 = 左子树的坡度 + 右子树的坡度 + 结点的坡度
return (findTilt(root->left) + findTilt(root->right) + oneTilt(root));
}
private:
int oneTilt(TreeNode* root){
if(!root) return 0;
//结点的坡度 = abs(左子树和 - 右子树和)
return abs(sum(root->left) - sum(root->right));
}
int sum(TreeNode *root){
if(!root) return 0;
//树的和 = 左子树的和 + 右子树的和 + 结点的val
return(root->val + sum(root->left) + sum(root->right));
}
};
效率提升版:和543题采取了相同的策略
class Solution {
public:
int findTilt(TreeNode* root) {
if(!root) return 0;
int tile = 0;
int summ = sum(root,tile);
return tile;
}
private:
int sum(TreeNode *root, int& tile){
if(!root) return 0;
int l = sum(root->left,tile);
int r = sum(root->right,tile);
//坡度等于之前的坡度的累积加上自己的坡度
tile += abs(l-r);
//树的和 = 左子树的和 + 右子树的和 + 结点的val
return(root->val + l + r);
}
};
核心:tile += abs(l-r);
给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
class Solution {
public:
bool isSubtree(TreeNode* s, TreeNode* t) {
if(!s && !t) return true;
if(t && !s) return false;
//if(s && !t) return false;
//如果t是s左子树下面,或者s右子树下面或者t=s,那么true
return isSubtree(s->left,t) || isSubtree(s->right,t) || isSame(s,t);
}
private:
bool isSame(TreeNode* s, TreeNode* t){
//以下3个判断目的是确保对 叶节点下一层 的判断
if(!s && !t) return true;
if(!s && t) return false;
//if(s && !t) return false;
//如果两棵树都有,且左右子树一样,当前结点一样,则true
return s&&t && isSame(s->left,t->left) && isSame(s->right,t->right) && (s->val == t->val);
}
};
思路很简答,而且是100题的拓展。但是要注意if的判断。if除了判断根节点,更重要的是判断叶节点后面的内容,这是迭代到头的依据。