【OJ】二叉树的经典OJ题
目录
1. 二叉树创建字符串
2. 二叉树的层序遍历
3. 二叉树的最近公共祖先
优化到 O(N)
4. 二叉搜索树转化为有序双向链表
5. 按前序、中序遍历构造二叉树
6. 非递归 · 前序
7. 非递归 · 中序
8. 非递归 · 后序
1. 二叉树创建字符串
606. 根据二叉树创建字符串 - 力扣(LeetCode)
class Solution {
public:string tree2str(TreeNode* root) {}
};
用前序遍例
输出:"1(2(4))(3)" 简化前:"1(2(4)()())(3()())"
输出:"1(2()(4))(3)"
思路:前序遍例(根、左、右)
1. 左空,右不空:左括号保留
2. 左不空,右空:右括号删
3. 左空,右空:左右括号都删
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)
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {}
};
广度优先遍历想到 queue
以前的层序遍历光让把数据打印出来,用一个队列:出一个节点并打印、入它的俩娃,直至队列为空
这道题不一样,要求一层一层遍历,并把每一层放到二维数组的每一行

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

思路2:用 levelSize 控制层数,一层一层出
以前:
while (!empty(q))
{TreeNode* front = q.front();q.pop();if (front->left)q.push(front->left);if (front->right)q.push(front->right);
}
队列中最多有两层的数据,以前每出一个,不知道出的在哪一层
现在:
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)
这样即可:
reverse(vv.begin(), vv.end());
return vv;
3. 二叉树的最近公共祖先
236. 二叉树的最近公共祖先 - 力扣(LeetCode)
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)

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);elsereturn root;}
};
20 - 25行很巧妙:
如果 p 在左,Find 能找到,pleft 是真,pright 自动为假
如果 p 在右,Find 找不到,pleft 是假,pright 自动为真
这样写,时间复杂度是 O(N^2)
从 root 开始,每层往下走等差数列次 ↓

优化到 O(N)
查找过程 + 栈,就能在普通二叉树里搞定节点的路径,存到栈里
遇到不相等的节点也要入栈,因为不确定这个节点在不在路径上
要用节点比较,不能用值比较,可能有相同的值

递归全程是在一个栈上入、删数据的,所以 path 引用传参
节点先入栈,在该节点构成的子树中找 x(找的顺序:根、左子树、右子树)
找到了(10-17 行):该节点在 x 的路径上
没找到(到第 19 行):该节点不在 x 的路径上,出栈
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. 二叉搜索树转化为有序双向链表
二叉搜索树与双向链表_牛客题霸_牛客网

#include <cstddef>
class Solution {
public:TreeNode* Convert(TreeNode* pRootOfTree) {}
};
说了二叉搜索树、有序:中序遍历
先写个中序遍历的框架,再不断往里面加东西
要找到当前根的左娃(前一个节点)、右娃(后一个节点),连接起来,所以要传 prev
上面栈帧的 prev 要用下面栈帧的 prev,来确保连接关系,所以 prev 要传引用,确保整个代码中只有一个 prev

下面的栈帧里改变了 prev,并且上面的栈帧要利用这次改变,prev 就要传引用或二级指针
站在当前节点,找左娃(前一个节点)很容易实现;找右娃(后一个节点)根本实现不了,怎么能预测明天的事呢
换个思路:当前节点能和左娃(前一个节点)链接,左娃(前一个节点)就能和当前节点链接
#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)
class Solution {
public:TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {}
};
前序:根 左子树 右子树 确定根
中序:左子树 根 右子树 确定左右子树,分割左右区间
preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]


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]

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)
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {}
};
前序遍历刚开始肯定是把左边一路节点遍历,所以可以这么看一颗二叉树:

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

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 的右子树
本质:从栈里取到栈顶节点,则这个节点的左子树已经访问完了,可以访问节点、右子树了
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
一个节点右子树为空,或上一个访问的节点是右子树的根,则说明右子树访问过了,可以访问当前根节点
本质:从栈里取到栈顶节点,则这个节点的左子树已经访问完了
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;}
};
本篇的分享就到这里了,感谢观看,如果对你有帮助,别忘了点赞+收藏+关注。
小编会以自己学习过程中遇到的问题为素材,持续为您推送文章
