23.二叉树进阶面试题

1.606. 根据二叉树创建字符串 - 力扣(LeetCode)

思路:前序遍历创建字符串,注意点:(左子树不为空)或者(左子树为空,右子树不为空),左子树要加();右子树不为空,右子树要加()。

class Solution {
public:
    string tree2str(TreeNode* root) {
        string str;
        if(root==nullptr)
            return str;
        str += to_string(root->val);
        //左不为空或者左为空,右不为空,左加()
        if(root->left || root->right)
        {
            str += "(";
            str += tree2str(root->left);
            str += ")";
        }
        //右不为空,右加()
        if(root->right)
        {
            str += "(";
            str += tree2str(root->right);
            str += ")";
        }
        return str;
    }
};

2.102. 二叉树的层序遍历 - 力扣(LeetCode)和 107. 二叉树的层序遍历 II - 力扣(LeetCode)

思路:用一个levelSize记录每层节点的数量,按层数出,达到分层遍历的效果。

102解答:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        if(root==nullptr)
            return vv;
        queue<TreeNode*> q;
        q.push(root);
        int levelSize = 1;
        while(!q.empty())
        {
            vector<int> v;
            while(levelSize--)
            {
                TreeNode* tmp = q.front();
                q.pop();
                if(tmp->left)
                    q.push(tmp->left);
                if(tmp->right)
                    q.push(tmp->right);
                v.push_back(tmp->val);
            }
            vv.push_back(v);
            levelSize = q.size();
        }
        return vv;
    }
};

107解答:在return vv; 前,将vv逆置一下,reverse(vv.begin(),vv.end());

3.236. 二叉树的最近公共祖先 - 力扣(LeetCode) 

思路一:最近公共祖先,两个子节点一定分布在它的左右子树。

还有种特殊情况,如果一节点是另一节点的祖先,那么该节点也是两节点的最近公共祖先。

实现过程:根节点位置,如果其中一个节点等于根节点,说明根结点就是最近公共祖先,直接返回根节点;若两个节点都不等于根结点,从左右子树去找两个节点,(如果同时在左子树/右子树,就说明最近公共祖先在左子树/右子树,就递归去对应的左子树/右子树找;如果一个节点在左子树,另一个节点在右子树,就说明根节点就是最近公共祖先,返回根结点)

class Solution {
public:
    bool find(TreeNode* root,TreeNode* x)
    {
        if(root==nullptr)
            return false;
        if(root==x)
            return true;
        return find(root->left,x) || find(root->right,x);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==nullptr)
            return nullptr;
        if(root==p || root==q)
            return root;
        bool pInLeft = find(root->left,p);
        bool pInRight = !pInLeft;
        bool qInLeft = find(root->left,q);
        bool qInRight = !qInLeft;
        //1.p,q在不同的左右子树,root就是最近公共祖先
        //2.p,q都在左子树,递归左子树
        //3.p,q都在右子树,递归右子树
        if((pInLeft&&qInRight)||(pInRight&&qInLeft))
        {
            return root;
        }
        else if(pInLeft&&qInLeft)
        {
            return lowestCommonAncestor(root->left,p,q);
        }
        else 
        {
            return lowestCommonAncestor(root->right,p,q);
        }
    }
};

但是这种实现有一个弊端,如果结构形如:

那么由于查找时间为O(N),总体时间复杂度为O(N^2),如果是一颗满二叉树,时间复杂度为O(N*logN)。

思路2:通过前序遍历加栈,实现存储节点到根节点的路径;得到两条路径后,把这两条路径转换成“链表相交”的模型,最近公共祖先就是交点。

实现过程:如果root为nullptr,返回false;前序遍历root,遇到节点先入栈,然后判断是否等于要找的节点x。若等于,返回true;若不等于,继续递归找左子树和右子树(此时不回退,因为root可能是路径上的一个节点);如果左右子树都没有,那么删除栈pop(),回溯,返回false。

