【C++进阶】----二叉树进阶算法题
1.根据二叉树创建字符串

思路:
前序遍历(根、左子树、右子树)将二叉树转换成字符串,把子树用括号包起来,空树的情况需要特殊处理。
1.如果左子树为空,空括号是否需要保留取决于右子树,右子树不为空,左子树的空括号需要保留,反之则不需要。
2.如果右子树为空,右子树的空括号不需要保留。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
string tree2str(TreeNode* root) {
string ret;
// 子树不存在,返回空字符串
if(root==nullptr)
return ret;
// 根,将数字转换成字符
ret+=to_string(root->val);
// 左子树
// 如果左子树不为空,元素需要用括号括起来
// 如果左子树为空,右子树不为空,需要保留左子树的空括号
if(root->left || root->right)
{
ret+='(';
ret+=tree2str(root->left);
ret+=')';
}
// 右子树
// 如果右子树不为空,元素需要用括号括起来
if(root->right)
{
ret+='(';
ret+=tree2str(root->right);
ret+=')';
}
return ret;
}
};
2.二叉树的层序遍历

思路:
二叉树的层序遍历可以利用队列先进先出的性质,上一层每个节点出队列的时候,将其左右孩子(即下一层的节点)带入队列。
我们在层序遍历过程中,增加⼀个levelSize,记录每层的数据个数,树不为空的情况下, 第1层levelSize=1,循环控制,第1层出完了,第2层就都进队列了,队列中size就是第2层的数据个 数。以此内推,假设levelSize为第n层的数据个数,因为层序遍历思想为当前层结点出队列,带⼊下⼀ 层结点(也就是⼦结点),循环控制第n层数据出完了,那么第n+1层结点都进队列了,队列size,就是下⼀ 层的levelSize。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q;
vector<vector<int>> vv;
int levelSize=0;
if(root)
{
q.push(root);
levelSize=1;
}
while(!q.empty())
{
vector<int> v;
// 记录当前层的节点个数
while(levelSize--)
{
TreeNode* cur=q.front();
q.pop();
v.push_back(cur->val);
// 将下一层的节点入队列
if(cur->left)
q.push(cur->left);
if(cur->right)
q.push(cur->right);
}
vv.push_back(v);
levelSize=q.size();
}
return vv;
}
};
二叉树的层序遍历(||)
反向层序遍历,在返回之前reverse一下即可
3.二叉树的最近公共祖先

