作为程序员面试中的常客,二叉树算法题既能考察基础数据结构掌握程度,又能检验递归思维和边界处理能力。今天我将分享两个经典问题的实战解法:翻转二叉树和判断相同二叉树。这两个问题看似简单,但其中蕴含着递归思想的精髓,也是大厂面试中的高频考点。
翻转二叉树的问题要求我们将给定二叉树的每个节点的左右子树进行交换。举个实际例子,假设我们有如下二叉树:
code复制 4
/ \
2 7
/ \ / \
1 3 6 9
翻转后应该变成:
code复制 4
/ \
7 2
/ \ / \
9 6 3 1
从LeetCode给出的示例可以看出,翻转操作需要递归地应用到每个子树。空树(root=[])作为边界情况,直接返回空树即可。
最直观的解法是使用前序遍历(根-左-右)递归地交换每个节点的左右指针。这种方法直接在原树上操作,只交换指针而不改变节点值,避免了值交换可能带来的复杂边界处理。
cpp复制void preorder(TreeNode* root) {
if (root == nullptr) return;
swap(root->left, root->right); // 交换当前节点的左右子树
preorder(root->left); // 递归处理左子树
preorder(root->right); // 递归处理右子树
}
注意:这里使用前序遍历是因为我们需要先处理当前节点,再递归处理其子树。使用中序或后序遍历同样可行,但前序最为直观。
对于不喜欢递归或者担心栈溢出的开发者,可以使用迭代法借助栈来实现:
cpp复制TreeNode* invertTree(TreeNode* root) {
if (!root) return nullptr;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
swap(node->left, node->right);
if (node->left) st.push(node->left);
if (node->right) st.push(node->right);
}
return root;
}
两种方法的时间复杂度都是O(n),需要访问树中的每个节点一次。空间复杂度方面:
实际开发中,建议对树的深度进行预检查,对于特别深的树可以考虑使用迭代法避免递归导致的栈溢出。
判断两棵二叉树是否相同,需要比较它们的结构和节点值。例如:
树1:
code复制 1
/ \
2 3
树2:
code复制 1
/ \
2 3
这两棵树是相同的。
而树1:
code复制 1
/
2
树2:
code复制 1
\
2
这两棵树结构不同,因此不相同。
采用深度优先搜索(DFS)同时遍历两棵树,比较当前节点的情况:
cpp复制bool isSameTree(TreeNode* p, TreeNode* q) {
// 两节点都为空
if (!p && !q) return true;
// 一个为空一个不为空
if (!p || !q) return false;
// 值不相等
if (p->val != q->val) return false;
// 递归比较左右子树
return isSameTree(p->left, q->left) &&
isSameTree(p->right, q->right);
}
同样可以使用迭代法,借助队列进行层序遍历比较:
cpp复制bool isSameTree(TreeNode* p, TreeNode* q) {
queue<TreeNode*> que;
que.push(p);
que.push(q);
while (!que.empty()) {
TreeNode* node1 = que.front(); que.pop();
TreeNode* node2 = que.front(); que.pop();
if (!node1 && !node2) continue;
if (!node1 || !node2) return false;
if (node1->val != node2->val) return false;
que.push(node1->left);
que.push(node2->left);
que.push(node1->right);
que.push(node2->right);
}
return true;
}
在工程实践中,对于浮点数等特殊节点值类型,需要特别注意比较方式,避免精度误差导致误判。
尾递归优化:虽然C++编译器不一定支持尾递归优化,但可以尝试改写递归形式:
cpp复制void invert(TreeNode* root) {
if (!root) return;
swap(root->left, root->right);
invert(root->left);
invert(root->right);
}
并行化处理:对于大规模树,可以考虑并行处理左右子树(需要线程安全的数据结构)
在递归比较时,一旦发现某部分不相同,可以立即返回false,避免不必要的比较:
cpp复制bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p || !q) return p == q;
return p->val == q->val &&
isSameTree(p->left, q->left) &&
isSameTree(p->right, q->right);
}
忘记处理空指针:导致访问空指针异常
cpp复制// 错误示例
void invert(TreeNode* root) {
swap(root->left, root->right); // 可能访问空指针
invert(root->left);
invert(root->right);
}
错误遍历顺序:使用中序遍历时需小心
cpp复制// 容易出错的中序实现
void invert(TreeNode* root) {
if (!root) return;
invert(root->left);
swap(root->left, root->right);
invert(root->left); // 注意这里应该是原来的右子树
}
可视化工具:使用二叉树可视化工具帮助调试
打印日志:在递归过程中打印节点信息
cpp复制bool isSameTree(TreeNode* p, TreeNode* q) {
cout << "Comparing: ";
if (p) cout << p->val << " "; else cout << "null ";
if (q) cout << q->val << endl; else cout << "null" << endl;
// ... rest of the code
}
单元测试:编写全面的测试用例,包括:
对于大规模树,应该进行性能测试:
cpp复制// 生成大规模树用于测试
TreeNode* createLargeTree(int depth) {
if (depth == 0) return nullptr;
TreeNode* root = new TreeNode(depth);
root->left = createLargeTree(depth - 1);
root->right = createLargeTree(depth - 1);
return root;
}
// 测试性能
void testPerformance() {
TreeNode* largeTree = createLargeTree(20);
auto start = chrono::high_resolution_clock::now();
invertTree(largeTree);
auto end = chrono::high_resolution_clock::now();
cout << "Time taken: "
<< chrono::duration_cast<chrono::milliseconds>(end - start).count()
<< " ms" << endl;
}
这两种算法中使用的递归思想可以推广到:
在实际工程中,我经常使用类似的递归思想来处理嵌套的JSON结构、DOM树操作等场景。掌握这种思维方式,能够让你在面对复杂数据结构时游刃有余。