【数据结构与算法】总结关于二叉树题型经典面试题

1.根据二叉树创建字符串

在这里插入图片描述
在这里插入图片描述

解题思路:
1.本质上考察的还是前序遍历,只不过要求加上括号,将子树括起来。
2.在递归到左子树之前要加上 ( 括号,在左子树递归结束后,要加上 )括号。
3.在递归右子树之前要加上 ( 括号,在右子树递归结束后,要加上 )括号。
4.题目要求空括号要省略,即为空结点点的位置就不需要再递归进去。(递归前后就将括号加上了)。所以只有不为空结点的位置才需要递归进去。
5.题目还有要求当左子树为空,而右子树不为空时,左子树为空的位置需要加上括号。

class Solution {
public:
      string tree2str(TreeNode* root) {
      if(root==nullptr)
      return "";
      //如果root为空,直接返回空字符串
      string str=to_string(root->val);//要将结点里的数据变成string类型
      
      //相当于前序遍历打印
      if(root->left||root->right)//①当左子树存在时,需要打印括号② 当左子树不存在时,右子树存在,则需要打印
      {
      //递归之前加上(
      str+='(';
      str+=tree2str(root->left);
      str+=')';
      //递归之后加上)
      }
      //当第一个条件为假时才会判断第二个条件,所以当左子树为空,右子树不为空时,也需要打印
      

     if(root->right)//当右子树存在时,需要打印
     {
      str+='(';
      str+=tree2str(root->right);
      str+=')';
     }
     

      return str;
    }
};

2.二叉树的层序遍历

在这里插入图片描述

解题思路
1.二叉树的层序遍历需要使用到队列。
2.题目要求将每层的结点输入到一个vector<vector<.int>>> vv数组里
3.首先判断root是否为空,不为空时,直接将root结点入栈。
4.如何一行一行的获取各个结点呢?每pop掉一个结点,就要将它的左右结点入队列。比如pop掉根结点之后,就要将根节点的左右子树入队列。(当左孩子不为空时入队列,当右孩子不为空时再入队列)
4.根据队列的大小,来确定二叉树每层结点的个数。队列里有几个结点,就循环pop几次,每次pop完,要将pop掉的结点的左右子结点再入队列。循环几次,就会将结点的左右子树都入队列中来,所以这样就可以根据队列中元素的多少确定每层的结点个数。
5.每次pop前将队列里的结点值插入到vector<.int> v数组里,然后再将每层有的元素插入到vv里。
6.当队列为空时,则结束循环。

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> vv;
    queue<TreeNode*> q;

    if(root)
    q.push(root);
    //首先将根结点入栈

    //这时栈里就不为空了
    while(!q.empty())
    {

         vector<int> v;
        //根据栈里元素的多少来确定每层的个数
        int num=q.size();
        for(int i=0;i<num;i++)
        {
            TreeNode* top=q.front();
            v.push_back(top->val);
            //每层的元素插入到v里
             q.pop();
           //将元素pop掉后就要将这个元素的左右孩子入栈
           if(top->left)
           q.push(top->left);
           if(top->right)
           q.push(top->right);
            
        }
        //将每层的v插入到vv里
        vv.push_back(v);
    }
    return vv;
    }
};

3.二叉树的最近公共祖先

在这里插入图片描述

解题思路①
1.如何判断某个结点是否是两个指定结点的最近公共祖先呢?
2.当指定的两个结点在当前结点的两侧时(左边和右边),就可以判断当前结点就是最近公共祖先。 或者有一个结点是根结点就可以判断该结点是公共祖先。
3.需要写一个查找FInd函数,用来判断结点是否在这颗树。
4.定义4个判断变量,分别是pinleft ,pinright, qinleft,qinright。用来查找p,q是否在左树还是在右树。
5.所以当两个在同一侧树时,公共祖先就不可能是另外一颗树。就直接递归到这一侧去找。比如同时在左边,则递归到左子树去找,同时在右边,则递归到右子树去找。当两个在两次时,直接返回该结点,该节点就是最近公共祖先。
6.时间复杂度O(N*2)

