【代码随想录】树的遍历题目

针对二叉树的问题,解题之前一定要想清楚究竟是前中后序遍历,还是层序遍历。

二叉树解题的大忌就是自己稀里糊涂的过了(因为这道题相对简单),但是也不知道自己是怎么遍历的。

226. 翻转二叉树

题目
思路复盘:
以为自己写的递归,实际上还是一个前序递归!!!!
以下是我之前写的时候想的:我写的递归,找不到最后的return,然后就用最简单的试了一下,最后return root 。分的不同情况,root为空,root是叶子结点,root左右子树只有一个不空或者两个都不空,则交换,然后再分别反转左右子树。]

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
         if(root==NULL) return NULL;
         if(root->left==NULL&&root->right==NULL) return root;
         swap(root->left,root->right);
         if(root->left) invertTree(root->left);//注意if里不是return
         if(root->right) invertTree(root->right);
         return root;
    }
};

BFS:
我写的

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
         queue<TreeNode *> q;
         if(root) q.push(root);
         while(!q.empty())
         {
        	 int size=q.size();
        	 for(int i=0;i<size();i++)
        	 {
        		 TreeNode * p=q.front();
        		 q.pop();
        		 if(p->left) q.push(p->left);
        		 if(p->right) q.push(p->right);
           		 if((p->left==NULL&&p->right)||(p->right==NULL&&p->left)||(p->left&&p->right))
           			 swap(p->left,p->right);          			 
        	 }
         }
         return root;
    }
};


DFS:
我写的

class Solution {
public:
	void DFS(TreeNode *cur)
	{
		if(cur==NULL) return;
		if((cur->left==NULL&&cur->right)||(cur->right==NULL&&cur->left)||(cur->left&&cur->right))
			swap(cur->left,cur->right);			
		DFS(cur->left);
		DFS(cur->right);
	}
    TreeNode* invertTree(TreeNode* root) {
       DFS(root);
       return root;
    }
};


前序非递归:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
    	if(root==NULL) return root;
       stack<TreeNode*> st;
       st.push(root);
       TreeNode *p=root;
       while(!st.empty())
       {
    	   if(p)
    	   {
    		   st.push(p);
    		   swap(p->left,p->right);
    		   p=p->left;
    	   }
    	   else{
    		   p=st.top();
    		   st.pop();
    		   p=p->right;
    	   }
       }
       return root;
    }
};

中序非递归
中序递归会遍历两次???????待验证

在这里插入代码片

反思:
本质都是遍历,只不过在遍历的过程中增加一些操作!如果是递归注意树的几种情况考虑清楚!即:

  1. 左右子树皆为空,即叶子结点
  2. 左右子树一个为空,一个不为空
  3. 左右子树都不为空

随想录解法:

统一模板的前序迭代写法:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
    	if(root==NULL) return root;
       stack<TreeNode*> st;
       st.push(root);
       TreeNode *p=root;
       while(!st.empty())
       {
            p=st.top();
    	   if(p)
    	   {
               st.pop();
    		   if(p->right) st.push(p->right);  //right
    		   if(p->left) st.push(p->left);   //left
    		   st.push(p);                     //mid
    		   st.push(NULL);                     		   
    	   }
    	   else{
    		   st.pop();  //NULL pop
    		   p=st.top();
    		   st.pop();
    		   swap(p->left,p->right);
    		  
    	   }
       }
       return root;
    }
};

使用迭代的统一模板因为是使用的栈进行遍历的,没有依靠指针加栈结合这种,所以是可以使用的。而使用普通的后序非迭代遍历是不行的。【随想录使用中序递归遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了。】
中序迭代模板遍历:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
    	if(root==NULL) return root;
       stack<TreeNode*> st;
       st.push(root);
       while(!st.empty())
       {
		   TreeNode *p=st.top();
    	   if(p)
    	   {
    		   st.pop();
    		   if(p->right) st.push(p->right);  //right
    		   st.push(p);                     //mid
    		   st.push(NULL);  
    		   if(p->left) st.push(p->left);   //left
    		                     		   
    	   }
    	   else{
    		   st.pop();  //NULL pop
    		   p=st.top();
    		   st.pop();
    		   swap(p->left,p->right);    		  
    	   }
       }
       return root;
    }
};

官方:
还是不要用swap了

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == nullptr) {
            return nullptr;
        }
        TreeNode* left = invertTree(root->left);
        TreeNode* right = invertTree(root->right);
        root->left = right;
        root->right = left;
        return root;
    }
};


101. 对称二叉树

题目
方法一:双指针递归遍历
思路复盘:
脑海中一瞬间有想过使用两个指针去分别遍历两棵子树,但是因为没有找到题目规律,也就是解题的逻辑,所以不知道如何写代码,
反思:下一次可以用自己的话把解题逻辑写出来,然后再去用代码或者符号去严谨的表示出来这种逻辑,再敲代码
这个题目中我体会到了算法和解题逻辑缺一不可,本题算法使用的就是递归,双指针遍历;解题逻辑是遍历两颗子树的顺序:左边的:左右中,右边的:右左中
而我没有用符号去想只是用人类的逻辑想了一下,没有深刻的去想如何实现,本质是什么?
解题逻辑中一个基本的逻辑:题目中出现的各种情况要列清楚!树的话不要出现空指针,即遍历左右子树时,一定判断当前指针是不是空指针
看了思路后自己写的:
注意:两个结点不能直接比较是否相等,只能比较val,即不是 node1==node2 而是node1->val==node2->val