思路1
思路1:时间复杂度:O(N^2)
观察一下,两个节点,最近公共祖先的特征就是一个节点在最近公共祖先的左子树,一个节点在右子树。⽐如6和4的公共祖先有5和3,但是只有最近公共祖先5满⾜6在左边,4在 右边。
当然也有特例:题目说了一个节点也可以是它的祖先,所以如果一个节点是另一个的祖先,那这个节点就是最近公共祖先。
"逐层判断法" 求解二叉树的最近公共祖先,核心思路如下:
核心依据:最近公共祖先的关键特征是目标节点
p和q分别位于其左、右子树中;或者其中一个目标节点本身就是公共祖先。判断节点是否在子树中:通过
isInTree函数递归检查某个节点(p或q)是否存在于以当前节点为根的子树中。逐层遍历判断:
- 从根节点开始,逐层检查当前节点
cur是否为p或q(若是则直接返回,因为自身就是最近公共祖先)。- 若不是,则分别判断
p和q是否在cur的子树中:
- 若
p和q分别在左、右子树中,当前节点cur就是最近公共祖先。- 若两者都在左子树,就将
cur移至左孩子继续判断。- 若两者都在右子树,就将
cur移至右孩子继续判断。
缺点:
- 时间复杂度:在最坏情况下(如链表式树),每次调用
isInTree都会遍历整棵树,导致整体时间复杂度达到 O (N²)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
// 思路1:O(N)
// 最近公共祖先的特点:p、q分别在以最近公共祖先为根的子树的两侧
// 特例:p、q之中有一个就是最近公共祖先
// x节点是否在root子树中
bool isInTree(TreeNode* root,TreeNode* x)
{
if(root==nullptr)
return false;
return root==x
||isInTree(root->left,x)
||isInTree(root->right,x);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==nullptr)
return nullptr;
TreeNode* cur=root;
while(cur)
{
// 特例:p、q之中有一个就是最近公共祖先
if(cur==p || cur==q)
return cur;
bool pIsInLeft=isInTree(cur->left,p);
bool pIsInRight=!pIsInLeft;
bool qIsInLeft=isInTree(cur->left,q);
bool qIsInRight=!qIsInLeft;
// 在两侧,cur就是最近公共祖先
if((pIsInLeft && qIsInRight) || (pIsInRight && qIsInLeft))
return cur;
// 如果p、q都在左子树或者右子树,更新cur
else if(pIsInLeft && qIsInLeft)
cur=cur->left;
else if(pIsInRight && qIsInRight)
cur=cur->right;
}
return nullptr;
}
};
思路2
思路2:时间复杂度:O(N)
如果能求出两个结点到根的路径,那么就可以转换为链表相交问题。
如:6到根3的路径为6- >5->3,4到根3的路径为4->2->5->3,那么看做两个链表找交点,交点5就是最近公共祖先。
"路径追踪法" 求解二叉树的最近公共祖先,核心思路如下:
获取路径:通过前序遍历分别找到从根节点到目标节点
p和q的完整路径,并用栈存储这两条路径。
- 遍历过程中,先将当前节点入栈
- 若找到目标节点则返回成功
- 若未找到,递归搜索左右子树
- 若左右子树均未找到目标节点,则将当前节点出栈(回溯)
对齐路径长度:由于两条路径起点都是根节点,将较长路径从顶部弹出节点,直到两条路径长度相等。
寻找公共祖先:同时从两条路径的顶部弹出节点,第一个相同的节点即为
p和q的最近公共祖先
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
// 思路2:O(N)
// 通过前序遍历二叉树,查找p和q的祖先路径,存到栈中
bool GetPath(TreeNode* root,TreeNode* x,stack<TreeNode*>& path)
{
if(root==nullptr)
return false;
// 前序遍历,找x的路径
path.push(root);
if(root==x)
return true;
if(GetPath(root->left,x,path))
return true;
if(GetPath(root->right,x,path))
return true;
// 如果x不在root的左右子树中,说明root不在x的路径中
path.pop();
return false;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
stack<TreeNode*> pPath;
stack<TreeNode*> qPath;
GetPath(root,p,pPath);
GetPath(root,q,qPath);
// 长的先走,找相交点
while(pPath.size()!=qPath.size())
{
if(pPath.size()>qPath.size())
pPath.pop();
else
qPath.pop();
}
while(qPath.top()!=pPath.top())
{
pPath.pop();
qPath.pop();
}
return pPath.top();
}
};
4.将二叉搜索树转化为排序的双向链表

思路:
- 利用 BST 特性:中序遍历(左→根→右)结果为升序,这是链表的有序基础;
- 递归中序遍历:用
prev记录上一节点,遍历中让当前节点左指prev、prev右指当前节点,逐步构建双向链表;- 补循环:遍历结束后,找到链表头(BST 最左节点)和尾(
prev最终指向),让头左连尾、尾右连头,形成循环。
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node() {}
Node(int _val) {
val = _val;
left = NULL;
right = NULL;
}
Node(int _val, Node* _left, Node* _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
public:
// 二叉搜索树->排序->中序遍历
void InOrderConvert(Node* cur,Node*& prev)
{
if(cur==nullptr)
return;
// 中序遍历
InOrderConvert(cur->left,prev);
// 当前结点的左,指向前⼀个结点
cur->left=prev;
// 前⼀个结点的右,指向当前结点
if(prev)
prev->right=cur;
// 更新前驱节点
prev=cur;
InOrderConvert(cur->right,prev);
}
Node* treeToDoublyList(Node* root) {
if(root==nullptr)
return nullptr;
Node* prev=nullptr;
InOrderConvert(root,prev);
// 从根开始往左⾛,找到第⼀个结点
Node* head=root;
while(head->left)
head=head->left;
// head为第⼀个结点,prev是最后⼀个结点
// 首尾相连形成双向链表
head->left=prev;
prev->right=head;
return head;
}
};
5.从前序与中序遍历序列构造二叉树

