【刷题记录】关于二叉树的OJ题

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

题目链接:606. 根据二叉树创建字符串 - 力扣(LeetCode)

题干:

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

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

示例 1:

img
输入:root = [1,2,3,4]
输出:"1(2(4))(3)"
解释:初步转化后得到 "1(2(4)())(3()())" ,但省略所有不必要的空括号对后,字符串应该是"1(2(4))(3)" 。

示例 2:

img
输入:root = [1,2,3,null,4]
输出:"1(2()(4))(3)"
解释:和第一个示例类似,但是无法省略第一个空括号对,否则会破坏输入与输出一一映射的关系。

题目分析:

这是一道力扣简单题,显而易见使用先序遍历即可,但是这里需要注意到的点就是,用括号把子树括起来这一点,需要我们着重考虑一下。因为其中有的情况要省略空括号,有的情况不能省略。分析题目可知,当该节点左子树为nullptr,并且右子树有值时不能省略

代码实现:

class Solution {
public:
    string tree2str(TreeNode* root) {
		if(root == nullptr)//当树为空树的时候,遍历之后的结果是空字符串
            return string();
        string str;//创建字符串保存结果
        str += to_string(root->val);//线序遍历,首先把根节点的值放进str,这里需要把val的类型转换成string类型才能追加
        //先序遍历,首先走左子树,然后走右子树
        if(root->left)//左子树不为空时,先序遍历左子树,然后把左子树的字符串加上括号追加到str上
        {
            str += '(';
            str += tree2str(root->left);
            str += ')';
        }
        else if(root->right)//左子树为空,且右子树不为空时
        {
            //此时左子树的空括号不能省略
            str += "()";
        }
        if(root->right)//右子树不为空时,先序遍历右子树
        {
            str += '(';
            str += tree2str(root->right);
            str += ')';            
        }
        return str;
    }
};
image-20230506145824561

2.二叉树的层序遍历

题目链接:102. 二叉树的层序遍历 - 力扣(LeetCode)

题干:

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例1

img
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例二

输入:root = [1]
输出:[[1]]

示例三

输入:root = []
输出:[]

题目分析:

二叉树的层序遍历,我们需要借助一下队列的数据结构,将每一层的节点放进队列中,然后需要在访问当前层节点的时候拿到当前节点的子节点,否则后面就找不到子节点了。所以思路就是首先非空节根节点入队列,然后每次出队列时,把该节点的子节点依次入队列,这要就能达到层序的目的,看起来很完美,但是注意一下题目示例,需要我们按照每层节点存放在一个单独的vector中,这就产生了一个问题:怎么判断当前节点是第几层的?仔细分析可知,队列中存放的节点只有两种可能:1.只有当前节点所在层数的节点;2.有当前节点所在层节点和下一层的部分节点,那我们采用一个变量存放当前层的节点个数,然后每当出一个节点就自减,直到为0时,队列中存放的节点个数就是下一层的节点个数,通过这个变量来控制存入第几个vector即可

代码实现:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv;
        queue<TreeNode*> q;
        int levelSize = 0;//存放当前层的节点个数
        if(root)
        {
            q.push(root);
            levelSize = 1;
        }
        while(!q.empty())
        {
            vector<int> v;//存放每一层的值,一层一层push_back到vv中
            while(levelSize--)
            {
                TreeNode* top = q.front();
                q.pop();
                v.push_back(top->val);
                if(top->left)
                    q.push(top->left);
                if(top->right)
                    q.push(top->right);
            }
            levelSize = q.size();
            vv.push_back(v);
        }
        return vv;
    }
};
image-20230506152450991

拓展与补充:107. 二叉树的层序遍历 II - 力扣(LeetCode)

这是上一道题的扩展,让我们自低向上层序遍历,这里有一个最简单的办法就是按照上述的代码走一遍,然后在return之前reverse一下即可。

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

题目链接:236. 二叉树的最近公共祖先 - 力扣(LeetCode)

题干:

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例1

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CvrEncu5-1683507657217)(null)]

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例3

输入:root = [1,2], p = 1, q = 2
输出:1

题目分析:

通过分析题目可知,需要找的p、q节点在树种的存在情况只有:1.pq都在当前节点所在树的左子树中,在左子树中找pq的公共祖先即可;2.pq都在右子树中,在右子树中找pq的公共祖先即可;3pq分别在左右子树中,当前节点即是所求公共祖先节点;4.pq中有一个是当前节点,返回当前节点即可

代码实现:

class Solution {
public:
    bool isInTree(TreeNode* root, TreeNode* node)//判断node在不在树root中
    {
        if(root == nullptr)
            return false;
        //这里需要比较地址,而不是比较值
        return root == node || isInTree(root->left, node) || isInTree(root->right, node);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //由于pq在树中,所以这里不需要考虑root为空的情况
        if(p == root || q == root)//pq有一个是当前节点
        {
            return root;
        }
        //判断pq的位置
        bool pInLeft = isInTree(root->left, p);
        bool pInRight = ! pInLeft;
        bool qInLeft = isInTree(root->left, q);
        bool qInRight = ! qInLeft;
        if((pInLeft && qInRight) || (pInRight && qInLeft))//pq在两侧
        {
            return root;
        }
        if(pInLeft && qInLeft)//都在左子树
        {
            return lowestCommonAncestor(root->left, p, q);
        }
        else//都在右子树
        {
            return lowestCommonAncestor(root->right, p, q);
        }
    }
};