class Solution {
public:
	bool PostOrder(TreeNode *node1,TreeNode *node2)
	{
		if(node1==NULL&&node2==NULL) return true;
		if(node1&&node2)
		{
		bool a=PostOrder(node1->left, node2->right);
		bool b=PostOrder(node1->right,node2->left);
		if((node1->val==node2->val)&&a&&b) return true;
		}//注意:如果这里写的是node1==node2,则是错的!!!
		return false;
		
	}
    bool isSymmetric(TreeNode* root) {
    	  	if(root==NULL) return true;
    	  	return PostOrder(root->left, root->right);
    }
};

随想录:
反思:
将每个空值情况都考虑清楚!!

class Solution {
public:
	bool PostOrder(TreeNode *node1,TreeNode *node2)
	{
	//为空时的情况
		if(node1==NULL&&node2==NULL) return true;
		else if(node1==NULL&&node2!=NULL) return false;
		else if(node1!=NULL&&node2==NULL) return false;
		else if(node1->val!=node2->val) return false;
		//这里就是不为空且值相等后的单层处理
		bool a=PostOrder(node1->left, node2->right);
		bool b=PostOrder(node1->right,node2->left);
		if(a&&b) return true;
		return false ;
		
	}
    bool isSymmetric(TreeNode* root) {
    	  	if(root==NULL) return true;
    	  	return PostOrder(root->left, root->right);
    }
};

方法二:迭代
使用队列,逻辑与递归相同,还是左子树:左右中,右子树:右左中,比较即可。
注意这里的迭代中队列只是充当了一个容器的角色,所以使用栈,队列,数组都可以,要看透本质!!!!
看了思路后自己写的:

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
    	  	if(root==NULL) return true;
    	  	queue<TreeNode*> que;
    	  	que.push(root->left);
    	  	que.push(root->right);
    	  	while(!que.empty())
    	  	{
    	  	/*
每次是取出对头的两个元素进行比较,再依次入队迭代,中间如果熬过了这一系列return false关卡,说明是值相同且左右子树不为空,那么就继续迭代进队比较它们的左右子树*/
    	  		TreeNode *node1=que.front();
    	  		que.pop();
    	  		TreeNode *node2=que.front();
    	  		que.pop();
    	  		if(node1==NULL&&node2==NULL) continue; //注意:这里不是return true,因为这只是一次比较,而fasle则可以直接return
    	  		else if(node1==NULL&&node2!=NULL) return false;
    	  		else if(node1!=NULL&&node2==NULL) return false;
    	  		else if(node1->val!=node2->val)   return false;
    	  		//node1->left->val==node2->val
    	  		//比较左子树的左和右子树的右
    	  		que.push(node1->left);
    	  		que.push(node2->right);
    	  		//下面这两个不要忘记!这两个是比较左子树的右 和右子树 的左
    	  		que.push(node1->right);
    	  		que.push(node2->left);
    	  	}
    	  	return true;
    	  	
    }
};

重要反思!! 从迭代法和递归法可以看出,它们的解题逻辑本质是不变的(对比左右中,左中右),变化的是算法逻辑,即使用什么算法和数据结构去实现,然后用代码表达出来即可。

解答错误的层次遍历???待解决找问题!!

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
    	queue<TreeNode*> q;
    	if(root) q.push(root);
    	while(!q.empty())
    	{
    		int size=q.size();
    		vector<TreeNode*> v;
    		for(int i=0;i<size;i++)
    		{
    			TreeNode *p=q.front();
    			q.pop();
    			v.push_back(p);
    			if(p) 
    				{
    				q.push(p->left);
    				q.push(p->right);
    				}   
    		}
    		for(int j=0;j<v.size()/2;j++)
    		{
    			if(v[j]!=v[size-1-j])
    				return false;
    		}
    	}
    	return true;
    	
    }
};

100. 相同的树

题目
与上面的思路一样,是比较两个树的思路。
类似题目:代码都一样

class Solution {
public:
    bool isSameTree(TreeNode* p, TreeNode* q) {
    	if(p==NULL&&q==NULL) return true;
    	else if(p==NULL&&q!=NULL) return false;
    	else if(p!=NULL&&q==NULL) return false;
    	else if(p->val!=q->val) return false;
    	bool a=isSameTree(p->left, q->left);
    	bool b=isSameTree(p->right, q->right);
    	if(a&&b) return true;
    	return false;
    }
};


572. 另一棵树的子树

题目

方法一:深度优先暴力法
我写的:
思路复盘:
使用层次遍历找到一个节点和subRoot的val相同的那个加入到vector里,再使用深度优先搜索进行比较,看以该结点为根的子树是否与subRoot相等
只要有解题思路,暴力也没关系,先做出来再想最优解!找到本质然后利用这个不变的模板,在上面修改。

class Solution {
public:
    bool isSubtree1(TreeNode* root, TreeNode* subRoot) {
    	if(root==NULL&&subRoot==NULL) return true;
    	else if(root!=NULL&&subRoot==NULL) return false;
    	else if(root==NULL&&subRoot!=NULL) return false;
    	else if(root->val!=subRoot->val) return false;
    	bool a=isSubtree1(root->left, subRoot->left);
    	bool b=isSubtree1(root->right, subRoot->right);
    	return a&&b;
    }
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
    	if(root==NULL&&subRoot==NULL) return true;
       queue<TreeNode*>que;
       if(root) que.push(root);
       vector<TreeNode*> v;
       while(!que.empty())
       {
    	   TreeNode *node=que.front();
    	   que.pop();
    	   if(node->val==subRoot->val)
    	   {
    		   v.push_back(node);
    		 
    	   }
    	   if(node->left!=NULL) que.push(node->left);
    	   if(node->right!=NULL) que.push(node->right);    	   
       }
       for(int i=0;i<v.size();i++)
       {
    	   if(isSubtree1(v[i], subRoot))
    		   return true;   		   
       }
       return false;
       }
};