class Solution {
public:
    bool getPath(TreeNode* root,stack<TreeNode*>& st,TreeNode* x)
    {
        if(root==nullptr)
            return false;
        st.push(root);
        //找到了节点,路径完整了
        if(st.top()==x)
            return true;
        //在左子树或右子树找到了,路径完整了
        if(getPath(root->left,st,x) || getPath(root->right,st,x))
            return true;
        //没有找到,回退root
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pst;
        stack<TreeNode*> qst;
        getPath(root,pst,p);
        getPath(root,qst,q);
        //让长的栈和短的栈一样长
        while(pst.size()!=qst.size())
        {
            if(pst.size()>qst.size())
            {
                pst.pop();
            }
            else
            {
                qst.pop();
            }
        }
        //找交点
        while(pst.top()!=qst.top())
        {
            pst.pop();
            qst.pop();
        }
        return pst.top();
    }
};

此算法的时间复杂度为O(N)。

4.二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com)

思路:用两个节点指针prev和cur,prev初始化成nullptr,cur初始化成根。cur指向当前中序遍历的节点,prev指向当前中序遍历节点的上一个节点,每次走,cur的前驱(left)指向prev,prev的后继(right)指向cur,直到cur走到空,此时prev为中序最后一个。找到中序的第一个,返回。

图中顺序为中序遍历的链接顺序。

class Solution {
public:
	void Inorder(TreeNode* cur,TreeNode*& prev)
	{
		if(cur==nullptr)
			return;
		Inorder(cur->left,prev);
		if(prev)
			prev->right = cur;
		cur->left = prev;
		prev = cur;
		Inorder(cur->right,prev);
	}
    TreeNode* Convert(TreeNode* pRootOfTree) {
		if(pRootOfTree==nullptr)
			return nullptr;
		TreeNode* prev = nullptr;
		Inorder(pRootOfTree,prev);
		TreeNode* firstInorder = pRootOfTree;
		//找到中序第一个
		while (firstInorder->left) 
		{
			firstInorder = firstInorder->left;
		}
		return firstInorder;
    }
};

5.105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

105思路:采用前序遍历递归构建二叉树,先构建根,然后根据中序遍历分割左右子树的区间,然后根据中序遍历的区间,递归构建左子树和右子树与根链接,这样一颗二叉树就构建好了。(思想是分治思想,类似快速排序,先排一个元素,然后分割左右区间,递归排序左右区间,整个区间就排序好了)

class Solution {
public:
    TreeNode* build(vector<int>& preorder, vector<int>& inorder,
                    int inbegin,int inend,int& prei)
    {
        //区间不存在,不用构建
        if(inbegin>inend)
            return nullptr;
        int val = preorder[prei++];
        TreeNode* root = new TreeNode(val);
        //分割左右子树
        int rooti;
        for(int i = inbegin;i<=inend;++i)
        {
            if(inorder[i]==val)
            {
                rooti = i;
                break;
            }
        }
        //分割出左右子树区间,
        //[inbegin,rooti-1] rooti [rooti+1,inend]
        //递归构建左右子树并链接
        root->left = build(preorder,inorder,inbegin,rooti-1,prei);
        root->right = build(preorder,inorder,rooti+1,inend,prei);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int prei = 0;
        TreeNode* root = build(preorder,inorder,0,inorder.size()-1,prei);
        return root;
    }
};

106思路:和前序遍历反过来差不多,后序遍历是 [左子树,右子树,根],根在最后,是倒序的方式,但是倒序是:根,右子树,左子树,划分区间与105思路一致,但是递归构建时优先构建右子树。

