十道二叉树面试题,对二叉树理解更进一步

学习二叉树有一个很重要的思想就是分治(将一个大问题划分为最小规模的子问题),常用方法之一就是递归,而递归是很抽象的,当对递归过程不理解时,建议多画图将递归过程实例化,拒绝人脑压栈。同时递归考虑边界和怎么去调用递归函数

根据二叉树创建字符串

原题:根据二叉树创建字符串
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。

空节点使用一对空括号对 “()” 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。
在这里插入图片描述

大致框架

**
 * 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:
    string tree2str(TreeNode* root);
};

先考虑可以省略括号的情况:

  1. 节点的right为空
  2. 节点的left和right都为空

不能省略括号就和以上情况相反

  1. 节点的right不为空
  2. 节点的left不为空或right不为空
class Solution {
public:
	//通过子函数完成字符串创建
    void _tree2str(TreeNode* root, string& str)
    {
        if (root == nullptr)
            return;
         //跟不需要括起来,直接插入
        str+=to_string(root->val);
        if (root->left || root->right)
        {
            str+='(';
            _tree2str(root->left, str);
            str+=')';
        }
        if (root->right)
        {
            str+='(';
            _tree2str(root->right, str);
            str+=')';
        }
    }
    string tree2str(TreeNode* root) {
        string str;
        _tree2str(root, str);

        return str;
    }
};

二叉树层序遍历(变形一)

原题:二叉树层序遍历
给你二叉树的根节点 root ,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)
在这里插入图片描述
大致框架