官方题解:
使用了两次深度优先搜索
学习到的点:在一个深度优先搜索时,又进入到另一个深度优先搜索递归

class Solution {
public:
    bool isSame(TreeNode* root, TreeNode* subRoot) {
    	if(root==NULL&&subRoot==NULL) return true;
    	else if(root!=NULL&&subRoot==NULL) return false;
    	else if(root==NULL&&subRoot!=NULL) return false;
    	else if(root->val!=subRoot->val) return false;
    	bool a=isSame(root->left, subRoot->left);
    	bool b=isSame(root->right, subRoot->right);
    	return a&&b;
    }
    bool DFS(TreeNode *root,TreeNode *SubRoot)
    {
    	if(root==NULL)
    		return false;  
    	return isSame(root,SubRoot)||DFS(root->left,SubRoot)||DFS(root->right,SubRoot);  //查看当前节点子树是否与subRoot相同,再分别遍历左右子树,相当于在一个深度优先搜索时,又进入到另一个深度优先搜索递归
    }
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
    	
       return DFS(root,subRoot);
       
       }
};

方法二:前序遍历上进行字串匹配
先序遍历特点:深度优先搜索即先序遍历的子树组成的串是连续的,但是必须将空结点也加入,这样深度优先搜索变成了完全二叉树,才能唯一确定一棵子树,否则只靠先序遍历也不能确定一颗树,即变成完全二叉树,完全二叉树可以被中序遍历,层序遍历,先序遍历,后序遍历唯一确定????
所以这里遇到空结点的时候就将vector加入INT_MIN(INT_MAX)
将找是否包含子树的问题,转化成了找子串匹配的问题。
我使用的是暴力法匹配串,官解使用的是KMP算法
字串匹配:力扣28题

class Solution {
public:
    void DFS(TreeNode *root,vector<int>&v)
    {
    	if(root==NULL)
    		{
    	//	v.push_back(0); //insert null
    		return;
    		}
    	v.push_back(root->val);
    	if(root->left)
    	   DFS(root->left,v);
    	else v.push_back(INT_MIN);
    	if(root->right)
    	  DFS(root->right,v);
    	else v.push_back(INT_MIN);//遇到空结点不能加入0,应该加入一个最大或者最小值
    }
    //string match question
    bool isSame(vector<int> v1,vector<int> v2)
    {
    	if(v1.size()<v2.size())
    		return false;
        int j;
    	for(int i=0;i<v1.size();i++)
    	{
    		for(j=0;j<v2.size();j++)
    		{
    			if(v1[i+j]!=v2[j])
    			{
    				break;
    			}
    		}
    		if(j==v2.size())
    			return true;
    	}
    	return false;
    }
    bool isSubtree(TreeNode* root, TreeNode* subRoot) {
    	vector<int> v1;
    	vector<int>v2;
    	DFS(root,v1);
    	DFS(subRoot,v2);
    	return isSame(v1,v2);
    }
    
};

bug点:
遇到空结点不能加入0,应该加入一个最大或者最小值,否则,如果结点有一个值为0,则结果不对!!!如样例:[3,4,5,1,2,null,null,null,null,0]
//[4,1,2]

方法三:树哈希
官方题解,待学。。

222. 完全二叉树的节点个数

题目
思路一:当成普通二叉树计算
利用二叉树的DFS遍历
我写的:

class Solution {
public:
	void DFS(TreeNode* root,int depth)
	{
		if(root==NULL) return;
		depth++;
		DFS(root->left,depth);
		DFS(root->right,depth);
	}
    int countNodes(TreeNode* root) {
    	int depth=0;
    	DFS(root,depth);
    	return depth;
    }
};

随想录:

class Solution {
public:
	int postOrder(TreeNode* root)
	{
		if(root==NULL) return 0;
		int leftnum=postOrder(root->left);
		int rightnum=postOrder(root->right);
		return leftnum+rightnum+1;
	}
    int countNodes(TreeNode* root) {
    	return postOrder(root);
    }
};

方法二:利用完全二叉树的特点来做
随想录:

class Solution {
public:
    int countNodes(TreeNode* root) {
        if (root == nullptr) return 0;
        TreeNode* left = root->left;
        TreeNode* right = root->right;
        int leftHeight = 0, rightHeight = 0; // 这里初始为0是有目的的,为了下面求指数方便
        while (left) {  // 求左子树深度
            left = left->left;
            leftHeight++;
        }
        while (right) { // 求右子树深度
            right = right->right;
            rightHeight++;
        }
        if (leftHeight == rightHeight) {
            return (2 << leftHeight) - 1; // 注意(2<<1) 相当于2^2,所以leftHeight初始为0
        }
        return countNodes(root->left) + countNodes(root->right) + 1;
    }
};

官方题解:
题解
很精妙的解法!!!!!还没看懂!!
利用完全二叉树的特点,如果最后一层没有填满,那么最后一层的结点是靠左的,所以每次从根节点走到最左下角的结点就是二叉树的最大深度

110. 平衡二叉树

题目