思路:
利用遍历特性:
- 前序遍历的第一个元素是根节点
- 中序遍历中,根节点左侧是左子树节点,右侧是右子树节点
递归构建过程:
- 从前序序列取当前根节点(通过
previ索引追踪)- 在中序序列中找到根节点位置
rooti,分割出左子树区间[inbegin, rooti-1]和右子树区间[rooti+1, inend]- 递归构建左子树(使用左子树区间)和右子树(使用右子树区间)
- 每构建一个节点就移动
previ索引(previ++),获取下一个根节点- 注意:前序数组中存放的顺序是:根、左子树、右子树,所以索引previ++后,previ指向的是左子树的根,应该优先递归构造左子树。
终止条件:当中序区间
inbegin > inend时,中序区间不存在,说明该方向无子树,返回空通过这种分治思想,将大问题拆解为构建左、右子树的小问题,最终递归重建出完整二叉树,时间复杂度为 O (N)(每个节点处理一次)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// previ,前序下标,inbegin、inend:中序区间
TreeNode* _buildTree(vector<int>& preorder,vector<int>& inorder,int& previ,int inbegin,int inend)
{
// 结束条件:中序区间不存在,如[0,1],无法构建子树
if(inbegin>inend)
return nullptr;
// 根据前序确定根,根据中序分割左右区间
TreeNode* root=new TreeNode(preorder[previ]);
// 在中序区间[inbegin,inend]中查找根的位置
int rooti=inbegin;
while(rooti<inend)
{
if(inorder[rooti]==preorder[previ])
break;
rooti++;
}
// 更新根
previ++;
// 区间分割成[inbegin,rooti-1]rooti[rooti+1,inend]
// 递归构造root的左子树
root->left=_buildTree(preorder,inorder,previ,inbegin,rooti-1);
root->right=_buildTree(preorder,inorder,previ,rooti+1,inend);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int i=0;
TreeNode* root = _buildTree(preorder,inorder,i,0,inorder.size()-1);
return root;
}
};
从后序与中序遍历序列构造二叉树
思路:
利用遍历特性:
- 后序遍历的最后一个元素是根节点
- 中序遍历中,根节点左侧是左子树节点,右侧是右子树节点
递归构建过程:
- 从后序序列取当前根节点(通过
posti索引追踪)- 在中序序列中找到根节点位置
rooti,分割出左子树区间[inbegin, rooti-1]和右子树区间[rooti+1, inend]- 递归构建左子树(使用左子树区间)和右子树(使用右子树区间)
- 每构建一个节点就移动
posti索引(posti--),获取下一个根节点- 注意:后序数组中存放的顺序是:左子树、右子树、根,所以索引posti后,posti指向的是右子树的根,应该优先递归构造右子树。
终止条件:当中序区间
inbegin > inend时,中序区间不存在,说明该方向无子树,返回空通过这种分治思想,将大问题拆解为构建左、右子树的小问题,最终递归重建出完整二叉树,时间复杂度为 O (N)(每个节点处理一次)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& posti,int inbegin,int inend)
{
// 结束条件:中序区间不存在,如[0,1],无法通过中序区间构造子树
if(inbegin>inend)
return nullptr;
// 后续确定根
TreeNode* root=new TreeNode(postorder[posti]);
// 在中序区间中查找根的位置,从而分割出左右子树的中序区间
int rooti=inbegin;
while(rooti<inend)
{
if(inorder[rooti]==postorder[posti])
break;
rooti++;
}
// 更新根,从后向前
posti--;
// 区间分割成[inbegin,rooti-1]rooti[rooti+1,inend]
// 与前序:根、左子树、右子树不同
// 后序:左子树、右子树、根
// 在后序数组中,从后往前分别确认的是根、右子树、左子树的根
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 i=postorder.size()-1;
TreeNode* root=_buildTree(inorder,postorder,i,0,inorder.size()-1);
return root;
}
};
6.二叉树的前序遍历

