【OJ】二叉树的经典OJ题

目录

[1. 二叉树创建字符串](#1. 二叉树创建字符串)

[2. 二叉树的层序遍历](#2. 二叉树的层序遍历)

[3. 二叉树的最近公共祖先](#3. 二叉树的最近公共祖先)

[优化到 O(N)](#优化到 O(N))

[4. 二叉搜索树转化为有序双向链表](#4. 二叉搜索树转化为有序双向链表)

[5. 按前序、中序遍历构造二叉树](#5. 按前序、中序遍历构造二叉树)

[6. 非递归 · 前序](#6. 非递归 · 前序)

[7. 非递归 · 中序](#7. 非递归 · 中序)

[8. 非递归 · 后序](#8. 非递归 · 后序)


1. 二叉树创建字符串

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

cpp 复制代码
class Solution {
public:
    string tree2str(TreeNode* root) {
        
    }
};

用前序遍例

输出:"1(2(4))(3)" 简化前:"1(2(4)()())(3()())"

输出:"1(2()(4))(3)"

**思路:**前序遍例(根、左、右)
1. 左空,右不空:左括号保留
2. 左不空,右空:右括号删
3. 左空,右空:左右括号都删

cpp 复制代码
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;
    }
};

第10行的写法非常经典:判断是否往左递归,录值或保留左括号
左不空:往左递归,录值
左空,右不空:往左递归,以保留左括号
左空,右空:不需要保留左括号

再到第17行:

右不为空:录值

右为空:不保留右括号

2. 二叉树的层序遍历

102. 二叉树的层序遍历 - 力扣(LeetCode)

cpp 复制代码
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        
    }
};

广度优先遍历想到 queue

以前的层序遍历光让把数据打印出来,用一个队列:出一个节点并打印、入它的俩娃,直至队列为空

这道题不一样,要求一层一层遍历,并把每一层放到二维数组的每一行

**思路1:**双队列,一个存节点,一个记录行数

**思路2:**用 levelSize 控制层数,一层一层出

以前:

cpp 复制代码
while (!empty(q))
{
    TreeNode* front = q.front();
    q.pop();

    if (front->left)
        q.push(front->left);
    if (front->right)
        q.push(front->right);
}

队列中最多有两层的数据,以前每出一个,不知道出的在哪一层

现在:

cpp 复制代码
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;
            for (int i = 0; i < levelSize; i++)
            {
                TreeNode* front = q.front();
                v.push_back(front->val);
                q.pop();

                if (front->left)
                    q.push(front->left);
                if (front->right)
                    q.push(front->right);
            }

            vv.push_back(v);
            levelSize = q.size();
        }

        return vv;
    }
};

原本 levelSize = 1,把 3 出了,levelSize = 2,就知道再往下的俩数据是一层


**二叉树的层序遍历II:**自底向上层层遍历,放到二维数组

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

这样即可:

cpp 复制代码
reverse(vv.begin(), vv.end());
return vv;

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

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

cpp 复制代码
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        
    }
};

树中所有节点的值都不相同,一个节点的祖先可以是自己

找 p q 俩节点的最近公共祖先 x p != q 且 都在二叉树中

思路:
A 链表相交类似问题
a 三叉链 (存父节点)O(N)
b 用栈找路径 O(N)
B 普通解法:
a 普通二叉树:通过查找函数,确定节点在左(右)树 O(N^2)
1. 若当前根就是 p 或 q 节点,则当前根为最近公共祖先
2. 若 p q 节点,一个在左树、一个在右树,则当前根为最近公共祖先
3. 若 p q 节点,都在左(右)树,则递归去左(右)树找,直至满足 1 2 的条件
b 二叉搜索树 通过比较大小 ,确定节点在左(右)树 O(N)

cpp 复制代码
class Solution {
public:
    bool Find(TreeNode* tree, TreeNode* x)
    {
        if (tree == nullptr)
            return false;

        return tree == x
            || Find(tree->left, x)
            || Find(tree->right, x);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == nullptr)
            return nullptr;

        if (root == p || root == q)
            return root;

        bool pleft, pright, qleft, qright;
        pleft = Find(root->left, p);
        pright = !pleft;

        qleft = Find(root->left, q);
        qright = !qleft;

        if (pleft && qleft)
            return lowestCommonAncestor(root->left, p, q);
        else if (pright && qright)
            return lowestCommonAncestor(root->right, p, q);
        else
            return root;
    }
};