重点掌握:两个递归遍历相互递归调用
思路:
一颗树是平衡二叉树的充要条件是该树的每一个子树都是平衡二叉树。
因此递归调用计算每一个结点的左右子树,看其绝对值之差是否是小于等于1的,如果大于1则不是,否则继续递归检查该节点的左右子树是否是平衡二叉树。
我的思路复盘:
一直想着用一个递归调用来完成,参数没确定好!!递归三部曲没想清楚,还有返回值,不知道时使用bool 还是depth。这个时候要想清楚解题逻辑,然后再分成两个函数来解决!
没写出来,看的答案写的:

class Solution {
public:
	int postOrder(TreeNode* root)
	{
		if(root==NULL) return 0;
		int leftnum=postOrder(root->left);
		int rightnum=postOrder(root->right);
		return max(leftnum,rightnum)+1;
	}

    bool isBalanced(TreeNode* root) {
        if(root==NULL) return true;
    	int left=postOrder(root->left);
    	int right=postOrder(root->right);
    	if(abs(left-right)>1)
    		return false;
    	return isBalanced(root->left)&&isBalanced(root->right);
    }
};

时间复杂度分析:
时间复杂度:O(n^2),其中 n 是二叉树中的节点个数。
最坏情况下,二叉树是满二叉树,需要遍历二叉树中的所有节点,时间复杂度是 O(n)。
对于节点 pp,如果它的高度是 d,则 height§ 最多会被调用 d 次(即遍历到它的每一个祖先节点时)。对于平均的情况,一棵树的高度 h 满足 O(h)=O(logn),因为 d \leq hd≤h,所以总时间复杂度为O(nlogn)。对于最坏的情况,二叉树形成链式结构,高度为 O(n),此时总时间复杂度为 O(n^2)

空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。
方法二:后序遍历
思路:太精妙了!
一定要再看一遍随想录的解析!
辨析:

  • 结点的高度:从下往上的,即以该节点为根节点从叶子结点到该结点的节点数。
  • 结点的深度:从真正的根节点开始从上往下,即该节点所在的层数。

因此,求高度是后序遍历(从下往上),求深度是前序遍历(从上往下)。可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
所以,这里求高度应该使用后序遍历,而同时还要判断是否是平衡二叉树,一个递归要实现这两个功能,即解决我上面的思路复盘中的问题,就是如果是平衡二叉树需要递归,如果不是平衡二叉树直接返回一个标志-1即可。
为什么使用-1来标记?
因为如果不是平衡二叉树返回高度就没有意义了;是平衡二叉树则返回高度继续往上进行。【这里的往上就是return max(leftnum,rightnum)+1】只要子树中有一个不是平衡二叉树,则整个树都是不是平衡二叉树。
与第一种方法的对比:
好处是第一种方法是从上往下进行计算每一个结点的高度,会重复计算!而从下往上计算就不会重复计算!

看的官方题解写的:

class Solution {
public:
	int postOrder(TreeNode* root)
	{
		if(root==NULL) return 0;
		int leftnum=postOrder(root->left);
		int rightnum=postOrder(root->right);
		if(leftnum==-1||rightnum==-1||abs(leftnum-rightnum)>1)
			return -1;
		return max(rightnum,leftnum)+1;	
	}

    bool isBalanced(TreeNode* root) {
       return postOrder(root)>=0;
       // return postOrder(root) == -1 ? false : true;
    }
};

迭代法:
看的随想录,思路:使用求深度的递归来求每一个子树的高度,然后再用迭代的深度遍历来比较每一个结点的左右子树高度之差。

class Solution {
public:
//该函数本质还是求得深度得方法,只不过每次参数传递的是每一个节点,所以是求得每一个节点的最大深度。
	int getHeight(TreeNode* root)
	{
		if(root==NULL) return 0;
		stack<TreeNode*>st;
		int result=0;
        int depth=0;
		st.push(root);
		while(!st.empty())
		{
			TreeNode* node=st.top();
			if(node)
			{
				st.pop();
				st.push(node);
				st.push(NULL);
				depth++;
				if(node->right) st.push(node->right);
				if(node->left) st.push(node->left);				
			}
			else{
				st.pop();
				node=st.top();
				st.pop();
				depth--;
			}
			result=result<depth?depth:result;//有点不懂!这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)
		}
			return result;
	}

    bool isBalanced(TreeNode* root) {
       stack<TreeNode*>st;
       if (root == NULL) return true;
       st.push(root);
       while(!st.empty())
       {
               TreeNode *node=st.top();
    		   st.pop();
    		   if(abs(getHeight(node->left)-getHeight(node->right))>1) 
    			   return false;
    		   if(node->right) st.push(node->right);
    		   if(node->left)  st.push(node->left);   
     	   
       }
       return true;
    }
};

随想录:
在这里插入图片描述

257. 二叉树的所有路径(好题)

题目
方法一:先序递归(带回溯)
随想录,这道题目我没做出来。
看随想录解析!!!!
这里使用path记录每条路径,并且因为是递归,所以可以复用,使用vector是方便进行回溯操作,也可以使用string。这里的终止条件不再是if(root==NULL) return ;因为这里终止条件是叶子结点,每当找到而叶子结点,就将path中的路径结点转化成string,加入到result中,因为没有判定root为空的条件,所以下面就每次递归时会判断左右子树是否为空。
注意:应当先加入当前的结点cur,再去看cur是否是叶子结点,否则放在终止条件之后的话该path就会少了当前的叶子结点!然后再递归的去遍历左右子树,注意每当递归完一个子树,就应该进行回溯,删除当前path中的最后一个结点。(可以画图理解)

