ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

二叉树算法实战:从递归到迭代的C++实现

二叉树算法实战:从递归到迭代的C++实现 1. 二叉树基础与字符串表示在C中处理二叉树问题时最基础也最容易被忽视的就是如何正确表示树结构。我们先来看一个看似简单但暗藏玄机的问题根据二叉树创建字符串。1.1 问题描述与示例分析LeetCode 606题要求我们将二叉树按照特定规则转换为字符串表示。规则如下空节点用空字符串表示非空节点用其值表示对于每个非空节点如果只有右孩子左孩子的空括号不能省略如果只有左孩子可以省略右孩子的空括号举个例子1 / \ 2 3 / 4应该表示为1(2(4))(3)而不是1(2(4)())(3())。1.2 递归解法实现这个问题的经典解法是递归遍历但有几个关键细节需要注意class Solution { public: string tree2str(TreeNode* root) { if (!root) return ; string res to_string(root-val); // 关键判断1左子树为空但右子树不空时 if (!root-left root-right) { res (); } // 关键判断2左子树不空时 if (root-left) { res ( tree2str(root-left) ); } // 关键判断3右子树不空时 if (root-right) { res ( tree2str(root-right) ); } return res; } };注意to_string()函数在转换节点值时可能会成为性能瓶颈对于高频调用场景建议预先分配缓冲区。1.3 迭代解法优化递归解法虽然直观但在处理大型树时可能引发栈溢出。我们可以用栈模拟递归过程string tree2str_iterative(TreeNode* root) { if (!root) return ; stackTreeNode* st; st.push(root); unordered_setTreeNode* visited; string res; while (!st.empty()) { TreeNode* node st.top(); if (visited.count(node)) { st.pop(); res ); } else { visited.insert(node); res ( to_string(node-val); // 处理右左子树的顺序 if (!node-left node-right) res (); if (node-right) st.push(node-right); if (node-left) st.push(node-left); } } return res.substr(1, res.size()-2); }这种解法虽然代码量增加但避免了递归深度限制适合生产环境使用。2. 最近公共祖先问题2.1 LCA问题定义最近公共祖先(Lowest Common Ancestor)是二叉树中的经典问题。给定两个节点p和q找到它们在树中最低的公共祖先节点。2.2 递归解法分析最直观的解法是通过递归搜索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; }这个解法的时间复杂度是O(n)空间复杂度最坏情况下也是O(n)。2.3 非递归解法实现我们可以使用父指针记录法来优化TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { unordered_mapTreeNode*, TreeNode* parent; stackTreeNode* st; parent[root] nullptr; st.push(root); // 构建父指针映射 while (!parent.count(p) || !parent.count(q)) { TreeNode* node st.top(); st.pop(); if (node-left) { parent[node-left] node; st.push(node-left); } if (node-right) { parent[node-right] node; st.push(node-right); } } // 收集p的祖先路径 setTreeNode* ancestors; while (p) { ancestors.insert(p); p parent[p]; } // 查找q的祖先中第一个出现在p路径中的节点 while (!ancestors.count(q)) { q parent[q]; } return q; }这种方法虽然空间复杂度略高但在多次查询时可以复用父指针映射适合查询密集型场景。3. 二叉搜索树与双向链表3.1 问题转换思路将二叉搜索树转换为排序的双向链表要求不能创建新节点只能调整指针指向。这是一个典型的树与链表转换问题。3.2 中序遍历解法利用BST的中序遍历特性我们可以得到有序序列class Solution { TreeNode* prev nullptr; TreeNode* head nullptr; public: TreeNode* treeToDoublyList(TreeNode* root) { if (!root) return nullptr; inorder(root); // 连接首尾形成循环 head-left prev; prev-right head; return head; } void inorder(TreeNode* node) { if (!node) return; inorder(node-left); if (!prev) { head node; // 记录链表头 } else { prev-right node; node-left prev; } prev node; inorder(node-right); } };注意在面试中常被问及非递归实现建议同时掌握迭代版本。3.3 迭代实现版本TreeNode* treeToDoublyList_iterative(TreeNode* root) { if (!root) return nullptr; stackTreeNode* st; TreeNode *head nullptr, *prev nullptr; TreeNode* curr root; while (curr || !st.empty()) { while (curr) { st.push(curr); curr curr-left; } curr st.top(); st.pop(); if (!prev) { head curr; } else { prev-right curr; curr-left prev; } prev curr; curr curr-right; } // 连接首尾 head-left prev; prev-right head; return head; }4. 前序与中序构建二叉树4.1 重建二叉树原理给定前序和中序遍历序列可以唯一确定一棵二叉树。前序的第一个元素是根节点中序中该元素左边是左子树右边是右子树。4.2 递归实现TreeNode* buildTree(vectorint preorder, vectorint inorder) { unordered_mapint, int inMap; for (int i 0; i inorder.size(); i) { inMap[inorder[i]] i; } return build(preorder, 0, preorder.size()-1, inorder, 0, inorder.size()-1, inMap); } TreeNode* build(vectorint preorder, int preStart, int preEnd, vectorint inorder, int inStart, int inEnd, unordered_mapint, 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 build(preorder, preStart1, preStartnumsLeft, inorder, inStart, inRoot-1, inMap); root-right build(preorder, preStartnumsLeft1, preEnd, inorder, inRoot1, inEnd, inMap); return root; }4.3 迭代实现优化递归解法在极端情况下可能导致栈溢出我们可以用栈来模拟递归过程TreeNode* buildTree_iterative(vectorint preorder, vectorint inorder) { if (preorder.empty()) return nullptr; stackTreeNode* st; TreeNode* root new TreeNode(preorder[0]); st.push(root); int inIndex 0; for (int i 1; i preorder.size(); i) { TreeNode* node st.top(); if (node-val ! inorder[inIndex]) { node-left new TreeNode(preorder[i]); st.push(node-left); } else { while (!st.empty() st.top()-val inorder[inIndex]) { node st.top(); st.pop(); inIndex; } node-right new TreeNode(preorder[i]); st.push(node-right); } } return root; }5. 二叉树的非递归遍历5.1 前序遍历的非递归实现vectorint preorderTraversal(TreeNode* root) { vectorint res; stackTreeNode* st; if (root) st.push(root); while (!st.empty()) { TreeNode* node st.top(); st.pop(); res.push_back(node-val); // 注意右子树先入栈 if (node-right) st.push(node-right); if (node-left) st.push(node-left); } return res; }5.2 中序遍历的非递归实现vectorint inorderTraversal(TreeNode* root) { vectorint res; stackTreeNode* st; TreeNode* curr root; while (curr || !st.empty()) { while (curr) { st.push(curr); curr curr-left; } curr st.top(); st.pop(); res.push_back(curr-val); curr curr-right; } return res; }5.3 后序遍历的非递归实现后序遍历是最复杂的需要记录访问状态vectorint postorderTraversal(TreeNode* root) { vectorint res; stackTreeNode* st; TreeNode* lastVisited nullptr; TreeNode* curr root; while (curr || !st.empty()) { if (curr) { st.push(curr); curr curr-left; } else { TreeNode* peek st.top(); if (peek-right peek-right ! lastVisited) { curr peek-right; } else { res.push_back(peek-val); lastVisited peek; st.pop(); } } } return res; }5.4 统一迭代法模板为了统一三种遍历方式可以使用标记法// 前序遍历 vectorint preorderTraversal_unified(TreeNode* root) { vectorint res; stackTreeNode* st; if (root) st.push(root); while (!st.empty()) { TreeNode* node st.top(); st.pop(); if (node) { if (node-right) st.push(node-right); // 右 if (node-left) st.push(node-left); // 左 st.push(node); // 中 st.push(nullptr); // 标记 } else { node st.top(); st.pop(); res.push_back(node-val); } } return res; }只需调整入栈顺序即可实现三种遍历的统一模板。6. 二叉树问题实战技巧6.1 调试与可视化在处理复杂二叉树问题时可视化工具能极大提升效率。可以自定义打印函数void printTree(TreeNode* root, int space 0, int height 10) { if (!root) return; space height; printTree(root-right, space); cout endl; for (int i height; i space; i) cout ; cout root-val \n; printTree(root-left, space); }6.2 内存管理注意事项在面试或竞赛中经常需要手动管理二叉树内存void deleteTree(TreeNode* root) { if (!root) return; deleteTree(root-left); deleteTree(root-right); delete root; }6.3 常见错误排查空指针异常总是检查节点是否为null无限递归确保递归有终止条件错误的状态维护在非递归遍历中正确维护栈状态指针修改错误在链表转换问题中注意指针修改顺序6.4 性能优化建议对于高频调用的辅助函数考虑使用静态变量缓存结果在递归解法中尽可能使用尾递归优化对于大型树优先考虑迭代解法避免栈溢出使用哈希表存储中间结果减少重复计算7. 二叉树扩展问题7.1 序列化与反序列化// 序列化为字符串 string serialize(TreeNode* root) { if (!root) return #; return to_string(root-val) , serialize(root-left) , serialize(root-right); } // 从字符串反序列化 TreeNode* deserialize(string data) { queuestring q; string s; for (char c : data) { if (c ,) { q.push(s); s ; } else { s c; } } if (!s.empty()) q.push(s); return helper(q); } TreeNode* helper(queuestring q) { string s q.front(); q.pop(); if (s #) return nullptr; TreeNode* root new TreeNode(stoi(s)); root-left helper(q); root-right helper(q); return root; }7.2 验证二叉搜索树bool isValidBST(TreeNode* root) { stackTreeNode* st; TreeNode* prev nullptr; TreeNode* curr root; while (curr || !st.empty()) { while (curr) { st.push(curr); curr curr-left; } curr st.top(); st.pop(); if (prev prev-val curr-val) return false; prev curr; curr curr-right; } return true; }7.3 二叉树的最大路径和int maxPathSum(TreeNode* root) { int maxSum INT_MIN; helper(root, maxSum); return maxSum; } int helper(TreeNode* node, int maxSum) { if (!node) return 0; int left max(helper(node-left, maxSum), 0); int right max(helper(node-right, maxSum), 0); maxSum max(maxSum, left right node-val); return max(left, right) node-val; }在实际工程中二叉树问题的解决往往需要结合具体业务场景进行调整。建议在掌握这些经典算法的基础上多思考如何将它们应用到实际问题中。例如文件系统的目录结构、组织架构图、决策树等都可以用二叉树模型来表示和处理。
返回列表