class Solution {
public:
    TreeNode* build(vector<int>& postorder, vector<int>& inorder,
                    int inbegin,int inend,int& prei)
    {
        //区间不存在,不用构建
        if(inbegin>inend)
            return nullptr;
        int val = postorder[prei--];
        TreeNode* root = new TreeNode(val);
        //分割左右子树
        int rooti;
        for(int i = inbegin;i<=inend;++i)
        {
            if(inorder[i]==val)
            {
                rooti = i;
                break;
            }
        }
        //分割出左右子树区间,
        //[inbegin,rooti-1] rooti [rooti+1,inend]
        //优先递归构建右子树        
        root->right = build(postorder,inorder,rooti+1,inend,prei);
        root->left = build(postorder,inorder,inbegin,rooti-1,prei);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int prei = postorder.size()-1;
        TreeNode* root = build(postorder,inorder,0,inorder.size()-1,prei);
        return root;
    }
};

6.二叉树的前中后序遍历的非递归。
144. 二叉树的前序遍历 - 力扣(LeetCode)

思路:
1.先访问左路节点(根和左子树)

2.访问左路节点的右子树

实现过程:从根开始一直访问左子树,存入根的值,直到左路节点被访问完了,然后从栈上获取数据,顶层节点的右即为第一个被访问的右节点,访问左子树的右,用cur记录。(再次循环,从根开始一直访问左子树,存入根的值,直到左路节点被访问完了,取出左路节点,访问右,赋值给cur)

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* cur = root;
        //cur不为空(当前根不是空)和栈不为空(还有右子树未访问)就继续
        while(cur || !st.empty())
        {
            //左路节点入栈,由于先访问根,存入vector中
            while(cur)
            {
                st.push(cur);
                v.push_back(cur->val);
                cur = cur->left;
            }
            //此时左路节点访问完了,访问左路节点的右子树
            //取出最顶层节点
            TreeNode* top = st.top();
            st.pop();
            //访问右子树
            cur = top->right;
        }
        return v;
    }
};

 94. 二叉树的中序遍历 - 力扣(LeetCode)

思路:

1.先访问左路节点

2.访问左路节点的右子树

实现过程:与前序遍历的思想类似,只不过区别在于根的访问时机,中序遍历是:左子树 根 右子树,第一个访问节点为最左节点,即栈上存左路节点的最顶层节点,于是只需将原访问根的顺序改为,取出节点时访问即可。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* cur = root;
        //cur不为空(当前根不是空)和栈不为空(还有右子树未访问)就继续
        while(cur || !st.empty())
        {
            //左路节点入栈,由于先访问根,存入vector中
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            //此时左路节点访问完了,访问左路节点的右子树
            //取出最顶层节点,这个节点就是左子树的根,访问
            TreeNode* top = st.top();            
            st.pop();
            v.push_back(top->val);
            //访问右子树
            cur = top->right;
        }
        return v;
    }
};

 145. 二叉树的后序遍历 - 力扣(LeetCode)

思路依旧是:

1.先访问左路节点

2.访问左路节点的右子树

问题就在于:后序遍历是:左子树,右子树,根的顺序,左子树的访问是可以确定能访问完的(先访问完左路节点),因此上一个题目中序非递归是能确定左子树访问完了,此时问题就在于如何判断右子树是否已经访问完了。

分析:

1.取出节点表示左子树是被访问完了。

2.如果取到的节点的右子树为空,说明右子树访问完了,可以访问根

3.如果取到的节点的右子树为后序遍历的上一个节点(等于prev),说明右子树访问完了,可以访问根。

4.如果取到的节点的右子树不是后序遍历的上一个节点(不等于prev),说明右子树还没有被访问完,要访问右子树。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> v;
        TreeNode* prev = nullptr;
        TreeNode* cur = root;
        //cur不为空(当前根不是空)
        //栈不为空(还有右子树和根或者根未访问)就继续
        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);
                prev = top;
                st.pop();
            }
            //右子树还未被访问,访问右子树
            else
            {
                cur = top->right;
            }
        }
        return v;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

挺6的还

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

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

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

打赏作者

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

抵扣说明:

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

余额充值