class Solution {
public:
//使用前序遍历,因为是求从根节点到叶子节点的路径,所以使用前序遍历可以方便的让双亲指向叶子结点,且涉及到回溯。
	void PreOrder(TreeNode *cur,vector<int>&path,vector<string>&result)//这里path,result都需要是引用类型!因为是递归回溯,相当于一直在利用之前递归下的数据,所以必须是引用,否则会丢失之前递归的数据
	{
        //单层递归逻辑:add cur to path
		path.push_back(cur->val);
        
		//终止条件
		if(cur->left==NULL&&cur->right==NULL) 
		{
			string s;
			for(int i=0;i<path.size()-1;i++)
			{
				s+=to_string(path[i])+"->";
			}
			s+=to_string(path[path.size()-1]);
			result.push_back(s);
            return;//不要忘记!!!(不是因为是终止条件!不需要执行下面的语句了,即遍历左右子树)
		}
		
	//	path.push_back(cur->val);而不是放在这,放在这,如果是叶子结点就会直接返回,少加了当前的结点
				
	    if(cur->left)
	    {
	    	PreOrder(cur->left, path, result);
	    	path.pop_back();//回溯,删除结点
	    }
	    if(cur->right)
	    {
	    	PreOrder(cur->right, path, result);
	    	path.pop_back(); //回溯删除结点
	    }
		//不能将回溯 path.pop_back()放到括号外面,因为每次递归完都需要回溯
        
	}
    vector<string> binaryTreePaths(TreeNode* root) {
        if(root==NULL) return {};
    	vector<string> result;
    	vector<int>path;
    	PreOrder(root, path, result);
    	return result;

    }
};

精简版
随想录:

class Solution {
private:

    void traversal(TreeNode* cur, string path, vector<string>& result) {
        path += to_string(cur->val); // 中
        if (cur->left == NULL && cur->right == NULL) {
            result.push_back(path);
            return;
        }
        if (cur->left) traversal(cur->left, path + "->", result); // 左
        if (cur->right) traversal(cur->right, path + "->", result); // 右
    }

public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        string path;
        if (root == NULL) return result;
        traversal(root, path, result);
        return result;

    }
};

也很重要,注意理解精简版的回溯!很精妙,放在path+“->”.
函数传参数,如果是复制传参,(没有加引用)每次在函数内部操作完path就会销毁,path在函数内的改变不会传回到调用该函数的那里,所以精简版的path没有加&,每次都是复制传参,相当于每次递归返回后自己就去掉了上一层中加的cur即(path+=cur)这个操作,path在本层的数值没变,就相当于是回溯了。为什么参数要加上“->”,如果不加,如下,即只是数字递归回溯了。在这里插入图片描述
如果是把"->"放到递归的前面就不能体现回溯了,因为每次递归都会加上“->”,等到返回上一层时还是会有->的。。。。感觉还是没有理解

        path += "->";
        if (cur->left) traversal(cur->left, path , result); // 左
        path += "->";
        if (cur->right) traversal(cur->right, path, result); // 右

在这里插入图片描述
可以像官方题解那样只加一次”->“,(修改随想录精简版),这个和之前的参数里depth+1,还是放在递归外面depth++有点像。

class Solution {
private:

    void traversal(TreeNode* cur, string path, vector<string>& result) {
        path += to_string(cur->val); // 中
        if (cur->left == NULL && cur->right == NULL) {
            result.push_back(path);
            return;
        }
        path+="->";
        if (cur->left) traversal(cur->left, path , result); // 左
        if (cur->right) traversal(cur->right, path , result); // 右
    }

public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;
        string path;
        if (root == NULL) return result;
        traversal(root, path, result);
        return result;

    }
};

我的误区:
但是这道题目说不在乎顺序,所以应该用其他遍历也行?所以我就用了后序遍历一直写不出来。这里其实只看了半句话,人家说的很清楚是从根节点到叶子节点的路径!!!好好看题目!顺序就是不同路径在result数组中可以不同,但是都是从根节点到叶子节点的!
【错的以下:】

class Solution {
public:
	string PostOrder(TreeNode *root,vector<string>&result,int depth)
	{
		if(root==NULL) return "";
		if(depth==result.size()) result.push_back("");
		string left=PostOrder(root->left,result,depth+1);
		string right=PostOrder(root->right,result,depth+1);
		
		result[depth]+=left+"->"+to_string(root->val);
		result[depth]+=right+"->"+to_string(root->val);
        return left+right+to_string(root->val);		
	}
    vector<string> binaryTreePaths(TreeNode* root) {
    	vector<string> result;
    	return  result;
    }
};

反思:

  • 和层序遍历进行区分,这里是深度优先遍历然后加入vector的,层序是depth==result.size()的时候才加入。
  • 王道:后续非递归遍历解决:二叉树的公共祖先,从根节点到某节点的路径
  • 本题是用先序遍历来实现从根节点到叶子结点的路径,可以使用先序递归遍历(有回溯),也可以用栈来模拟递归,用先序的迭代法。官方题解还用了层序遍历实现

值传递和引用传递的本质:

  1. 值传递就是复制了一份实际参数,相当于被调用函数的局部变量,函数调用结束后就会销毁,不会对主函数中的实际参数有影响。
  2. 引用传递,穿的是一个地址,在函数中做的修改就相当于对实参的修改。
    本题中,精简版的递归调用path参数使用的是值传递,而且是自己调用自己,所以每次调用完自己后产生的path值(修改后的)会复制为新的副本并加上"->“传递到下一个递归path参数中,( 这里与depth+1有点相似)因此“->”不会重复,并且回退后,path还是上一层的值(递归到深层的只是path的副本),如果在外面写“->”,则相当于每次都复制了一份path+”->“,到下一层会变成path+”->“+”->"。