还有一种方法可以将时间复杂度提升到O(N)程度

解题思路②
1.利用栈将结点的路径(从根节点到该结点的路径)给找到并存起来,那么两个结点的路径中的交点,就是公共祖先。就相当于转化为链表相交问题了。
2.就是Find查找过程中将结点入栈。当不是所找结点的路径时就出栈。先入栈,再比较。
3.如果当前结点为空,直接返回false。
4.如果当前结点不为空,先入栈,然后进行比较。如果当前结点是所找结点,直接返回true。
5.走到这里说明,当前结点不是所找结点,那么就需要递归到左子树去找,如果在左子树里,那么直接返回true。
6.走到这里说明,所找结点不在左子树,那么就递归到右子树去找,如果在右子树,那么就直接返回true。
7.走到这里说明,所找结点也不知右子树,这说明当前结点和左右子树都没有,那么该结点肯定不是所找结点的路径,可以将其从栈中pop掉。然后返回false。
8.将两个结点的路径存到两个栈里后,根据链表相交解题原理,让长的路径先走长度差,这里直接pop长度差次。
9.然后比较栈里相同的结点就是公共祖先。

在这里插入图片描述

 // 最近公共祖先--》 孩子在左右两侧的结点就是最近公共祖先
class Solution {
public:
// 查找函数,用来查找node结点是否在该树里
    bool Find(TreeNode* root, TreeNode* node)
    {
        if(root==nullptr)
        return false;
        
        if(root==node)
        return true;

        return Find(root->left,node)||Find(root->right,node);

    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if(root==nullptr)
    return nullptr;
    //其中一个结点为根节点直接可以返回根节点,根据定义可以找到根节点就是公共祖先
    if(root==p||root==q)
    {
        return root;
    }
    //到这里说明p 或 q不是根结点,所以要么在左子树或者在右子树
    //需要写一个能查找p q结点是在左右子树那一步的函数
    bool pinleft,pinright,qinleft,qinright;

    pinleft=Find(root->left,p);//查找p是否在左子树,不在左子树就在右子树
    pinright=!pinleft;

    qinleft=Find(root->left,q);//查找q是否在左子树,不在左子树就在右子树
    qinright =!qinleft;
     
     //p,q两个都在左子树
    if(pinleft&&qinleft)
    {
        return lowestCommonAncestor(root->left,p,q);
    } 
    else if(pinright&&qinright)//p,q两个都在右子树
    {
        return lowestCommonAncestor(root->right,p,q);
    }
    else//p,q两个在两侧
    {
        return root;//直接返回该节点。
    }
    }
};

解题②:

//特殊树会怎么样? 三叉链 二叉搜索树?
 //通过这个题目L:理解 通过Find查找这个过程,在加入一个栈, 可以将一个普通二叉树的路径搞出来
class Solution {
public:
      //查找某个结点的同时将该结点的路径放入栈里
    bool Path(TreeNode* root, TreeNode*node, stack<TreeNode*>& st)
    {
        if(root==nullptr)
        return false;

        //当前结点不为空,首先需要入栈
        st.push(root);
        //入栈以后再进行比较
        if(root==node)
        return true;

        //走到这里表明,当前结点并不是所找的结点,那么就需要递归到左子树去找
        if(Path(root->left,node,st))//如果为真,则说明该结点在左子树,如果为假的,就不走这里。递归到右子树去找
        {
          return true;
        }
        if(Path(root->right,node,st))//如果为真,则说明该结点在右子树,如果为假的,则说明该结点不在右子树
        {
            return true;
        }

        //能走到这里说明,当前结点的左右子树都没有所要找的结点,则说明该结点不可能是所找结点路径
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pPath,qPath;
        Path(root,p,pPath);
        Path(root,q,qPath);
      //因为不知道谁的路径长,所以这里两个都比较一下,只会走一个。
        while(pPath.size()>qPath.size())
        {
            pPath.pop();
        }
        while(qPath.size()>pPath.size())
        {
            qPath.pop();
        }

        while(qPath.top()!=pPath.top())
        {
            pPath.pop();
            qPath.pop();
        }

        return pPath.top();
    }
};