image-20230506154625766

看一下结果,执行用时572ms,效率有点低啊,能不能优化一下嘞?分析一下我们的代码,我们的代码花费了太多时间在判断pq的位置上了,这里是一个普通二叉树,没办法很快的找到pq的位置,所以只能想想怎么优化掉这个过程了。

我们知道,pq节点肯定是在二叉树中的,那么他们在二叉树中就存在从根到节点的唯一路径,而且在这个路径中,绝对存在相同的部分,如果按照自低向上的看法,那么最终第一个相交的地方就是我们要找的公共祖先。那么做法思路就出来了:首先找到自低向上的节点路径,然后就可以类比链表相交的做法找到第一个相交的节点

代码实现:

class Solution {
public:
    bool GetPath(TreeNode* root, TreeNode* node, stack<TreeNode*>& path)
    {
        if(root == nullptr)
            return false;
        path.push(root);//首先当前节点入栈
        if(root == node)//当前节点就是所求节点时
            return true;
        //接下来需要在左右子树分别找,如果在左树中找到,就不需要去右树,否则还要去右树中找,所以这里需要判断有没有找到,所以这里把函数的返回值设置成bool
        if(GetPath(root->left, node, path))
        {
            return true;
        }
        //左树中没有,去右树种找
        if(GetPath(root->right, node, path))
        {
            return true;
        }
        //都没有时,代表当前根节点下边没有,所以pop掉
        path.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
		//在找pq的路径的时候,只能自顶向下找,所以找到之后需要逆置,而且后续操作中需要头删,所以这里干脆使用stack,就不需要中间的这些操作
        stack<TreeNode*> pPath;
        stack<TreeNode*> qPath;
        //这里不需要判断是否找到,因为题中已经确定pq在树种
        GetPath(root, p, pPath);
        GetPath(root, q, qPath);
        //让两个路径种长的先走,直到一样长的时候
        while(pPath.size() != qPath.size())
        {
            if(pPath.size() > qPath.size())
                pPath.pop();
            else
                qPath.pop();
        }
        //同时走,当节点相同时pop,返回相同节点即可
        while(pPath.top() != qPath.top())
        {
            pPath.pop();
            qPath.pop();
        }
        return pPath.top();
    }
};

image-20230506160945240

可以看到,效率提高显著。

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

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

题干:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示

img

数据范围:输入二叉树的节点数 0≤n≤1000,二叉树中每个节点的值 0≤val≤1000
要求:空间复杂度O(1)(即在原树上操作),时间复杂度 O*(*n)

注意:

1.要求不能创建任何新的结点,只能调整树中结点指针的指向。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继
2.返回链表中的第一个节点的指针
3.函数返回的TreeNode,有左右指针,其实可以看成一个双向链表的数据结构

4.你不用输出双向链表,程序会根据你的返回值自动打印输出

输入描述:

二叉树的根节点

返回值描述:

双向链表的其中一个头节点。

示例1

输入:{10,6,14,4,8,12,16}
返回值:From left to right are:4,6,8,10,12,14,16;From right to left are:16,14,12,10,8,6,4;
说明:输入题面图中二叉树,输出的时候将双向链表的头节点返回即可。     

示例2

输入:{5,4,#,3,#,2,#,1}
返回值:From left to right are:1,2,3,4,5;From right to left are:5,4,3,2,1;
说明:               5
                  /
                4
              /
            3
          /
        2
      /
    1
树的形状如上图       

题目分析:

由于这是一个二叉搜索树,所以走中序遍历久是有序的结果,所以这里可以按照中序遍历的方式连接节点

代码实现:

class Solution {
public:
	void InOrderConvert(TreeNode* cur, TreeNode*& prev)//注意这里的参数类型设置,prev的参数类型要是引用,需要把prev的值带到其他栈帧中
	{
		if(cur == nullptr)//空树直接返回
			return;
		InOrderConvert(cur->left, prev);
		//中序操作
		cur->left = prev;//连接左结点
		if(prev)//连接右节点
			prev->right = cur;
		prev = cur;//交替向后走遍历所有节点
		InOrderConvert(cur->right, prev)
	}
    TreeNode* Convert(TreeNode* pRootOfTree) {
		TreeNode* prev = nullptr;
        InOrderConvert(pRootOfTree, prev);//重新连接节点

		TreeNode* head = pRootOfTree;
		while(head && head->left)//中序遍历的流程拿到最左节点
		{
			head = head->left;
		}
		return head;
    }
};
截屏2023-05-07 19.16.30

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

题目链接: 105. 从前序与中序遍历序列构造二叉树

题干:

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

示例1:

img
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]

