• 冰冰学习笔记:二叉树的进阶OJ题与非递归遍历


    欢迎各位大佬光临本文章!!!

    还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

    本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

    我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=bloghttps://blog.csdn.net/bingbing_bang?type=blog

    我的gitee:冰冰棒 (bingbingsupercool) - Gitee.https://gitee.com/bingbingsurercool


    系列文章推荐

    冰冰学习笔记:《二叉树的功能函数与OJ练习题》

    冰冰学习笔记:《二叉搜索树》


    目录

    系列文章推荐

    前言

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

    2.二叉树的层序遍历

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

    4. 二叉树与双向链表

    5. 二叉树的非递归遍历


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

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

    题目描述:

            给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

            二叉树最常用的解题思路就是递归求解。观察这个题目我们发现这个题目的求解其实就是一个简单的前序遍历,只不过我们在遍历将字符串进行拼接前需要使用括号将该节点的子树括起来。但是我们还发现,有些括号是需要省略的,例如根节点的左右子树都为空的情况下,子树的括号就不需要了。当左子树不为空右子树为空,右子树的括号也需要省略,当左子树为空,右子树不为空的情况时左子树的括号需要保留。

    根据上面的分析,我们可以将代码实现:

    1. class Solution {
    2. public:
    3. string tree2str(TreeNode* root) {
    4. if(root==nullptr)
    5. return string();
    6. string s;
    7. s+=to_string(root->val);
    8. if(root->left||root->right)
    9. {
    10. s+="(";
    11. s+=tree2str(root->left);
    12. s+=")";
    13. }
    14. if(root->right)
    15. {
    16. s+="(";
    17. s+=tree2str(root->right);
    18. s+=")";
    19. }
    20. return s;
    21. }
    22. };

            当根节点为空时,我们需要返回给上一层调用,此时返回空字符串即可。当根节点不为空我们需要将节点中int类型的val转化为string类型,使用库函数to_string即可。 

            处理完根节点后,我们就要判断左右子树情况,来进行括号的增加,右子树只有存在的情况下才会增加括号,子树不存在括号不需要,并且也不需要增加任何字符,可以直接返回给上一层拼接的字符串。左子树的括号在左子树不为空或者右子树不为空的情况下都需要添加。

    2.二叉树的层序遍历

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

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

            咋一看我们会以为该题目就是简单的层序遍历,其实不然,我们发现题目需要返回一个vector>类型,并且每一层的数据都保存在一个vector中。这就需要我们不仅进行层次遍历,还需要把每一层的结果分开放置。

            那我们如何将每一层的结果分开呢?我们知道使用层序遍历必然使用队列进行数据保存,队列中没弹出一个数据会将该数据的左右子树继续入队,从而达到层序遍历的结果。如果我们在弹出之前能够获得每一层的节点数量size,那我们只需要使用队列弹出size次是不是就达到了子弹出一层的结果,循环结束后,上一层弹出完毕,会将下一层的结果带入队列中,然后我们对size进行更新,开始弹出下一层。

    代码实现:

    1. class Solution {
    2. public:
    3. vectorint>> levelOrder(TreeNode* root) {
    4. queue q;
    5. vectorint>> vv;
    6. if(root)
    7. q.push(root);
    8. while(!q.empty())
    9. {
    10. int size=q.size();
    11. vector<int> tmp;
    12. while(size--)
    13. {
    14. TreeNode* cur=q.front();
    15. q.pop();
    16. tmp.push_back(cur->val);
    17. if(cur->left)
    18. q.push(cur->left);
    19. if(cur->right)
    20. q.push(cur->right);
    21. }
    22. vv.push_back(tmp);
    23. }
    24. return vv;
    25. }
    26. };

            如果题目要求从下往上遍历我们可以在用此种方式遍历完后进行逆转操作。

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

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

    题目描述:

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

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

    (1)递归查找 

            既然是二叉树的题目我们就少不了递归查找,一个节点如果是p,q的祖先,那么就说明p,q节点一定位于这个节点的左右两边。如果不在左右两边,那么该节点就一定是p,q其中的一个节点。

            因此我们使用递归写法就是判断当前节点的左子树中是否存在p和q,如果存在就说明当前节点的右子树不用查找了,我们只需要去递归左子树即可。如果当前节点左子树中存在p或者q其中一个,就需要判断右子树是否存在另一个,如果存在那么当前节点就是最近公共祖先,直接返回。如果不存在则不满足题目要求。

    1. class Solution {
    2. public:
    3. bool FindRoot(TreeNode* root,TreeNode* x)
    4. {
    5. if(root==nullptr)
    6. return false;
    7. return root==x
    8. || FindRoot(root->left,x)
    9. || FindRoot(root->right,x);
    10. }
    11. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    12. if(root==nullptr)
    13. return nullptr;
    14. if(root==p||root==q)//祖先就是root的情况
    15. return root;
    16. //正常情况
    17. bool pInLeft,pInRight,qInLeft,qInRight;
    18. pInLeft=FindRoot(root->left,p);
    19. pInRight=!pInLeft;//不在左子树一定在右子树
    20. qInLeft=FindRoot(root->left,q);
    21. qInRight=!qInLeft;
    22. //如果p在左q在右,或者p在右q在左--->root
    23. if(pInLeft&&qInRight || pInRight&&qInLeft)
    24. {
    25. return root;
    26. }
    27. else if(pInLeft&&qInLeft)//都在左边,去左树
    28. {
    29. return lowestCommonAncestor(root->left,p,q);
    30. }
    31. else if(pInRight&&qInRight)//都在右边,去右树
    32. {
    33. return lowestCommonAncestor(root->right,p,q);
    34. }
    35. else
    36. return nullptr;//不可能的情况

            查找之前我们先进行判断当前节点是否为空,如果为空则说明祖先不存在。由于题目限制了p,q一定存在于树中,所以我们可以先进行判断当前节点是否等于p,q中的一个,如果满足则表明当前节点就是公共祖先。 

            均不满足上述条件后,我们就先判断p,q是否满足一个在左子树一个在右子树,如果满足条件那么当前节点就是最近公共祖先。那么如何判断呢?这里我们定义了4个bool类型的变量。然后定义一个查找函数,查找函数很简单,只需要递归去一一比对节点与基准值是否相等即可。

            我们先去左子树中查找p是否存在,如果存在,pInLeft即为真,pInRight即为真。然后去左子树查找q是否存在,存在qInLeft即为真,不存在qInRight即为真。然后进行判断,如果一左一右返回当前节点,如果都在左,则递归去左子树中查找,如果都在右则递归去右子树中查找。

    (2)保存路径,转化为链表相交

            如果我们能够找到两个节点的访问路径,并且将路径保存在一个容器中,然后从查找结点开始,一一比对两个路径中节点的大小,如果相同就自然而然的找到了最近的公共祖先。

    1. class Solution {
    2. public:
    3. bool FindPath(TreeNode* root, TreeNode* x, stack& path)
    4. {
    5. if(root==nullptr)
    6. {
    7. return false;
    8. }
    9. path.push(root);
    10. if(root==x)
    11. return true;
    12. bool left=FindPath(root->left,x,path);
    13. if(left)
    14. return true;
    15. bool right=FindPath(root->right,x,path);
    16. if(right)
    17. return true;
    18. path.pop();
    19. return false;
    20. }
    21. TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    22. stack pPath, qPath;
    23. FindPath(root, p, pPath);
    24. FindPath(root, q, qPath);
    25. while(pPath.size()!=qPath.size())
    26. {
    27. if(pPath.size()>qPath.size())
    28. pPath.pop();
    29. else
    30. qPath.pop();
    31. }
    32. while(qPath.top()!=pPath.top())
    33. {
    34. qPath.pop();
    35. pPath.pop();
    36. }
    37. return pPath.top();
    38. //
    39. }
    40. };

    4. 二叉树与双向链表

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

    题目描述:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表

    注意:

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

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

            该题目的难点在于不能创建任何新的节点,只能改变树结构中的左右指针的指向。这里我们采用的是记录上一次访问的节点,当中序遍历的左子树访问完毕后,访问根节点时,将根节点的左指针指向上一次访问的内容。但是这种方式我们没办法改变当前节点的右指针,但是在递归调用到下一层的时候pre指向的就是前一次访问的根节点,此时我们改变pre的右指针同样可以改变前一层节点的右指针。 

    例如下面的例子:

    1. class Solution {
    2. public:
    3. //传入的pre是引用,因为pre的改变需要返回给上层
    4. void InorderConvert(TreeNode* cur,TreeNode* &pre)
    5. {
    6. if(cur==nullptr)
    7. return ;
    8. InorderConvert(cur->left, pre);
    9. //中序
    10. cur->left=pre;
    11. if(pre)
    12. pre->right=cur;
    13. pre=cur;
    14. InorderConvert(cur->right, pre);
    15. }
    16. TreeNode* Convert(TreeNode* pRootOfTree) {
    17. TreeNode* pre=nullptr;
    18. InorderConvert(pRootOfTree, pre);
    19. TreeNode* head=pRootOfTree;
    20. while(head&&head->left)
    21. {
    22. head=head->left;
    23. }
    24. return head;
    25. }
    26. };

    5. 二叉树的非递归遍历

    (1)前序非递归

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

            前序遍历的规则是一直访问根然后左子树,当左子树为空说明根节点的左边访问结束,然后访问根节点的右边,右边同样满足根左子树右子树的规则。 

            因此我们采用循环加栈容器的方式模拟递归,遇到根将根的内容输出,并且将根节点入栈,然后继续访问左子树。当左子树为空时,栈顶元素就是上一次访问的根,然后将根的右子树作为下一个开始的根节点继续重复循环遍历,并将当前栈顶元素弹出。

            循环什么时候停止呢?当栈为空,但是栈顶元素的右子树不为空时不能停止,这仅代表前面访问的节点的左右子树都为空,但是当前访问的节点右子树并不为空,还需要继续入栈访问。当栈不为空就更不能停止,这说明栈中的元素还没有完全访问结束,因为一旦访问过就会弹出栈,所以循环停止的条件应该是二者都为空。

    1. class Solution {
    2. public:
    3. vector<int> preorderTraversal(TreeNode* root) {
    4. stack st;
    5. vector<int> v;
    6. TreeNode*cur=root;
    7. while(cur||!st.empty())
    8. {
    9. while(cur)
    10. {
    11. v.push_back(cur->val);
    12. st.push(cur);
    13. cur=cur->left;
    14. }
    15. TreeNode* top=st.top();
    16. st.pop();
    17. cur=top->right;
    18. }
    19. return v;
    20. }
    21. };

    (2)中序非递归

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

            中序和前序的思路一样, 只是遇到跟节点我们不能先访问,应该直接入栈,当左边全部入栈结束后再拿出栈顶的数据进行访问,然后弹栈,并且将原栈顶元素的右子树作为循环开始的下一个节点。

    1. class Solution {
    2. public:
    3. vector<int> inorderTraversal(TreeNode* root) {
    4. stack st;
    5. vector<int> v;
    6. TreeNode*cur=root;
    7. while(cur||!st.empty())
    8. {
    9. while(cur)
    10. {
    11. st.push(cur);
    12. cur=cur->left;
    13. }
    14. TreeNode* top=st.top();
    15. v.push_back(top->val);
    16. st.pop();
    17. cur=top->right;
    18. }
    19. return v;
    20. }
    21. };

    (3)后续非递归

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

            后续遍历比较麻烦因为我们要确定一个节点是否需要访问因该是左右节点均访问过了才行,当左子树入栈完毕后,如果当前栈顶的节点的右子树为空可以直接访问栈顶元素,并进行弹栈处理。但是如果不为空,这说明右子树也需要入栈,并且栈顶元素不能弹出,只有当右子树全部访问结束才能进行出栈。怎么判断呢?我们采用pre记录上一次访问节点,当pre与当前栈顶元素的右子树相同时,说明右边已经访问结束,可以弹栈,然后我们更新pre为此次访问的节点。

    1. class Solution {
    2. public:
    3. vector<int> postorderTraversal(TreeNode* root) {
    4. vector<int> v;
    5. stackst;
    6. TreeNode* cur=root;
    7. TreeNode* pre=nullptr;
    8. while(cur||!st.empty())
    9. {
    10. while(cur)
    11. {
    12. st.push(cur);
    13. pre=cur;
    14. cur=cur->left;
    15. }
    16. TreeNode* tmp=st.top();
    17. if(tmp->right==nullptr||tmp->right==pre)
    18. {
    19. v.push_back(tmp->val);
    20. pre=tmp;
    21. st.pop();
    22. cur=nullptr;
    23. }
    24. else
    25. cur=tmp->right;
    26. }
    27. return v;
    28. }
    29. };

  • 相关阅读:
    Web3中文|元宇宙在商业中的最佳应用
    java毕业设计颜如玉图书销售网站的设计与实现Mybatis+系统+数据库+调试部署
    Elasticsearch学习--查询(prefix、wildcard、regexp、fuzzy)
    vscode工程屏蔽不需要的文件(保持搜索便利)
    融云「百幄」之数字人,升级交互体验的「新同事」
    equals提高执行速度/性能优化
    JVM启动参数大全及默认值
    一. HTML基础开发标签
    展望未来:在【PyCharm】中结合【机器学习】实现高效的图形化处理
    半路入行网络安全,怎么学才不会走弯路
  • 原文地址:https://blog.csdn.net/bingbing_bang/article/details/127664831