4.二叉搜索树与双向链表

在这里插入图片描述

解题思路
1.题目要求二叉搜索树转化成一个排序好的双向链表,那么这里肯定要走中序,因为中序才可以有序。
2.题目要求只能在原树上改动,不能开辟空间。
3.那么就写一个特殊的中序遍历走完,就变成双向链表。
4.中序遍历,左 根 右,这里递归到左子树和递归到右子树是肯定的,要操作的地方就在于根这里
5.要求将左指针变成前驱,右指针变成后继,首先我们要明白,当前结点的右指针指向我们是无法明确的,就像我们不知道明天会发生什么,但左指针我们是可以明确的,就是前一个结点,所以我们可以先将各个结点的左指针改成前驱。要知道前一个结点的位置,我们就要在递归之前记录这个结点。这里我们再传一个参数prev,用来记录每个结点的前一个位置。一开始给nullptr。(注意这个参数需要给引用,因为想要让下一个结点知道上一个结点的改动,就需要用到引用)
6.所每递归到一个结点,就让这个结点的左指针指向prev(也就是前一个结点的位置)。
7.虽然我们不知道每个结点的右指针指向哪里,但我们知道每个结点前一个结点的右指针指向哪里呀,每一个结点的前一个结点的右指针就指向当前结点。我们可以穿越回去,让前一个结点的右指针自己。
8.所以左指针的指向可以在当前结点完成,而右指针的指向需要穿越到前一个结点完成,也就是当前结点能完成左指针的指向,而右指针的指向只能完成前一个结点。
9.题目要求获取双向链表的第一个结点,在二叉树里即最小结点,直接返回最左边结点即可。

class Solution {
public:
    void Inoder(TreeNode* cur,TreeNode* &prev)//prev用来记录当前结点的前一个结点
	{
        if(cur==nullptr)
		return ;

		//走中序遍历
		Inoder(cur->left,prev);
        //根 ->有序
		cur->left=prev;//当前结点的左指针我们是知道的,但右指针是不知道的

		//当前结点是无法知道后一个指针指向那里的,但我可以穿越到前一个结点,将前一个结点的右指针指向我自己,那么前一个结点的右指针就链接上了
      
	   if(prev!=nullptr)
		prev->right=cur;
        //prev要求改变,在整个递归中只有以一个prev

      //在每次递归之前,记录当前结点的位置
		prev=cur;
		//想要让下一次递归看到上一次的改变,就需要用引用,不然看不到
		
     //左指针指向是在当前结点完成的,而右指针的指向是在后一个结点完成的。
		Inoder(cur->right,prev);
	}
    TreeNode* Convert(TreeNode* pRootOfTree) {
        TreeNode* prev=nullptr;
		Inoder(pRootOfTree,prev);

		TreeNode* head=pRootOfTree;
		//返回二叉树中最左结点,这里还有要注意,head必须是不为空才可以走这个循环,不然head为空,就没有左指针
		while(head&&head->left)
		{
			head=head->left;
		}
		return head;
    }
};

5.从前序与中序遍历序列构造二叉树

在这里插入图片描述

解题思路
1.利用前序方式构建二叉树。即首先创建结点,然后递归左子树,递归右子树。
2.怎么创建呢?我们根据前序遍历可以确定根结点,根据中序遍历可以确定根结点的左右区间。一旦知道左右区间我们就可以递归了。
3.首先根据前序遍历创建结点,然后到中序遍历里找到该根节点,并确定其左右区间。
4.然后根据左区间,递归到左子树。根据右区间,递归到右子树。
5.题目给的函数参数不满足我们所需,所以创建一个子函数,我们需要前序数组的下标,需要中序数组的区间。前序的下标要给引用,因为在递归中一直走的都是这个数组。

class Solution {
public:

     //原理:根据前序确定根,中序确定根的左右区间,利用前序遍历方式创建结点
     //需要子函数,唯一我们还需要两个数组的下标

     TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder,int& prei,int begin,int end)
     {
         //当还右一个结点也要创,当没有结点了,就返回
         if(begin>end)
         return nullptr;
            //前序不断确定根节点,通过中序丘吉尔的左右区间

            TreeNode* newnode=new TreeNode(preorder[prei]);//根据前序确定根,创建根结点

            //再确定该结点在中序中的位置
            int pos=begin;
            while(pos<=end)
            {
                if(preorder[prei]==inorder[pos])
                break;
                else
                ++pos;
            }
            //中序确定左右子树区间
            //[begin   pos-1]  pos  [pos+1,   end ]
            ++prei;
            //确定左右区间后,就可以递归创建左右子树
             newnode->left=_buildTree(preorder,inorder,prei,begin,pos-1);

             newnode->right=_buildTree(preorder,inorder,prei,pos+1,end);
             return newnode;

     }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int x=0;
         return _buildTree(preorder,inorder,x,0,inorder.size()-1);
    }
};

6.从中序与后序遍历序列构造二叉树

在这里插入图片描述

解题思路
1.与上一题类似,只不过这里根据后序和中序来构建二叉树。
2.这里要注意的是后序根节点在哪呢?后序,左右根,最后一个才是根节点,并且根节点前一个是右子树。
3.所以我们在创建完结点后,先递归走的是右子树然后再递归左子树。
4.根据后序确定根节点,根据中序确定结点的左右区间。
5.类似于前一个,需要创建一个子函数,用来获取想要的参数,后序的下标,中序的区间。
后序从最后一个开始,依次往前走。

lass Solution {
public:
//后序遍历 :左 右  根
//后序确定根结点,中序确定左右区间,  创建完根,递归应该先递归创建右子树,再创建左子树

     TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& posi,int begin,int end)
     {
         if(begin>end)
         return nullptr;
             //根据后序最后一个先确定根结点
             TreeNode* newnode =new TreeNode(postorder[posi]);

             //从后序中确定根结点后,再从中序中找到这个结点,从而确定左右区间
             int j=begin;
             while(j<=end)
             {
                 if(inorder[j]==postorder[posi])
                 break;
                 else
                 ++j;
             }
             //这时结点的左右区间就分割出来了
             //[begin  j-1] j  [j+1,  end]
             --posi;
             //首先递归创建右子树
             newnode->right=_buildTree(inorder,postorder,posi,j+1,end);
             //然后再递归创建左子树
             newnode->left=_buildTree(inorder,postorder,posi,begin,j-1);
             return newnode;
     }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int i=postorder.size()-1;
      return _buildTree(inorder,postorder,i,0,inorder.size()-1);

    }
};

7.二叉树的前序遍历(非递归方法)

在这里插入图片描述

解题思路
1.将一颗树看成两部分,左路结点和右子树。
2.前序遍历,我们可以确定最先被访问的是左路各个结点,然后就是从最下面的左路结点的右子树,访问完再访问上一个左路结点的右子树,依次类推。而要实现这样最下面的左路结点的右子树最先被访问,需要用到栈。
3.首先将左路结点全部入栈。(左路结点在入栈之后就已经被访问完了,然后就开始要访问右子树)栈里一旦有元素就说明有右子树要被访问。要访问右树之前先将左路结点pop掉。
4.怎么访问右子树呢?子问题转化,可以将右子树再看成由左路结点和右子树构成,每一个右子树都可以看成由左路结点和右树构成。
5.题目要求放入一个数组里,在访问完结点后,就将结点里的值放入数组里,而栈里的结点是用来找左路结点对应的右树的。