官方题解:

注意这里的"->"

class Solution {
public:
    void construct_paths(TreeNode* root, string path, vector<string>& paths) {
        if (root != nullptr) {
            path += to_string(root->val);
            if (root->left == nullptr && root->right == nullptr) {  // 当前节点是叶子节点
                paths.push_back(path);                              // 把路径加入到答案中
            } else {
                path += "->";  // 当前节点不是叶子节点,继续递归遍历
                construct_paths(root->left, path, paths);
                construct_paths(root->right, path, paths);
            }
        }
    }

    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> paths;
        construct_paths(root, "", paths);
        return paths;
    }
};

方法二:迭代法:
随想录:
先序迭代就用随想录的方法,更加简洁好记。每次取栈的时候是左,左,左,左,想象一下…
一个重要的细节:在想好解题逻辑的前提下,自己用的每一个数据结构都要明确其功能,并使用相应的命名取名字,这样在写的过程中才不会混乱。

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
    	stack<TreeNode*> treeSt;//记录树遍历结点的栈,就是先序遍历模拟递归的栈
    			stack<string> pathSt;//记录路径的栈
    			vector<string> result;//vector用来保存结果
    			if(root)
    		    {
    				treeSt.push(root);
    				pathSt.push(to_string(root->val));//只要遍历过就要加入pathSt栈中
    		    }
    			while(!treeSt.empty())
    			{
    				TreeNode* node=treeSt.top(); 
    				treeSt.pop();
    				string path=pathSt.top();//取出当前的路径
    				pathSt.pop();
    				if(node->left==NULL&&node->right==NULL)//如果是叶子节说明从根到叶子这条路径已经结束,就加入到result里
    				{
    					result.push_back(path);
    				}
    				//继续向下将右边子树加入到栈中
    				if(node->right)
    				{
    					treeSt.push(node->right);
    					pathSt.push(path+"->"+to_string(node->right->val));//这条是右子树的路径,即从path+当前遍历的结点右孩子
    				}
    				//左边子树加入到遍历栈中
    				if(node->left)
    				{
    					treeSt.push(node->left);
    					pathSt.push(path+"->"+to_string(node->left->val));//这条是左边子树的路径
    				}
    				
    			}	
    	return result;

    }
};

注意是有两条路径的,两个if语句里分别都加入各自的路径,一个是左子树的路径,一个是右子树的路径。
官方题解:
和随想录的类似,只不过随想录是用栈模拟递归,而官方是用两个队列来实现,一个保存遍历的树结点,一个保存路径,相当于是层次遍历,只不过每次在取出队列元素时,查看是否是叶子结点如果是,则将path队列的队头元素加入到result中,如果不是,则将该结点的所有孩子加入到队尾部。

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> paths;
        if (root == nullptr) {
            return paths;
        }
        queue<TreeNode*> node_queue;
        queue<string> path_queue;

        node_queue.push(root);
        path_queue.push(to_string(root->val));

        while (!node_queue.empty()) {
            TreeNode* node = node_queue.front(); 
            string path = path_queue.front();
            node_queue.pop();
            path_queue.pop();

            if (node->left == nullptr && node->right == nullptr) {
                paths.push_back(path);
            } else {
                if (node->left != nullptr) {
                    node_queue.push(node->left);
                    path_queue.push(path + "->" + to_string(node->left->val));
                }

                if (node->right != nullptr) {
                    node_queue.push(node->right);
                    path_queue.push(path + "->" + to_string(node->right->val));
                }
            }
        }
        return paths;
    }
};


404. 左叶子之和

题目
方法一:前序遍历
我的思路:
左叶子首先是叶子结点,所以肯定是左右孩子是空,其次它一定是双亲结点的左孩子,所以使用一个几点记录上一个结点,并且检查时pre要不为空!!

class Solution {
public:
    void DFS(TreeNode* root,TreeNode* pre,int&sum)
    {
        if(root==NULL) return;
        //stop
        if(pre!=NULL&&root->left==NULL&&root->right==NULL&&pre->left==root)
        {
           sum+=root->val;
           return;
        }
      
        DFS(root->left,root,sum);
        DFS(root->right,root,sum);
    }
    int sumOfLeftLeaves(TreeNode* root) {
       int sum=0;
       DFS(root,NULL,sum);
       return sum;

    }
};

看了随想录后写的前序遍历:
思路:也是利用左叶子的双亲来判断,只不过没有记录pre,直接使用root->left!=NUll&&root->left->leftnull&&root->left->rightNULL来判断的,相当于和链表一样,遍历一个结点的->next->next。
注意return那里注释

class Solution {
public:
    void DFS(TreeNode* root,int&sum)
    {
        if(root==NULL) return;
        //stop
        if(root->left!=NULL&&root->left->left==NULL&&root->left->right==NULL)
        {
           sum+=root->left->val;
          //return 这里不能写return,因为当前结点的左右孩子还没继续遍历,对比上面使用了pre的写法,因为上面那个直接是root本身就是叶子结点,所以不需要往下参与递归了,而这里的root不是叶子,还有左右孩子,所以不能判断完就return
        }
      
        DFS(root->left,sum);
        DFS(root->right,sum);
    }
    int sumOfLeftLeaves(TreeNode* root) {
       int sum=0;
       DFS(root,sum);
       return sum;

    }
};

方法二:后序遍历
随想录:
用递归函数的返回值来累加左叶子的。