20 - 25行很巧妙:

如果 p 在左,Find 能找到,pleft 是真,pright 自动为假

如果 p 在右,Find 找不到,pleft 是假,pright 自动为真

这样写,时间复杂度是 O(N^2)

从 root 开始,每层往下走等差数列次 ↓

优化到 O(N)

查找过程 + 栈,就能在普通二叉树里搞定节点的路径,存到栈里

遇到不相等的节点也要入栈,因为不确定这个节点在不在路径上

要用节点比较,不能用值比较,可能有相同的值

递归全程是在一个栈上入、删数据的,所以 path 引用传参

节点先入栈,在该节点构成的子树中找 x**(找的顺序:根、左子树、右子树)**
找到了(10-17 行):该节点在 x 的路径上
没找到(到第 19 行):该节点不在 x 的路径上,出栈

cpp 复制代码
class Solution {
public:
    bool FindPath(TreeNode* root, TreeNode* x, stack<TreeNode*>& path)
    {
        if (root == nullptr)
            return false;

        path.push(root);

        if (root == x)
            return true;

        if (FindPath(root->left, x, path))
            return true;

        if (FindPath(root->right, x, path))
            return true;

        path.pop();
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pPath, qPath;
        FindPath(root, p, pPath);
        FindPath(root, q, qPath);
        
        while (pPath.size() > qPath.size())
        {
            pPath.pop();
        }

        while (pPath.size() < qPath.size())
        {
            qPath.pop();
        }

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

        return pPath.top();
    }
};

4. 二叉搜索树转化为有序双向链表

二叉搜索树与双向链表_牛客题霸_牛客网

cpp 复制代码
#include <cstddef>
class Solution {
public:
    TreeNode* Convert(TreeNode* pRootOfTree) {
        
    }
};

说了二叉搜索树、有序:中序遍历

先写个中序遍历的框架,再不断往里面加东西

要找到当前根的左娃(前一个节点)、右娃(后一个节点),连接起来,所以要传 prev

上面栈帧的 prev 要用下面栈帧的 prev 来确保连接关系,所以prev 要传引用 确保整个代码中只有一个 prev

下面的栈帧里改变了 prev,并且上面的栈帧要利用这次改变,prev 就要传引用或二级指针

站在当前节点,找左娃(前一个节点)很容易实现;找右娃(后一个节点)根本实现不了,怎么能预测明天的事呢

换个思路:当前节点能和左娃(前一个节点)链接,左娃(前一个节点)就能和当前节点链接

cpp 复制代码
#include <cstddef>
class Solution {
public:
	void InOrder(TreeNode* cur, TreeNode*& prev)
	{
		if (cur == nullptr)
			return;

		InOrder(cur->left, prev);

        // 当前节点的left指向前一个
		cur->left = prev;
        // 前一个节点的right指向当前节点(链接上了)
		if (prev)
			prev->right = cur;
		prev = cur;

		InOrder(cur->right, prev);
	}

    TreeNode* Convert(TreeNode* pRootOfTree) {
		TreeNode* prev = nullptr;
        InOrder(pRootOfTree, prev);

		TreeNode* head = pRootOfTree;
		while (head && head->left)
		{
			head = head->left;
		}

		return head;
    }
};

5. 按前序、中序遍历构造二叉树

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

cpp 复制代码
class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        
    }
};

前序: 根 左子树 右子树 确定根
中序: 左子树 根 右子树 确定左右子树,分割左右区间

preorder = [3,9,20,15,7]

inorder = [9,3,15,20,7]