示例2

输入: preorder = [-1], inorder = [-1]
输出: [-1]

题目分析:

已知的条件是二叉树的前序和中续遍历的序列,那么通过前序序列可知根节点,在中序序列中,根节点把左右子树分开,就可以分别再构造左右子树,然后最后构造出整个二叉树

代码实现:

class Solution {
public:
    TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int& prei, int begin, int end)
    {
        if(begin > end)//当区间不存在时,直接return
        {
            return nullptr;
        }
        int i = 0;
        for(; i < inorder.size(); ++i)//找到当前的[begin,end]的中序序列里面根的位置
        {
            if(inorder[i] == preorder[prei])
                break;
        }
        TreeNode* root = new TreeNode(preorder[prei++]);//构造一个当前序列的根节点
				//分别构建左右子树
        // [begin,i-1] i [i+1, end]
        root->left = _buildTree(preorder, inorder, prei, begin, i-1);
        root->right = _buildTree(preorder, inorder, prei, i+1, end);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int i = 0;//创建先序序列中根的下标
        return _buildTree(preorder, inorder, i, 0, preorder.size()-1);
    }
};
截屏2023-05-07 21.13.31

拓展与补充106. 从中序与后序遍历序列构造二叉树

与上题类似,这里通过后续遍历序列来确定根节点,然后中序遍历序列中的根节点的位置来分别构造左右子树

6.二叉树的遍历

**题目链接 144. 二叉树的前序遍历 **

题干

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

示例1

img
输入:root = [1,null,2,3]
输出:[1,2,3]

示例2

输入:root = []
输出:[]

题目分析:

这个题使用递归的方式实现是非常轻松的,这里就不过多赘述,我们主要来理解迭代的思想使用迭代的方式解决此问题

改写迭代的重要性:递归的深度越深,消耗的资源越多,这个资源是在栈上的,而对于目前的操作系统来说,栈上的空间是很小的,大概只有8M左右,所以很容易出现栈溢出的问题,即使代码是没有问题的,也不一定能运行,相对于栈来说,堆上的空间就大很多了,所以一般将递归改写成迭代。

这里如果想要用迭代的方法遍历,就需要借助数据结构的栈来实现。我们来分析一下前序遍历的过程。对于任意一颗二叉树来说,可以分为左路节点和左路节点的右子树,首先就是根节点,然后是左子树的根,然后左子树的左子树,一直走下去,直到左子树为空然后走这个子树的右子树,右子树走完之后再回到上一个根的位置,再走右子树,直到回到整棵树的根节点为止。所以这里遵循后进先出的规则,因此我们借助栈来实现迭代的改写。

代码实现

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> preorder;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            //开始遍历一棵树,根节点为cur
            while(cur)//遍历左路节点并入栈
            {
                st.push(cur);
                preorder.push_back(cur->val);
                cur = cur->left;
            }
            //cur现在指向二叉树的最左节点,下一步出栈,并处理右子树
            TreeNode* top = st.top();
            st.pop();
            cur = top->right;//处理右子树
        }
        return preorder;
    }
};
截屏2023-05-08 08.02.22 **拓展1**:

94. 二叉树的中序遍历 递归解法

中序遍历与前序遍历类似,只是中序序列push_back的时候,不能先push根节点,而是在处理完左子树之后在push,所以代码示例如下

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> inorder;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            inorder.push_back(top->val);//这里是在处理右子树之前push_back
            cur = top->right;
        }
        return inorder;
    }
};
截屏2023-05-08 08.26.29

拓展2

145. 二叉树的后序遍历 递归解法

同样的大思路,但是细节的地方还是要做一些修改。后续遍历要求访问完左右子树之后再访问根节点。分析过程可知,按照此思路走对于任意的右子树,将会访问两遍,分别是在此树访问右子树前和访问右子树后,这里要判断一下是否已经访问过了,如果访问过了就直接访问根,否则就访问右子树

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> postorder;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;//这里使用一个prev保存上一个访问的节点
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
//这里对右子树是否访问过的判断方法是判断上一个访问的节点是否是右子树,也可以使用其他方法,例如stack<pair<TreeNode*, bool>>
            if(top->right == nullptr || top->right == prev)//当右子树为空或者右子树已经访问过
            {
                st.pop();
                postorder.push_back(top->val);//访问根
                prev = top;//记录上一个访问节点
            }
            else//右子树没有被访问过,迭代访问右子树
                cur = top->right;
        }
        return postorder;
    }
};
截屏2023-05-08 08.49.48

这道题还有一个比较清奇的解法,就是按照类似前序遍历的方式,但是需要先遍历右子树,即根节点->右子树->左子树的顺序遍历一遍,也就是改写一下上述前序的方法,然后最终的结果reverse一下就是后序遍历序列,这里提供一下思路,有兴趣的可以试一下

  • 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、付费专栏及课程。

余额充值