class Solution {
public:
    int DFS(TreeNode* root)
    {
        if(root==NULL) return 0;
         int leftnum=DFS(root->left);
        int rightnum=DFS(root->right);
        int sum=0;
        if(root->left!=NULL&&root->left->left==NULL&&root->left->right==NULL)
        {
           sum+=root->left->val;
        }
        return sum+leftnum+rightnum;
      
       
    }
    int sumOfLeftLeaves(TreeNode* root) {
      
       return DFS(root);
      

    }
};

方法三:迭代前序遍历
看的随想录写的

class Solution {
public:
    int DFS(TreeNode* root)
    {
        if(root==NULL) return 0;
         int leftnum=DFS(root->left);
        int rightnum=DFS(root->right);
        int sum=0;
        if(root->left!=NULL&&root->left->left==NULL&&root->left->right==NULL)
        {
           sum+=root->left->val;
        }
        return sum+leftnum+rightnum;
      
       
    }
    int preOrder(TreeNode* root)
    {
        stack<TreeNode*>st;
        if(root) st.push(root);
        int sum=0;
        while(!st.empty()) 
        {
            TreeNode*node=st.top();
            st.pop();
            if(node->left!=NULL&&node->left->left==NULL&&node->left->right==NULL)
        {
           sum+=node->left->val;
        }
        if(node->right) st.push(node->right);
        if(node->left) st.push(node->left);
        }
        return sum;
    }
    int sumOfLeftLeaves(TreeNode* root) {
      
       return513.找树左下角的值 preOrder(root);
      

    }
};

官方题解还没看。。。。。

513. 找树左下角的值

题目
方法一:先序递归
看的随想录。自己没有想出来。
先序遍历保证了先访问的就是最左边的结点。不断更新深度,一直更新最大的深度的最左结点,最终返回的就是最后一层的最左节点。depth也体现了回溯。(简化了,直接写的depth+1)
思路复盘:
我的疑惑是如何在递归的过程中找出最大的深度,就是怎么实现递归过程中的比较,答案是设置全局变量。
对比404. 左叶子之和:
该题目和左叶子题目不同,这个是最左下角的叶子,如1 null 2,这里应该返回2,而左叶子那个题目则没有左叶子。本题本质是找最后一层的第一个结点。

class Solution {
public:
    int maxdepth=INT_MIN;
    //result也可以设置成全局变量,和最大的深度一样,用来在递归的过程中更新。
    void findleft(TreeNode *cur,int depth,int&result)
    {
       if(cur==NULL) return ;
       if(cur->left==NULL&&cur->right==NULL)
       {
           if(depth>maxdepth)
           {
             result=cur->val;
             maxdepth=depth;
           }
             
       }
       findleft(cur->left,depth+1,result);
       findleft(cur->right,depth+1,result);      
    }
    int findBottomLeftValue(TreeNode* root) {
         int result=0;
         findleft(root,0,result);
         return result;
    }
};

官方题解:
后续递归遍历,思想是一样的。

class Solution {
public:
    void dfs(TreeNode *root, int height, int &curVal, int &curHeight) {
        if (root == nullptr) {
            return;
        }
        height++;
        dfs(root->left, height, curVal, curHeight);
        dfs(root->right, height, curVal, curHeight);
        if (height > curHeight) {
            curHeight = height;
            curVal = root->val;
        }
    }

    int findBottomLeftValue(TreeNode* root) {
        int curVal, curHeight = 0;
        dfs(root, 0, curVal, curHeight);
        return curVal;
    }
};

方法二:迭代法层次遍历
随想录:
用result记录每一层的第一个左节点。最后一次更新的result就是所求的最后一层的第一个左节点。从这里也可以看出,左节点并不是左叶子,而是每一层的第一个左边的结点,不是真正的左叶子。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
         queue<TreeNode*> que;
         if(root) que.push(root);
         int result=0;
         
         while(!que.empty())
         {
            int size=que.size();
            result=que.front()->val;
            for(int i=0;i<size;i++)
            {
              TreeNode *p=que.front();
              que.pop();
              if(p->left)   que.push(p->left);
              if(p->right)  que.push(p->right);
             }
         }
           return result;
    }
};

官方题解:
先放右子树,再放左子树,这样就是从右向左遍历每一层的结点,遍历的最后一个结点就是每一层的最左结点。

class Solution {
public:
    int findBottomLeftValue(TreeNode* root) {
        int ret;
        queue<TreeNode *> q;
        q.push(root);
        while (!q.empty()) {
            auto p = q.front();
            q.pop();
            if (p->right) {
                q.push(p->right);
            }
            if (p->left) {
                q.push(p->left);
            }
            ret = p->val;
        }
        return ret;
    }
};

分析题目的逻辑:
看到本质是什么,然后将问题分为小问题,看是否可以在一个函数中解决这几个小问题。比如层次遍历的本质就是找到最后一层的第一个结点。将题目翻译成本质。
反思和启示:
反思:
不管是层序遍历还是递归,要找最后一层的逻辑和找每一层的逻辑都一样,因此是循环不变量。虽然不知道是不是最后一层,但是可以在循环时知道每一层,所以可以用一个循环外的变量不断去更新这个数值,那么循环到最后一层,result也就是最终的答案了。
这里为什么可以直接用先序或者层序遍历求?
因为树的遍历逻辑就是从左往右,先序,层序都是先遍历的左子树,所以恰好符合题目中的找”左边结点“
启示:当知道每一层的算法时,要找某一层,只需要循环这个逻辑,用一个变量不断去更新,用一个条件判断是不是符合题目条件即可!(本题题目条件是最后一层)

112. 路径总和