cpp 复制代码
class Solution {
public:
    TreeNode* _build(vector<int>& preorder, vector<int>& inorder, 
                    int& prei, int inbegin, int inend)
    {
        if (inbegin > inend)
            return nullptr;

        TreeNode* root = new TreeNode(preorder[prei]);
        // 找中序的分割区间
        int rooti = inbegin;
        while (rooti <= inend)
        {
            if (preorder[prei] == inorder[rooti])
                break;
            
            rooti++;
        }

        // prei创节点、找分割区间的任务完成了
        // [inbegin, rooti-1] rooti [rooti+1, inend]
        // 开始递归
        ++prei;
        root->left = _build(preorder, inorder, prei, inbegin, rooti - 1);
        root->right = _build(preorder, inorder, prei, rooti + 1, inend);
        return root;
    }

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

按中序、后序遍历构造二叉树

https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/

后序最后一个确定根,倒着来,先构建右子树

如果右树不为空,后序的下一个值就是右子树的根

inorder = [9,3,15,20,7]

postorder = [9,15,7,20,3]

cpp 复制代码
class Solution {
public:
    TreeNode* _build(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 = inend;
        while (inbegin <= rooti)
        {
            if (postorder[posti] == inorder[rooti])
                break;

            --rooti;
        }

        // posti使命完成
        // [inbegin, rooti-1] rooti [rooti+1, inend]
        --posti;
        root->right = _build(inorder, postorder, posti, rooti + 1, inend);
        root->left = _build(inorder, postorder, posti, inbegin, rooti - 1);
        return root;
    }

    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int i = postorder.size() - 1;
        return _build(inorder, postorder, i, 0, i);
    }
};

按前序、后序不能重建二叉树,分割不出左子树右子树

6. 非递归 · 前序

144. 二叉树的前序遍历 - 力扣(LeetCode)

cpp 复制代码
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        
    }
};

前序遍历刚开始肯定是把左边一路节点遍历,所以可以这么看一颗二叉树:

1. cur 指向一颗节点,意味着要访问它的右子树
2. 栈里的节点,意味着要访问它的右子树

cpp 复制代码
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        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;
    }
};

7. 非递归 · 中序

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

前序遍历,左路节点入栈时访问;中序遍历只是节点访问时机不同

思路:
1. 左路节点入栈
2. 访问左路节点、左路节点右子树

左路节点入完时, ++此时栈顶 top 指向 1,cur 指向 1 的左,cur == nullptr,不用访问(1 的左已经访问,可以访问 1 这个节点了)++,访问它;再访问 1 的右子树

本质:从栈里取到栈顶节点,则这个节点的左子树已经访问完了,可以访问节点、右子树了

cpp 复制代码
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while (cur || !st.empty())
        {
            // 左路节点入栈,后续依次访问左路节点、右子树
            while (cur)
            {
                st.push(cur);
                cur = cur->left;
            }

            // 访问左路节点
            TreeNode* top = st.top();
            st.pop();
            v.push_back(top->val);

            // 访问右子树
            cur = top->right;
        }
        return v;
    }
};

8. 非递归 · 后序

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

右子树访问完,才能访问根

右不为空,怎么识别能不能访问 3 这个节点?
怎么知道是第一次访问 3 的右,还是 3 的右已经访问过了,避免死循环

如果 3 的右子树没有访问过,上一个访问的节点是****左子树的根 1
如果 3 的右子树访问过,上一个访问的节点是****右子树的根 6

一个节点右子树为空,或上一个访问的节点是右子树的根,则说明右子树访问过了,可以访问当前根节点

本质:从栈里取到栈顶节点,则这个节点的左子树已经访问完了

cpp 复制代码
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;
        while (cur || !st.empty())
        {
            // 左路节点入栈
            while (cur)
            {
                st.push(cur);
                cur = cur->left;
            }

            TreeNode* top = st.top();
            // 能不能访问这个节点?有2个标志
            if (top->right == nullptr || top->right == prev)
            {
                prev = top;
                v.push_back(top->val);
                st.pop();
            }
            else // 不能访问这个节点,要先访问它的右子树
            {
                cur = top->right;
            }
        }
        return v;
    }
};

本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注

小编会以自己学习过程中遇到的问题为素材,持续为您推送文章

相关推荐
mit6.8246 小时前
list
c++
满天星83035776 小时前
【C++/STL】哈希表的模拟实现+封装
c++·哈希算法·散列表
自在极意功。6 小时前
贪心算法深度解析:从理论到实战的完整指南
java·算法·ios·贪心算法
wydaicls7 小时前
C语言对单链表的操作
c语言·数据结构·算法
傻童:CPU7 小时前
C语言需要掌握的基础知识点之排序
c语言·算法·排序算法
骁的小小站9 小时前
Verilator 和 GTKwave联合仿真
开发语言·c++·经验分享·笔记·学习·fpga开发
大数据张老师11 小时前
数据结构——邻接矩阵
数据结构·算法
低音钢琴12 小时前
【人工智能系列:机器学习学习和进阶01】机器学习初学者指南:理解核心算法与应用
人工智能·算法·机器学习
旭意12 小时前
C++蓝桥杯之结构体10.15
开发语言·c++