/**
 * 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>> levelOrder(TreeNode* root);
};

这道题就是层序遍历的变形,先来回顾一下层序遍历是如何实现的

void LevelOrder(TreeNode* root)
{
	if (root == nullptr)
		return;
	queue<TreeNode*> q;
	q.push(root);
	while (!q.empty())
	{
		TreeNode* front = q.front();
		cout << front->val << " ";
		
		//孩子进队列
		if (front->left)
			q.push(front->left);
		if (front->right)
			q.push(front->right);
		//父亲出队列
		q.pop();
	}
	cout<<endl;
}

上面不需要记录每层的节点个数,直接输出到控制台或是数组中即可,但这道题需要记录每层节点的个数,每层放在一个一维数组中,然后返回二维数组。所以我们只需要定义一个LevelSize记录每层的节点个数即可

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> q;
        if (root)
            q.push(root);
        int LevelSize=1;//第一层只有一个根节点,所以初始化为1
        vector<vector<int>> vv;
        while (!q.empty())
        {   
            vector<int> v;  
            //根据LevelSize的大小确定每一层的节点个数
            while (LevelSize--)
            {
                TreeNode* front = q.front();
                //pop之前将队头插入vector,再让它的左右孩子入队列
                v.push_back(front->val);
                if (front->left)
                    q.push(front->left);
                if (front->right)
                    q.push(front->right);
                q.pop();
            }
            //一层遍历完后,插入到二维数组中
            vv.push_back(v);
            //更新下一层节点个数
            LevelSize=q.size();

        }
        return vv;
    }
};

二叉树层序遍历(变形二)

原题:二叉树层序遍历ii
搞懂上面的层序遍历,这道题就非常简单了,题目说要倒着遍历,所以只需要正常遍历,最后reverse即可
在这里插入图片描述

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        vector<vector<int>> vv;
        queue<TreeNode*> q;
        if (root)
        {
            q.push(root);
        }
        int LevelSize=1;
        while (!q.empty())
        {   
            vector<int> v;
            while (LevelSize--)
            {
                TreeNode* front=q.front();
                //孩子进队列
                if (front->left)
                    q.push(front->left);
                if (front->right)
                    q.push(front->right);
                //父亲出队列
                v.push_back(front->val);
                q.pop();
            }
            vv.push_back(v);
            LevelSize=q.size();
        }
        reverse(vv.begin(), vv.end());
       return vv;
        
    }
};

二叉树的最近公共祖先(LCA)

原题:二叉树最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
在这里插入图片描述

大致框架

/**
 * 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 lowestCommonAncestor(TreeNode* root, int p, int q);
 };

通过画图可以发现,p,q的最近公共祖先就是两者到根节点路径上的交点,最近公共祖先有以下特征:

  1. 当一个节点的左子树出现节点p,右子树出现节点q,或左子树出现节点q,右子树出现节点p,那么该节点就是p,q的公共祖先
  2. 当然也有可能p,q的公共祖先就是p,q其中之一
    在这里插入图片描述

这题如果是搜索二叉树就很简单了
在这里插入图片描述

class Solution {
public:
     int lowestCommonAncestor(TreeNode* root, int p, int q) {
        if (root == nullptr)
            return -1;
         //一个在左一个在右,或者祖先是自己,就找到了
        if ((root->val>=p && root->val<=q)||(root->val<=p&&root->val>=q))
            return root->val;
         //都在左树,进左树找
        else if (root->val > p && root->val > q)
            return lowestCommonAncestor(root->left, p, q);
        else
        //都在右树,进右树找
            return lowestCommonAncestor(root->right, p, q);
    }
 
};

再回到本题中来
思路一:暴力破解,逐个查找,时间复杂度O(N2)

class Solution {
public:
	//查找root的子树是否存在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 || root == p || root == q)
            return root;
        //通过4个bool值,判断p,q在root的左子树还是右子树
        bool pInLeft, pInRight, qInLeft, qInRight;
        pInLeft = Find(root->left, p);//p在左子树,就不会再右子树,反之
        pInRight=!pInLeft;
        qInLeft = Find(root->left, q);//同理q
        qInRight = !qInLeft;
        //如果都在当前节点的左树,就往左子树找
        if (pInLeft && qInLeft)
            return lowestCommonAncestor(root->left, p, q);
         //同理
        else if (pInRight && qInRight)
            return lowestCommonAncestor(root->right, p, q);
        //一个在左子树,一个在右子树直接往回返
        else if ((pInLeft && qInRight) || (pInRight &&qInLeft))
            return root;
        else
        	return nullptr;
    }
};

思路二:时间复杂度O(N)

  • 采用后序遍历的思想,自底向顶回溯,p,q找到后不断往回返,回溯到公共祖先
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //走到空,或当前根为p或q就返回根
        if (root == nullptr || root == p || root == q)
            return root;
        //查找左子树是否存在p或q
        TreeNode* isLeft=lowestCommonAncestor(root->left, p, q);
        //查找右子树是否存在p或q
        TreeNode* isRight=lowestCommonAncestor(root->right, p, q);
        //如果当前节点的左右子树一个包含p,一个包含q,说明该节点是最近公共祖先
        if (isLeft && isRight)
            return root;
        //如果左子树不为空,说明p,q其中之一在该节点的左子树,直接往回返
        else if (isLeft)
            return isLeft;
        //右子树不为空,,说明p,q其中之一在该节点的右子树上,继续往回返
        else if (isRight)
            return isRight;
        else
        //都找不到就返回nullptr
            return nullptr;
    }
};

思路三:时间复杂度O(N)
如果这是一个三叉链的结构,就个指向父亲的指针就非常简单了,就是链表相交的问题。通过p,q两节点到根节点的路径,找到路径相交点,也就是p,q的公共祖先,采用先序思想,我们可以通过两个栈模拟实现,栈Pathq实现q到根节点的路径,栈Pathp实现p到根节点的路径
以p->value=6,q->value=7为例,公共祖先就是5
基本思想,以栈Pathq为例:

  1. 只要节点不为空,直接往栈里push
  2. 判断该节点的左右子树是否包含存在q
  3. 如果左右子树都不存在q就pop掉,再return false

在这里插入图片描述

class Solution {
public:
    bool SearchPath(TreeNode* root, TreeNode* node, stack<TreeNode*>& st)
    {
        if (root == nullptr)
            return false;
        st.push(root);//先入栈再判断
        if (root == node)
            return true;
        //在左树
        if (SearchPath(root->left, node, st))
            return true;
        //在右树
        if (SearchPath(root->right, node, st))
            return true;
        //左右都没有找到,证明不在当前子树中,pop掉
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr || root == p || root == q)
            return root;
        stack<TreeNode*> Pathp, Pathq;
        SearchPath(root, p, Pathp);
        SearchPath(root, q, Pathq);
        while (Pathp.size() != Pathq.size())
        {
            if (Pathp.size() > Pathq.size())
                Pathp.pop();
            else 
                Pathq.pop();
        }
        //长度相等时,找到相交路径
        while (Pathp.top() != Pathq.top())
        {
            Pathp.pop();
            Pathq.pop();
        }
        //返回其中一个即可
        return Pathp.top();
    }
};

二叉搜索树转排序双向链表

原题:二叉树与中序链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示
在这里插入图片描述在这里插入图片描述

大致框架

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
	TreeNode* Convert(TreeNode* pRootOfTree);
};

对搜索二叉树不了解的可以看这篇文章:二叉搜索树以及K模型与KV模型
首先分析题目要求:

  1. 要排升序
  2. 将节点的left当做双链表的prev指向前一个节点
  3. 将节点的right当做双链表的next指向下一个节点

而二叉搜索树中序遍历就可以排升序,所以只需要在中序递归遍历过程中更改节点的链接关系即可。定义一个prev初始化为nullptr,这样就可以让当前根节点(定义为cur),cur->left指向prev,那如何让cur->right指向下一个节点呢?以下图为例
下一个节点肯定是无法预料到的,就像我们不知道明天会发生什么,但我们知道昨天发生了什么啊,所以可以让prev->right指向cur
ps:递归prev肯定要传引用,因为prev每次都要发生变化,每次递归调用之前让prev走到cur的位置,同时需要判断prev是否为空
在这里插入图片描述

class Solution {
public:
    //中序转换
    void InOrderConvert(TreeNode* cur, TreeNode*& prev)
    {
        if (cur == nullptr)
            return;
        //左
        InOrderConvert(cur->left, prev);
        //根:访问的节点依次为4 6 8 10 12 14 16
        cur->left=prev;
        if (prev)
            prev->right=cur;
        prev=cur;
        //右
        InOrderConvert(cur->right, prev);
    }
    TreeNode* Convert(TreeNode* pRootOfTree) {
        if (pRootOfTree == nullptr)
            return nullptr;
        TreeNode* prev=nullptr;
        InOrderConvert(pRootOfTree, prev); 
        //找到链表的头
        TreeNode* head = pRootOfTree;
        while (head->left)
        {
            head=head->left;
        }
        
        return head;
    }
};

中序+前序构造二叉树

原题:中序+前序构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
在这里插入图片描述大致框架

/**
 * 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* buildTree(vector<int>& preorder, vector<int>& inorder);
};

当中序和前序数组中无重复元素时,那我们就可以根据这两个数组建树

  1. 根据前序可以确定根
  2. 确定根后,可以将中序数组划分为左子树     右子树

以示例一为例在这里插入图片描述

class Solution {
public:
    TreeNode* _bulidTree(vector<int>& preorder, vector<int>& inorder, int& index
    , int inbegin, int inend)
    {   
       // 区间不存在时,返回空
        if (inbegin > inend)
            return nullptr;
        //根
        TreeNode* root=new TreeNode(preorder[index]);
        //确定左子树  根 右子树
        int rooti=inbegin;//找到根在inorder中的位置
        while (inorder[rooti] != preorder[index])
        {
            ++rooti;
        }
        ++index;//往后走
        //确定区间[inbegin, rooti-1] rooti  [rooti+1, inend]
        root->left=_bulidTree(preorder,inorder,index,inbegin,rooti-1);
        root->right=_bulidTree(preorder,inorder,index,rooti+1,inend);

        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int index=0;//preorder下标从0开始,传引用
        return _bulidTree(preorder, inorder, index, 0, inorder.size()-1);
    }
};

当然如果搞一个hash表存中序数组的元素和对应下标可以减少查找

中序+后序构造二叉树

原题:中序+后序构造二叉树
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

在这里插入图片描述这道题和上面的类似,是通过中序和后序建树的,后序遍历顺序为:左子树->右子树->根,所以需要从右往前确定根然后再确定右子树的根

class Solution {
public:
     TreeNode* _bulidTree(vector<int>& inorder, vector<int>& postorder, int& posti  
      , int inbegin, int inend)
    {
        if (inbegin > inend)
            return nullptr;
        //根
        TreeNode* root=new TreeNode(postorder[posti]);
        //确定左子树  根 右子树
        int rooti=inbegin;
         while (inorder[rooti] != postorder[posti])
        {
            ++rooti;
        }
        --posti;//往前走
        //确定区间[inbegin, rooti-1] rooti  [rooti+1, inend]
        root->right=_bulidTree(inorder,postorder, posti, rooti+1,inend);
        root->left=_bulidTree(inorder,postorder, posti,inbegin,rooti-1);

        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int posti=postorder.size()-1;//postorder下标从最后开始
        return _bulidTree(inorder, postorder, posti, 0, inorder.size()-1);
    }   
};

所以中序+前序和后序其中之一就可以建树,如果是前序+后序是无法构造的,因为无法区分树的左右子树
ps:当然如果给定数组有标识符表示空节点,那么前后序其中之一就可以建树

前序非递归实现

原题:前序遍历
在这里插入图片描述
先来回顾一下前序遍历的顺序:先访问根 ,再访问左子树,最后访问右子树。
一颗不为空的树除了可以划分为跟,左子树,右子树外,还可以划分为左路节点+左路节点的右子树。
具体步骤:

  1. 依次访问左路节点3 9 15,然后再从后往前依次访问左路节点的右子树,所以可以用一个栈模拟实现,边访问左路节点变压栈
  2. 那如何访问左路节点的右子树呢?以子问题的形式访问右子树,如何访问左路节点的就如何访问右子树

在这里插入图片描述

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* cur = root;
        //cur不为空说明还有节点没有push入栈
        //栈不为空说明还有节点的右子树没有访问
        while (cur || !st.empty())
        {
            //左路节点入栈
            while (cur)
            {	
            	v.push_back(cur->val);
                st.push(cur);
                cur=cur->left;
            }
            TreeNode* top = st.top();    
            st.pop();
            //以子问题的形式访问右子树,如何访问左路节点就如何访问右子树
            cur=top->right;
        }
        return v;
    }
};

中序非递归实现

原题:中序遍历

 	中序遍历顺序:先访问左子树,再访问根,最后访问右子树,和前序非递归类似
在这里插入图片描述

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* cur = root;
        while (cur || !st.empty())
        {
            //左路节点入栈
            while (cur)
            {
                st.push(cur);
                cur=cur->left;
            }
            TreeNode* top = st.top();
            v.push_back(top->val);
            st.pop();
            //以子问题的形式访问右子树,如何访问左路节点就如何访问右子树
            cur=top->right;
        }
        return v;
    }
};

后序非递归实现

原题:后序遍历
前中后序遍历的不同点就是访问根节点的时机不同,而前中序都有一个特点,都是最后访问右子树,而我们把左路节点压栈后,很容易就可以实现最后访问右子树。
而后序遍历需要先访问左子树,再访问右子树,最后访问根。也就是说需要先访问完右子树才能访问根,以下图为例,访问完15后还不能访问9需要先访问9的右子树7才能访问9
ps:这里因为9的右子树只有一个7,而7的左右子树都为空,实际上是最后访问完7再访问的9,那就可以定义一个prev初始化为nullptr,每访问一个节点就记录这个节点,所以当栈顶的top为9时且prev为7时证明已经访问过7了也就是右子树,就可以访问根9了
在这里插入图片描述

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* prev = nullptr;
        TreeNode* cur = root;
        while (cur || !st.empty())
        {
            //左路节点入栈
            while (cur)
            {
                st.push(cur);
                cur=cur->left;
            }
            TreeNode* top = st.top();
            //当右树为空或右树已经访问完了就可以访问根
            if (top->right == nullptr || top->right == prev)
            {
                v.push_back(top->val);
                st.pop();
                //记录访问的节点
                prev=top;
            }
             //以子问题的形式访问右子树,如何访问左路节点就如何访问右子树
            else
                cur=top->right;
        }
        return v;
    }
};
  • 39
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 41
    评论
Python二叉树面试题有很多种,以下是其中几个常见的面试题: 1. 二叉树的最大深度:这个问题要求计算给定二叉树的最大深度。可以使用递归的方法来解决,递归函数的定义是返回当前节点的深度,递归终止条件是节点为空时返回0,递归过程中比较左右子树的深度并返回较大值加1。时间复杂度为O(n),空间复杂度为O(n)。 2. 二叉树的前序遍历:这个问题要求按照前序遍历的顺序输出二叉树的节点值。可以使用递归或迭代的方法来解决。递归方法的思路是先输出当前节点的值,然后递归遍历左子树,最后递归遍历右子树。迭代方法可以使用栈来辅助实现,把根节点压入栈中,然后循环弹出栈顶节点,输出其值,并将其右子节点和左子节点依次压入栈中。时间复杂度为O(n),空间复杂度为O(n)。 3. 二叉树的层序遍历:这个问题要求按照层序遍历的顺序输出二叉树的节点值。可以使用队列来辅助实现。首先将根节点入队,然后循环直到队列为空,每次从队列中取出一个节点,输出其值,并将其左右子节点依次入队。时间复杂度为O(n),空间复杂度为O(n)。 以上是几个常见的Python二叉树面试题的解法,根据具体的问题要求和输入条件选择合适的解法即可。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [【编程之路】面试必刷TOP101:二叉树系列(23-30,Python实现)](https://blog.csdn.net/be_racle/article/details/125531755)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [【面试题8】二叉树的下一个节点](https://download.csdn.net/download/weixin_38589168/14035034)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

寄一片海给你

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

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

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

打赏作者

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

抵扣说明:

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

余额充值