大家好,我是白晨,一个不是很能熬夜,但是也想日更的人✈。如果喜欢这篇文章,点个赞,关注一下白晨吧!你的支持就是我最大的动力!
虽然还有很多课,但是也不能忘了写编程题呀。
本次白晨为大家总结了二叉树的经典进阶题目,需要一定的二叉树的基础,如果没有了解过二叉树的同学可以先读【数据结构】二叉树全解析(入门篇)了解一下二叉树这种十分经典的数据结构。这次白晨总结的题目都是互联网大厂必考的二叉树题目,也是思想非常经典的题目,第一次做可能想不到这样的思路,但是当见得多了就自然而然有这样的思路了,在做题中就是锻炼这种思路的过程。
都是很有代表性的经典题目,适合大家复习和积累经验。
大家可以自己先试着自己挑战一下,再来看解析哟!
原题链接
:根据二叉树创建字符串
算法思想
:
打眼一看就应该是前序遍历得到的字符串,但是为了将树的层次分清楚(也就是带括号),需要对前序遍历进行一定的控制。
什么叫不影响字符串与原始二叉树之间的一对一映射关系的空括号对呢?
其实就是一个结点有左子树但是没有右子树,不需要再用一个空括号去表示右空树,可以直接省略这个括号。但是如果一个结点有右子树但是没有左子树,则不能省略左空树的括号,不然会导致左右子树无法分辨的问题。
具体实例可以见力扣题目的例题,展示的很详细。
前序遍历思路:
代码实现
:
class Solution {
public:
void _tree2str(TreeNode* root, string& s) {
if (root == nullptr)
return;
s += to_string(root->val);
// 只要左右结点有一个为真,就遍历左子树
if (root->left || root->right)
{
s += '(';
_tree2str(root->left, s);
s += ')';
}
// 右子树为真,遍历右子树
if (root->right)
{
s += '(';
_tree2str(root->right, s);
s += ')';
}
}
string tree2str(TreeNode* root) {
string s;
_tree2str(root, s);
return s;
}
};
class Solution {
public:
string tree2str(TreeNode* root) {
// root为空,返回空字符串
if (root == nullptr)
return "";
// 当左右都为空,说明是叶子结点,直接返回结点值的字符串,不用任何括号
if (root->left == nullptr && root->right == nullptr)
return to_string(root->val);
// 当左子树不为空,右子树为空,只用加一对括号,遍历左子树
if (root->left && root->right == nullptr)
return to_string(root->val) + '(' + tree2str(root->left) + ')';
// 一般情况,左右都不为空,分别遍历左右子树
return to_string(root->val) + '(' + tree2str(root->left) + ")(" + tree2str(root->right) + ')';
}
};
原题链接
:二叉树的层序遍历
算法思想
:
代码实现
:
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 num = 1;// 记录每一层的节点个数,第一层为1
vector<int> v;// 记录一层的输出
// 当队中不为空,继续循环
while (!q.empty())
{
TreeNode* cur = q.front();
q.pop();
num--;// 一层剩余结点数-1
v.push_back(cur->val);// 存放数据
if (cur->left)
q.push(cur->left);
if (cur->right)
q.push(cur->right);
// 当一层没有结点了,说明这一层遍历完了,准备进行下一层的遍历
if (num == 0)
{
vv.push_back(v);// 将这一层的数据记录
v.clear();// 清空v
num = q.size();// 此时队列的长度就是下一层结点的数量
}
}
return vv;
}
};
原题链接
:二叉树的层序遍历 II
算法思想
:
代码实现
:
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> vv;
if (root == nullptr)
return vv;
queue<TreeNode*> q;
q.push(root);
int num = 1;
vector<int> v;
while (!q.empty())
{
TreeNode* cur = q.front();
q.pop();
num--;
v.push_back(cur->val);
if (cur->left)
q.push(cur->left);
if (cur->right)
q.push(cur->right);
if (num == 0)
{
vv.push_back(v);
v.clear();
num = q.size();
}
}
// 结果逆置
reverse(vv.begin(), vv.end());
return vv;
}
};
原题链接
:二叉树的最近公共祖先
- 法一:查找左右子树法
算法思想
:
何为最近公共祖先?
如果不算自己就是祖先的情况,那么我们可以定义:
p,q
公共祖先结点就是如果一个结点n
可以在左子树可以找到结点p (q)
,在右子树找到结点q (p)
,那么这个结点n
就是最近公共祖先结点。我们将定义拓展,
- 如果一个结点
n
本身就是题目给出的结点p (q)
,并且在其子树中找到另一个结点q (p)
,那么这个结点也算是p,q
最近公共的祖先结点。那么,有没有可能同时出现两个满足上面条件之一的结点呢?
答案是不可能,大家可以自行去验证。
所以,我们可以唯一找到一个符合上面条件之一的结点。
根据上面的分析,我们可以得到一种方法:
p,q
结点。p,q
结点分别在此结点的左右(右左)子树,此节点就是公共祖先结点,返回此结点。p,q
结点都在此结点的左(右)子树,这个结点是这两个结点的祖先结点,但不是最近的,需要继续向这个结点左(右)孩子结点继续查找,直到发现p,q
结点在不同的子树上。p,q
结点为当前查找的结点,根据前序遍历,根节点先被访问,根据题意,必有最近公共祖先结点。所以,p,q
先被访问的结点就是最近公共祖先结点。eg. leetcode示例二
代码实现
:
class Solution {
public:
// 查找函数
bool Find(TreeNode* root, TreeNode* x)
{
if (root == nullptr)
return false;
if (root == x)
return true;
return Find(root->right, x) || Find(root->left, x);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 遍历到空节点,返回空
if (root == nullptr)
return nullptr;
// 遍历到p结点,所以最近公共祖先就是p
if (root == p)
return p;
// 遍历到q结点,所以最近公共祖先就是q
else if (root == q)
return q;
// 查找此节点的左右子树
bool pInLeft = Find(root->left, p), pInRight = !pInLeft;
bool qInLeft = Find(root->left, q), qInRight = !qInLeft;
// 当p,q分别在不同子树时,此节点就是最近公共祖先
if ((pInLeft && qInRight) || (pInRight && qInLeft))
return root;
// 当p,q在同一棵子树时,向着子树方向继续找
else if (pInRight && qInRight)
return lowestCommonAncestor(root->right, p, q);
else if (pInLeft && qInLeft)
return lowestCommonAncestor(root->left, p, q);
// p,q不在此节点的左右子树
else
return nullptr;
}
};
- 法二:保存路径法
算法思想
:
p,q
结点重复查找了很多次,浪费了时间,所以我们可以将其优化。p,q
结点时,将其从根结点到p,q
结点的路径进行记录,最后再比较路径,找出最近的公共结点即可。代码实现
:
class Solution {
public:
// 查找并保存路径
bool FindAndRecord(TreeNode* root, TreeNode* x, stack<TreeNode*>& st)
{
if (root == nullptr)
return false;
// 入栈根节点
st.push(root);
// 前序查找
if (root == x)
return true;
if (FindAndRecord(root->left, x, st))
return true;
if (FindAndRecord(root->right, x, st))
return true;
// 找不到,出栈根节点
st.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr)
return nullptr;
stack<TreeNode*> stp, stq;
FindAndRecord(root, p, stp);
FindAndRecord(root, q, stq);
// 结点高度深的先走
while (stp.size() != stq.size())
{
if (stp.size() > stq.size())
stp.pop();
else
stq.pop();
}
// 当两个栈大小相同时,开始同时出栈,直到找到相同结点
while (stp.top() != stq.top())
{
stp.pop();
stq.pop();
}
return stp.top();
}
};
- 法三:后序查找
算法思想
:
代码实现
:
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 如果结点为空,为目标结点,直接返回根节点即可
if (root == nullptr || root == p || root == q)
return root;
TreeNode* lson = lowestCommonAncestor(root->left, p, q);
TreeNode* rson = lowestCommonAncestor(root->right, p, q);
if (lson == nullptr)
return rson;
if (rson == nullptr)
return lson;
return root;
}
};
原题链接
:二叉搜索树与双向链表
算法思想
:
代码实现
:
class Solution {
public:
// 用引用不断改变前驱结点
void InOrderConnect(TreeNode* cur, TreeNode*& prev)
{
if (cur == nullptr)
return;
InOrderConnect(cur->left, prev);
// 如果前驱结点不为空,前驱结点的后继节点就是当前结点
// 前驱结点为空:中序遍历的第一个结点的前驱就为空
if (prev)
prev->right = cur;
// 当前结点的前驱结点就是prev
cur->left = prev;
// 跟新前驱节点
prev = cur;
InOrderConnect(cur->right, prev);
}
TreeNode* Convert(TreeNode* pRootOfTree) {
if (pRootOfTree == nullptr)
return nullptr;
TreeNode* prev = nullptr;
TreeNode* head = pRootOfTree;
InOrderConnect(pRootOfTree, prev);
// 找头节点
while (head->left)
head = head->left;
return head;
}
};
class Solution {
public:
TreeNode* head = nullptr;
TreeNode* prev = nullptr;
TreeNode* Convert(TreeNode* pRootOfTree) {
if (pRootOfTree == nullptr)
return nullptr;
Convert(pRootOfTree->left);
// 顺便记录头节点
if (head == nullptr)
head = prev = pRootOfTree;
else
{
prev->right = pRootOfTree;
pRootOfTree->left = prev;
prev = pRootOfTree;
}
Convert(pRootOfTree->right);
return head;
}
};
原题链接
:从前序与中序遍历序列构造二叉树
算法思想
:
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
,对于中序遍历,我们得到的序列是[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
。代码实现
:
class Solution {
public:
// 前序序列构建二叉树
TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int& prei, int inbegin, int inend)
{
// 当中序的开始下标大于结束下标,说明这棵树不存在,返回nullptr
if(inbegin > inend)
return nullptr;
// 以当前前序结点创建树结点
TreeNode* root = new TreeNode(preorder[prei]);
// 在中序序列中找根节点
int rooti = inbegin;
while(rooti <= inend)
{
if(inorder[rooti] == preorder[prei])
break;
else
rooti++;
}
// 前序序列的下标++,遍历下一个根节点
prei++;
// 递归创建左右子树
// 左子树中序区间[inbegin, rooti - 1]
root->left = _buildTree(preorder, inorder, prei, inbegin, rooti - 1);
// 右子树中序区间[rooti + 1, inend]
// 注意在此时prei可能已经不是左子树传递的prei了,因为prei为引用,会实时改变
root->right = _buildTree(preorder, inorder, prei, rooti + 1, inend);
// 返回根结点
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
// 前序遍历从下标为0的根节点开始,注意prei传递的是引用类型,方便实时修改根节点下标
int prei = 0;
TreeNode* head = _buildTree(preorder, inorder, prei, 0, inorder.size() - 1);
return head;
}
};
原题链接
:从中序与后序遍历序列构造二叉树
算法思想
:
[[左子树的前序遍历结果], [右子树的前序遍历结果],根节点]
,对于中序遍历,我们得到的序列是[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
。[根节点, [右子树的前序遍历结果],[左子树的前序遍历结果]]
,所以我们应该先创建右子树,再创建左子树,才能保证遍历的顺序和创建子树的顺序对应。代码实现
:
class Solution {
public:
TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder, int& posti, int inbegin, int inend)
{
// 当中序的开始下标大于结束下标,说明这棵树不存在,返回nullptr
if(inbegin > inend)
return nullptr;
// 创建根节点
TreeNode* root = new TreeNode(postorder[posti]);
// 在中序序列中找根节点
int rooti = inbegin;
while(rooti <= inend)
{
if(inorder[rooti] == postorder[posti])
break;
else
rooti++;
}
// 后序序列从后向前,所以下标--
posti--;
// 先创建右子树,再创建左子树
root->right = _buildTree(inorder, postorder, posti, rooti + 1, inend);
root->left = _buildTree(inorder, postorder, posti, inbegin, rooti - 1);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
int posti = postorder.size() - 1;
TreeNode* head = _buildTree(inorder, postorder, posti, 0, inorder.size() - 1);
return head;
}
};
原题链接
:二叉树的前序遍历
算法思想
:
st
来模拟递归的情况,使用数组v
存储遍历结果,cur
表示当前遍历的结点cur
初始值为root
。cur
节点出发,如果cur
的左子树不为空,将当前结点入栈,并且将当前结点cur
对应的元素值加入v
,再让cur
向左走。直到cur
的左子树为空。right
赋给cur
,并将栈顶结点出栈(与递归不同的是,递归是将右子树遍历完才出栈,这里将右子树给cur后就可以出栈了)。cur
为空。cur
为空。代码实现
:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
while (cur || !st.empty())
{
// 一直向左走
while (cur)
{
// 入栈cur
st.push(cur);
// 由于是前序序列,在入栈元素时就要将其元素值加入结果。
v.push_back(cur->val);
cur = cur->left;
}
// 访问栈顶元素的右子树
TreeNode* top = st.top();
st.pop();
cur = top->right;
}
return v;
}
};
原题链接
:二叉树的中序遍历
算法思想
:
[左,根,右]
,所以直到将根结点出栈时(与递归不同的是,递归是将右子树遍历完才出栈,这里将右子树给cur后就可以出栈了),才能将根结点的元素值加入v
。st
来模拟递归的情况,使用数组v
存储遍历结果,cur
表示当前遍历的结点cur
初始值为root
。cur
节点出发,如果cur
的左子树不为空,将当前结点入栈,再让cur
向左走。直到cur
的左子树为空。right
赋给cur
,将栈顶结点对应的元素值加入v
,并将栈顶结点出栈。cur
为空。cur
为空。代码实现
:
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> v;
TreeNode* cur = root;
while (cur || !st.empty())
{
// 一直向左走
while (cur)
{
st.push(cur);
cur = cur->left;
}
// 出栈栈顶元素,并将其元素值加入v
TreeNode* top = st.top();
v.push_back(top->val);
st.pop();
// 开始遍历栈顶元素的右子树
cur = top->right;
}
return v;
}
};
原题链接
: 二叉树的后序遍历
算法思想
:
[左,右,根]
,所以后序遍历的根节点必须要等左右子树都遍历完才能加入v
,在遍历到根节点是还必须要区分是否将左右子树全都遍历完。pair
结构记录有没有访问右子树,如果已经访问过,就可以出栈根节点,如果没有访问过,那就访问右子树,并且将bool值改为true。prev
记录当前结点的前驱结点。根据后序遍历顺序,如果当前结点的前驱结点恰巧就是当前结点的右子树,那么说明当前结点的左右子树已经全部被访问完,根节点可以出栈。反之,如果当前结点的前驱结点不是当前结点的右子树,那么此时没有访问右子树,不能出栈。st
来模拟递归的情况,使用数组v
存储遍历结果,cur
表示当前遍历的结点cur
初始值为root
。cur
节点出发,如果cur
的左子树不为空,将当前结点入栈,再让cur
向左走。直到cur
的左子树为空。v
,更新prev
。反之,将栈顶元素的right
赋给cur
,遍历右子树。cur
为空。代码实现
:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
TreeNode* cur = root, * prev = nullptr;
stack<TreeNode*> st;
vector<int> v;
while (cur || !st.empty())
{
while (cur)
{
st.push(cur);
cur = cur->left;
}
TreeNode* top = st.top();
// 栈顶元素的右子树为空 或者 上一个出栈的元素是栈顶元素的右子树根 时
// 栈顶元素出栈(可能会有连续出栈,可自行画棵树验证)
if (top->right == nullptr || prev == top->right)
{
v.push_back(top->val);
st.pop();
prev = top;
}
else
{
cur = top->right;
}
}
return v;
}
};
这次题目是二叉树
这个面试最爱考的题目,比较考验大家的逻辑思维以及代码实现能力,相信大家做完会有所收获。
《二叉树经典进阶题目》——隶属于【刷题日记】系列,白晨开这个系列的目的是向大家分享经典的笔试编程题,以便于大家参考,查漏补缺以及提高代码能力。如果你喜欢这个系列的话,不如关注白晨,更快看到更新呀。
如果喜欢这个系列的话,不如订阅【刷题日记】系列专栏,更快看到更新哟
如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦!
如果这篇文章有帮到你,还请给我一个大拇指
和小星星
⭐️支持一下白晨吧!喜欢白晨【刷题日记】系列的话,不如关注
白晨,以便看到最新更新哟!!!