1. 二叉树基础回顾与OJ解题方法论
在开始解决这些二叉树OJ题目之前,我们需要先夯实基础。二叉树作为非线性数据结构,在C++中通常通过结构体或类来实现节点定义。一个标准的二叉树节点包含数据域和左右子节点指针:
cpp复制struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
二叉树问题解决的核心在于遍历方式的选择。前序(根-左-右)、中序(左-根-右)和后序(左-右-根)这三种深度优先遍历,以及层次遍历(广度优先),构成了解决绝大多数二叉树问题的基础框架。
实际工程中,我习惯在节点结构体中加入父节点指针,这在处理如"最近公共祖先"等问题时会大幅简化代码逻辑,但会增加插入/删除操作的复杂度。
递归是处理二叉树最直观的方式,但需要注意栈溢出风险。对于大型树结构,必须掌握非递归实现方法。以非递归前序遍历为例,我们需要显式使用栈来模拟调用过程:
cpp复制void preorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
if (root) s.push(root);
while (!s.empty()) {
TreeNode* node = s.top(); s.pop();
cout << node->val << " ";
if (node->right) s.push(node->right); // 右子节点先入栈
if (node->left) s.push(node->left); // 左子节点后入栈
}
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二叉树创建字符串问题详解
LeetCode 606题要求我们将二叉树转换为特定的字符串表示形式,其中空子树用"()"表示,但可以省略不影响唯一性的括号。例如:
code复制 1
/ \
2 3
/
4
应转换为"1(2(4))(3)"。这个问题的关键在于理解括号省略的规则:当右子树存在而左子树为空时,必须保留左子树的空括号;否则可以省略。
我的解题思路采用前序遍历框架,在访问每个节点时处理括号逻辑:
cpp复制string tree2str(TreeNode* root) {
if (!root) return "";
string res = to_string(root->val);
if (!root->left && !root->right) return res;
res += "(" + tree2str(root->left) + ")"; // 左子树必须处理
if (root->right) // 右子树选择性处理
res += "(" + tree2str(root->right) + ")";
return res;
}
实际编码时容易忽略右子树存在而左子树为空的情况,测试用例要特别关注这种边界条件。我在第一次提交时就因为没有处理这种情况导致WA。
3. 最近公共祖先(LCA)问题深度解析
寻找二叉树中两个节点的最近公共祖先(Lowest Common Ancestor)是面试高频题。LeetCode 236题给出了标准定义:对于有根树T的两个节点p、q,最近公共祖先表示为一个节点x,满足x是p和q的祖先且x的深度尽可能大。
3.1 递归解法
递归解法基于后序遍历思想,核心逻辑是:
- 如果当前节点是p或q,则返回当前节点
- 递归查询左右子树
- 如果左右子树都返回非空节点,说明当前节点就是LCA
- 否则返回非空的那一侧结果
cpp复制TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
3.2 父指针法
对于有父指针的树结构,可以转化为链表相交问题:
- 从p和q分别向上遍历到根节点,记录路径
- 找到两条路径最后一个相同的节点
cpp复制TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
unordered_set<TreeNode*> visited;
while (p) {
visited.insert(p);
p = p->parent;
}
while (q) {
if (visited.count(q)) return q;
q = q->parent;
}
return nullptr;
}
在工程实践中,如果树结构频繁查询LCA,可以考虑使用Tarjan离线算法或倍增法预处理,将查询时间复杂度降到O(1)。
4. 二叉搜索树与双向链表转换
剑指Offer 36题要求将二叉搜索树转换为排序的双向链表,要求不能创建新节点,只能调整树中节点指针的指向。这个问题的关键在于利用BST的中序遍历特性。
4.1 递归解法
维护一个全局prev指针指向前一个访问的节点:
cpp复制TreeNode *prev = nullptr, *head = nullptr;
void inorder(TreeNode* node) {
if (!node) return;
inorder(node->left);
if (prev) {
prev->right = node;
node->left = prev;
} else {
head = node; // 记录链表头
}
prev = node;
inorder(node->right);
}
TreeNode* treeToDoublyList(TreeNode* root) {
if (!root) return nullptr;
inorder(root);
// 处理头尾连接
head->left = prev;
prev->right = head;
return head;
}
4.2 迭代解法
使用栈实现中序遍历的非递归版本:
cpp复制TreeNode* treeToDoublyList(TreeNode* root) {
if (!root) return nullptr;
stack<TreeNode*> s;
TreeNode *prev = nullptr, *head = nullptr;
TreeNode* curr = root;
while (curr || !s.empty()) {
while (curr) {
s.push(curr);
curr = curr->left;
}
curr = s.top(); s.pop();
if (prev) {
prev->right = curr;
curr->left = prev;
} else {
head = curr;
}
prev = curr;
curr = curr->right;
}
head->left = prev;
prev->right = head;
return head;
}
调试这类指针操作问题时,建议先画图理清指针关系。我曾因为忽略最后头尾连接步骤导致链表不闭环,花了很长时间排查。
5. 前序与中序序列构建二叉树
LeetCode 105题要求根据前序遍历和中序遍历序列重建二叉树。这个问题的关键在于发现前序序列的第一个元素是根节点,然后在中序序列中找到该元素,左边是左子树,右边是右子树。
5.1 递归实现
cpp复制TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
unordered_map<int, int> inMap;
for (int i = 0; i < inorder.size(); i++)
inMap[inorder[i]] = i;
return helper(preorder, 0, preorder.size()-1,
inorder, 0, inorder.size()-1, inMap);
}
TreeNode* helper(vector<int>& preorder, int preStart, int preEnd,
vector<int>& inorder, int inStart, int inEnd,
unordered_map<int, int>& inMap) {
if (preStart > preEnd || inStart > inEnd) return nullptr;
TreeNode* root = new TreeNode(preorder[preStart]);
int inRoot = inMap[root->val];
int numsLeft = inRoot - inStart;
root->left = helper(preorder, preStart+1, preStart+numsLeft,
inorder, inStart, inRoot-1, inMap);
root->right = helper(preorder, preStart+numsLeft+1, preEnd,
inorder, inRoot+1, inEnd, inMap);
return root;
}
5.2 迭代实现
使用栈来模拟递归过程:
cpp复制TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) return nullptr;
stack<TreeNode*> s;
TreeNode* root = new TreeNode(preorder[0]);
s.push(root);
int inIndex = 0;
for (int i = 1; i < preorder.size(); i++) {
TreeNode* node = s.top();
if (node->val != inorder[inIndex]) {
node->left = new TreeNode(preorder[i]);
s.push(node->left);
} else {
while (!s.empty() && s.top()->val == inorder[inIndex]) {
node = s.top(); s.pop(); inIndex++;
}
node->right = new TreeNode(preorder[i]);
s.push(node->right);
}
}
return root;
}
这类问题的变种很多,比如给出后序和中序构建树。核心思路都是先确定根节点,然后递归处理左右子树。在实际笔试中,要特别注意序列为空或长度不等的边界情况。
6. 二叉树非递归遍历全解
虽然递归遍历简洁明了,但掌握非递归实现对于理解遍历本质和应对面试都至关重要。下面给出三种深度优先遍历的非递归实现。
6.1 前序遍历非递归实现
如前所述,前序遍历的非递归版本使用栈,按照根-左-右的顺序访问节点。关键点是右子节点先入栈:
cpp复制vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
if (root) s.push(root);
while (!s.empty()) {
TreeNode* node = s.top(); s.pop();
res.push_back(node->val);
if (node->right) s.push(node->right);
if (node->left) s.push(node->left);
}
return res;
}
6.2 中序遍历非递归实现
中序遍历需要先处理左子树,因此需要借助指针来跟踪当前访问节点:
cpp复制vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode* curr = root;
while (curr || !s.empty()) {
while (curr) {
s.push(curr);
curr = curr->left;
}
curr = s.top(); s.pop();
res.push_back(curr->val);
curr = curr->right;
}
return res;
}
6.3 后序遍历非递归实现
后序遍历是最复杂的,需要确保左右子树都处理完才能访问根节点。可以使用双栈法或记录前驱节点法:
cpp复制vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode *curr = root, *last = nullptr;
while (curr || !s.empty()) {
if (curr) {
s.push(curr);
curr = curr->left;
} else {
TreeNode* top = s.top();
if (top->right && top->right != last) {
curr = top->right;
} else {
res.push_back(top->val);
last = top;
s.pop();
}
}
}
return res;
}
在调试非递归遍历时,建议先用小规模的树结构手动模拟栈操作,理清节点访问顺序。我在初学阶段经常混淆中序和后序的非递归实现,后来发现关键在于理解何时弹出栈顶元素。
7. 二叉树问题实战技巧总结
经过这些OJ题目的训练,我总结出一些二叉树问题的通用解题技巧:
-
递归三要素:终止条件、本级递归做什么、返回值是什么。写递归前先明确这三点。
-
遍历选择:
- 前序:适合需要先访问父节点再处理子节点的问题
- 中序:BST相关题目几乎都用中序
- 后序:需要先知道子树结果才能处理当前节点的问题
- 层次:需要按层处理或找最短路径时使用
-
空间优化:
- Morris遍历可以在O(1)空间完成遍历
- 某些递归问题可以改为迭代节省栈空间
-
调试技巧:
- 画小规模的树结构示例
- 打印中间结果验证逻辑
- 使用可视化工具观察树结构
-
常见陷阱:
- 忘记处理空指针
- 混淆节点值比较和指针比较
- 递归时修改了不该修改的状态
- 没有及时更新父节点指针
在实际工程中,二叉树结构常用于文件系统、DOM树、路由表等场景。掌握这些算法不仅能通过面试,更能提升解决实际问题的能力。建议在LeetCode上按标签分类练习,从简单题开始逐步提升难度,每道题都尝试多种解法并比较优劣。