在这里插入图片描述

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
      stack<TreeNode*> st;
      vector<int> v;
      TreeNode* cur=root;
      //用cur表示要一开始要访问的树
      //当栈里还有元素,表明还有右路需要访问
      //在访问右子树之前就会将左路结点pop掉,如果不加上cur的话,最后一个左路结点的右树就不会被访问到,因为这时,栈已经空了。
      while(cur||!st.empty())
      {
          //首先需要将左路结点不断入栈
          while(cur)
          {
              st.push(cur);
              v.push_back(cur->val);
              //将访问完的结点值放入数组里
              cur=cur->left;
          }

          //栈里结点是用来找右路的
          //首先将栈里元素取出来,然后pop掉
          TreeNode* Top =st.top();
          st.pop();

          //用子问题的方式去访问右路
          cur =Top->right;
      }
      return v;
    }
};

8.二叉树的中序遍历(非递归方法)

在这里插入图片描述

解题思路
1.与前序不同的是访问结点的时机不同,前序是根左子树右子树,而中序是先左子树再根然后右子树。
2.前序在入栈之后就访问完根结点了,而中序虽然同样是把左路结点都入栈,在入栈后并不是真正的访问根结点,只有当将左路结点从栈里取出来时,才是真正的访问到这个结点。而取出来这个结点说明它的左路已经被访问完了。比如最下面的左路结点一开始入栈并不是真正访问到,当出栈说明它的左路已经被访问完,它的左路就是空结点,直接可以访问到根结点了(因为左中右顺序)。而访问到根结点后,就可以将根节点值放入数组里。
3.前序中序后序其中本质上就是访问结点的时机不同而已。
4.当根结点访问完,就可以访问右子树了,右子树如何访问呢?子问题转化!

 //先将左路入栈
 //当栈里元素被取出,表明左路结点已经被访问。
 //然后就访问左路结点的右树
 //与前序本质就是访问左路结点的时机不同,前序是在入栈之前就访问了,而中序是在入栈取出之后才访问完。
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
      stack<TreeNode*> st;
      vector<int> v;
      TreeNode* cur=root;
      //用cur表示要一开始要访问的树
      //当栈里还有元素,表明还有右路需要访问
      while(cur||!st.empty())
      { 
          //将左路全部入栈
            while(cur)
            {
                st.push(cur);
                cur=cur->left;
            }
          //首先将栈里元素取出来,然后pop掉,这时才访问完左路
          TreeNode* Top =st.top();
          st.pop();
          v.push_back(Top->val);
          //访问完根结点后就开始访问右树
          //用子问题的方式去访问右路
          cur =Top->right;
      }
   
      return v;
    }
};

9.二叉树的后序遍历(非递归方法)

在这里插入图片描述

解题思路
1.与前序中序不同的是访问结点的时间不同,后序是左右根,根结点是最后再访问。
2 前序是将左路入栈之后就访问完了,中序将左路入栈后并不是真正的访问,而当左路被取出来后才是真正的访问到结点。而后序将左路入栈后不是真正的访问,将左路再取出来时,也不是真正的访问这个结点,而还需要再访问这个左路结点的右树后,回来才算真正的访问到这个结点。而如何判断右树是否被访问过了呢?当右树没有被访问时,前一个访问的结点是谁?(左路结点)当右树被访问完时,前一个访问的结点是谁?(右树结点),所以根据上一次访问的结点是否是右树结点来判断是否访问完。
3.当右树为空时,可以直接去访问这个结点了。当右数不为空时,那么先去访问右子树,如何访问右子树?子问题转化。
4.而当右子为空可以访问这个结点,或者当右树被访问完后,也就可以访问这个结点了。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
     stack<TreeNode*> st;
      vector<int> v;
      TreeNode* cur=root;
      TreeNode*prev=nullptr;
      while(cur||!st.empty())
      {
          //首先将左路全部入栈
          while(cur)
          {
              st.push(cur);
              cur=cur->left;
          }
          //当栈里元素取出来时,表明这个结点的左路已经访问完,而要访问这个结点需要判断右路如何
          TreeNode* top=st.top();
          //右路结点为空,或者上次访问的结点为右路时,则可以访问该结点
          if(top->right==nullptr||prev==top->right)
          {
              v.push_back(top->val);
              //要记录一下上一次访问的结点是哪一个
              st.pop();
               prev=top;
          }
          else
          {
              cur=top->right;
          }

        
      }
      return v;
    }
};
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值