题目
方法一:前序递归
我的思路:
与257类似的思路,我是用了一个vector保存每一个路径的sum值,这里sum体现了回溯。然后再检查是否有与targetsum相同的。
特别注意回溯是回溯递归前一层的值,所以应该减去递归前一层的值。而不是本层递归的值。

class Solution {
public:
    //int sum=0; sum当全局变量也可以!!!!即回溯的变量可以当全局变量!!
    vector<int> v;
    void preOrder(TreeNode* root,int targetSum,int &sum)
    {
        sum+=root->val;
       if(root->left==NULL&&root->right==NULL)
       {
             v.push_back(sum);
             return;
       }
   
       if(root->left)
       {
          preOrder(root->left,targetSum,sum);
          sum-=root->left->val;//注意!!sum体现了回溯,不是sum-=root->val;因为sum进入递归后,返回来回溯退出去的应该是上一次加入的值,也就是递归里的root->left->val;这里与寻找从根节点到叶子结点的所有路径类似。
          
       }
       if(root->right)
       {
         preOrder(root->right,targetSum,sum);
         sum-=root->right->val;
       }
      
    }
    
    bool hasPathSum(TreeNode* root, int targetSum) {
        // if(root==NULL&&targetSum!=0) return false;
        // else if(root==NULL&&targetSum==0) return true;//为空和targetSum==0不是一回事
        if(root==NULL) return false;//注意因为第一次进递归就直接加入root->val,所以必须再进递归时加上root是否为空的判断。
        int sum=0;
        preOrder(root,targetSum,sum);
        for(int i=0;i<v.size();i++)
        {
            if(v[i]==targetSum)
              return true;
        }
        return false;
          

    }
};

看了随想录了写的:
思路:使用逆向思维采用减法的思想,遍历一个结点减去一个,这样少了sum这个变量,直接让targetSum作为回溯变量,并且因为targetSum是值传递,所以不需要再减去上一层的root->va;我写的这样其实并不是前序遍历,因为后面还有return语句

class Solution {
public:
    
    bool preOrder(TreeNode* root,int targetSum)
    {
        if(root==NULL) return false;
        targetSum-=root->val;
       if(root->left==NULL&&root->right==NULL)
       {
             if(targetSum==0)
               return true;
       }
         
       return preOrder(root->left,targetSum)||preOrder(root->right,targetSum);
      
    }
    
    bool hasPathSum(TreeNode* root, int targetSum) {
        
       return preOrder(root,targetSum);
      
          

    }
};

误区:
写成下面这样,虽然上面有root==null的判定,但是可能会出现空指针


       class Solution {
public:
    
    bool preOrder(TreeNode* root,int targetSum)
    {
        if(root==NULL) return false;
        targetSum-=root->val;
       if(root->left==NULL&&root->right==NULL)
       {
             if(targetSum==0)
               return true;
       }
          bool left=preOrder(root->left,targetSum);
          targetSum+=root->left->val;//root->left可能是空指针!!!

         bool right=preOrder(root->right,targetSum);
         targetSum+=root->right->val;
       
       return left||right;
      
    }
    
    bool hasPathSum(TreeNode* root, int targetSum) {
        
       return preOrder(root,targetSum);
      
          

    }
};

    

随想录:

class solution {
private:
    bool traversal(treenode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回

        if (cur->left) { // 左
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { // 右
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool haspathsum(treenode* root, int sum) {
        if (root == null) return false;
        return traversal(root, sum - root->val);
    }
};

方法二:迭代法
随想录
使用的是pair<TreeNode*,int>来记录每一个结点从根到该节点的sum值。

class Solution {
public:
    
    bool preOrder(TreeNode* root,int targetSum)
    {
        stack<pair<TreeNode*,int>> treest;
        if(root) treest.push({root,root->val});
        while(!treest.empty())
        {
            pair<TreeNode *,int> p=treest.top();
            treest.pop();
            if(targetSum==p.second&&p.first->left==NULL&&p.first->right==NULL)
              return true;           
           
            if(p.first->right)
            {
                //!!!注意这里不是p.second累加!!!而是加一个就行了!
            //    p.second+=p.first->right->val;
                treest.push({p.first->right, p.second+p.first->right->val});
                
            }
             if(p.first->left)
            {
               //  p.second+=p.first->left->val;
                treest.push({p.first->left,p.second+p.first->left->val});
               
            }
        }
      return false;
    }
    
    bool hasPathSum(TreeNode* root, int targetSum) {
        
       return preOrder(root,targetSum);
      
          

    }
};

关于递归返回值【随想录】

解释

可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树

确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

113. 路径总和 II

题目
这道题目是要遍历整个树,所以不需要返回值!而上面那个题目是只要有一个分支满足条件即bool类型就可以了,不需要遍历整个树,所以需要返回值。

class Solution {
public:
    vector<int> path;
    void preOrder(TreeNode* root,int targetSum,vector<vector<int>>&result)
    {
          targetSum-=root->val;
          path.push_back(root->val);
          if(targetSum==0&&root->left==NULL&&root->right==NULL)
          {
              result.push_back(path);
              return;
          }
          if(root->left)
          {
             preOrder(root->left,targetSum,result);
         //   targetSum+=root->left->val; 如果targetSum是引用的话则要体现回溯!!!!
             path.pop_back();
          }
          if(root->right)
          {
             preOrder(root->right,targetSum,result);
         //   targetSum+=root->right->val;
             path.pop_back();
          }
         
    }
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
       if(root==NULL) return {};
       vector<vector<int>> result;
       preOrder(root,targetSum,result);
       return result;
 
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值