迭代法(非递归)实现二叉树的前序遍历,核心思路是利用栈模拟递归过程,遵循 “根→左→右” 的遍历顺序,具体步骤如下:
核心逻辑:通过栈存储待访问的节点,分两步循环处理:
- 访问左路节点:从当前节点出发,沿左子树一直深入,沿途节点依次入栈并记录值(实现 “根→左” 的访问);
- 访问左路节点的右子树:当左路节点访问完毕(当前节点为空),弹出栈顶节点,转向其右子树,重复第一步(将右子树视为新的待访问树)。
循环条件:
cur不为空(还有未访问的子树)或栈不为空(还有未处理左路节点的右子树)时,循环继续。遍历顺序保证:通过 “先左路入栈并记录,再处理右子树” 的方式,严格遵循前序遍历 “根→左→右” 的顺序。
该方法用栈替代递归调用栈,空间复杂度为 O (H)(H 为树高),时间复杂度为 O (N)(每个节点访问一次),高效实现了非递归的前序遍历。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 访问一棵树
// 1.访问左路节点,并放入栈中
// 2.左路节点访问完了,取栈顶元素(最后一个访问的左路节点),访问左路节点的右子树
vector<int> preorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur=root;
// 1.cur不为空表示还有子树没有访问
// 2.st不为空表示还有左路节点的右子树没有访问
while(cur || !st.empty())
{
// 访问一棵树的开始
// 1.访问左路节点,左路节点入栈
while(cur)
{
st.push(cur);
v.push_back(cur->val);
cur=cur->left;
}
// 2.取栈顶元素(最后一个访问的左路节点)
TreeNode* top=st.top();
st.pop();
// 访问左路节点的右子树
// 循环子问题方式访问左路节点的右子树
cur=top->right;
}
return v;
}
};
二叉树的中序遍历
迭代法(非递归)实现二叉树的中序遍历,核心是借助栈模拟递归逻辑,严格遵循 “左→根→右” 的遍历顺序,思路拆解如下:
核心逻辑:通过栈暂存未访问 “根节点”,分两步循环处理:
- 压入左路节点:从当前节点
cur出发,沿左子树一路深入,将所有左路节点依次压入栈(暂不记录值,仅存节点),直到cur为空(左路节点处理完);- 处理根与右子树:弹出栈顶节点(此时其左子树已处理完),记录该节点值(实现 “根” 的访问),再将
cur指向其右子树(后续对右子树重复第一步,实现 “右” 的遍历)。循环条件:
cur不为空(还有未遍历的左路节点)或栈不为空(还有未处理的根节点)时,循环继续。顺序保证:通过 “先压左→弹栈记根→再处理右” 的流程,精准匹配中序遍历 “左子树→根节点→右子树” 的顺序。
该方法空间复杂度为 O (H)(H 为树高,栈存左路节点),时间复杂度 O (N)(每个节点压栈、弹栈各一次),高效完成非递归中序遍历。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 访问一棵树
// 1.访问左路节点,并放入栈中
// 2.左路节点访问完了,取栈顶元素(最后一个访问的左路节点),访问左路节点的右子树
vector<int> inorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur=root;
// 1.cur不为空,表示还有子树没有访问
// 2.st不为空,表示还有左路节点的右子树没有访问
while(cur || !st.empty())
{
// 访问一棵树
// 1.访问左路节点
while(cur)
{
st.push(cur);
cur=cur->left;
}
// 取栈顶元素(最后一个入栈的最路节点)
TreeNode* top=st.top();
st.pop();
// 访问左路节点的右孩子之前先将节点入数组
// 因为中序:左子树、根、右子树
v.push_back(top->val);
// 2.访问左路节点的右子树
// 循环子问题的方式访问左路节点的右子树
cur=top->right;
}
return v;
}
};
二叉树的后序遍历
迭代法(非递归)实现二叉树的后序遍历,核心是借助栈暂存节点 +
prev指针记录上一访问节点,解决 “根节点需在左右子树后访问” 的问题,严格遵循 “左→右→根” 顺序,思路如下:
核心逻辑:通过 “栈存未处理节点 +
prev判右子树状态”,分两步循环:
- 压入左路节点:从
cur出发,沿左子树深入,将所有左路节点压栈(暂不记录),直到cur为空(左路处理完);- 判断根节点是否可访问:取栈顶节点(左子树已处理),通过
prev判断其右子树状态:
- 若右子树为空,或
prev是右子树的根(说明右子树已访问):弹出栈顶节点,记录值(实现 “根” 的访问),更新prev为该节点;- 若右子树未访问:将
cur指向右子树,重复第一步(处理右子树)。循环条件:
cur不为空(有未压栈的左路节点)或栈不为空(有未判断的根节点)时,循环继续。顺序保证:通过
prev指针精准判断右子树是否访问,确保 “左→右→根” 的后序顺序 —— 只有左右子树都处理完,才记录根节点值。该方法空间复杂度 O (H)(H 为树高),时间复杂度 O (N)(每个节点压栈、弹栈各一次),高效解决了非递归后序遍历的核心难点。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
// 访问一棵树
// 1.访问左路节点,并将左路节点入栈
// 2.左路节点访问完了,取栈顶元素(最后一个入栈的左路节点)
// 取一个左路节点时,左路节点的左孩子已经访问过了
// 后续:左子树、右子树、根
// 如果左路节点的右子树为空,这个左路节点可以入数组
// a.如果左路节点的右子树不为空,右子树没有访问过,那么上一个访问的节点是左子树的根
// b.如果左路节点的右子树不为空,右子树访问过了,那么上一个访问的节点是右子树的根
// 因此我们要增加一个前驱变量,用于判断左路节点的右子树是否访问过
vector<int> postorderTraversal(TreeNode* root) {
vector<int> v;
stack<TreeNode*> st;
TreeNode* cur=root;
// 增加一个前驱变量,用于记录上一个访问的节点
TreeNode* prev=nullptr;
// 1.cur不为空表示还有子树没有访问
// 2.st不为空表示还有左路节点的右子树没有访问
while(cur || !st.empty())
{
// 访问一棵树
// 1.访问左路节点,将左路节点入栈
while(cur)
{
st.push(cur);
cur=cur->left;
}
// 取栈顶元素(最后一个访问的左路节点)
TreeNode* top=st.top();
// 如果的右子树为空或者上一个访问的节点是右子树的根,代表右子树访问过来
// 可以访问当前根
if(top->right==nullptr || prev==top->right)
{
st.pop();
// 更新上一个访问的节点
prev=top;
v.push_back(top->val);
}
// 右子树不为空,且没有访问过
// 访问左路节点的右子树
// 循环子问题的方式访问左路节点的右子树
else
{
cur=top->right;
}
}
return v;
}
};
总结
前中后序遍历的思路是一样的
1.访问一棵树时,先访问左路节点
2.访问左路节点的右子树
区别:记录节点的值的时机不提供
a.前序遍历需要在将左路节点压入栈的时候记录值
b.中序遍历需要在弹出左路节点的时候记录值
c.后序遍历需要在确保左右子树访问过之后弹出左路节点时记录值
相比于前序遍历和中序遍历,后序遍历访问左路节点的右子树时需要考虑右子树是否已经访问过了,所以后序遍历额外引入了一个perv指针用于记录上一个访问的节点。
一句话总结
前、中、后序遍历的本质是根节点的访问时机差异:
- 前序:见根就取(压栈时访问);
- 中序:左空再取(弹栈时访问);
- 后序:右完再取(需
prev判断右子树状态,弹栈时访问)。递归是 “天然栈” 实现,迭代是 “手动栈” 模拟,核心都是围绕 “左→右” 的子树优先级,控制根的访问顺序。
更多推荐
所有评论(0)