二叉树详解:递归的艺术
大部分二叉树问题都可以分解为"处理当前节点 + 递归处理子树"
二叉树是我刷题过程中遇到最多的数据结构之一。刚开始觉得递归很绕,后来发现只要抓住"根-左-右"的思路,大部分问题都能迎刃而解。如果你对递归还不太熟悉,建议先画图,把每一步的调用栈写出来,会清晰很多。
什么是二叉树
二叉树是一种树形结构,每个节点最多有两个子节点,分别称为左子节点 和右子节点 。
1 2 3 4 5 1 ← 根节点 / \ 2 3 ← 左子节点、右子节点 / \ \ 4 5 6 ← 叶子节点(没有子节点)
节点定义
1 2 3 4 5 6 struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode (int x) : val (x), left (nullptr ), right (nullptr ) {} };
几个概念
术语
含义
根节点
树的最顶端节点
叶子节点
没有子节点的节点
深度
从根到该节点的边数
高度
从该节点到最远叶子的边数
层
深度相同的节点在同一层(从1开始)
二叉树的分类
graph TD
A[二叉树] --> B[满二叉树]
A --> C[完全二叉树]
A --> D[二叉搜索树 BST]
A --> E[平衡二叉树 AVL]
style A fill:#FFE4B5
style D fill:#90EE90
style E fill:#87CEEB
满二叉树
每一层都是满的,节点数 = 2^h - 1(h是层数)
1 2 3 4 5 1 / \ 2 3 / \ / \ 4 5 6 7
完全二叉树
除了最后一层,其他层都是满的,且最后一层节点靠左排列。堆就是完全二叉树。
二叉搜索树(BST)
左子树所有节点 < 根 < 右子树所有节点。中序遍历是有序的。
1 2 3 4 5 6 7 5 / \ 3 7 / \ / \ 2 4 6 8 中序遍历:2 3 4 5 6 7 8(有序!)
平衡二叉树
任意节点的左右子树高度差不超过1。
四种遍历方式
二叉树的遍历是基础中的基础,必须熟练掌握。
前序遍历(根-左-右)
1 2 3 4 5 6 void preorder (TreeNode* root) { if (!root) return ; cout << root->val << " " ; preorder (root->left); preorder (root->right); }
中序遍历(左-根-右)
1 2 3 4 5 6 void inorder (TreeNode* root) { if (!root) return ; inorder (root->left); cout << root->val << " " ; inorder (root->right); }
后序遍历(左-右-根)
1 2 3 4 5 6 void postorder (TreeNode* root) { if (!root) return ; postorder (root->left); postorder (root->right); cout << root->val << " " ; }
层序遍历(BFS)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 vector<vector<int >> levelOrder (TreeNode* root) { vector<vector<int >> result; if (!root) return result; queue<TreeNode*> q; q.push (root); while (!q.empty ()) { int size = q.size (); vector<int > level; for (int i = 0 ; i < size; ++i) { TreeNode* node = q.front (); q.pop (); level.push_back (node->val); if (node->left) q.push (node->left); if (node->right) q.push (node->right); } result.push_back (level); } return result; }
遍历过程示例
1 2 3 4 5 6 7 8 9 10 1 / \ 2 3 / \ 4 5 前序:1 2 4 5 3 (根-左-右) 中序:4 2 5 1 3 (左-根-右) 后序:4 5 2 3 1 (左-右-根) 层序:1 | 2 3 | 4 5
经典问题:最大深度
LeetCode 104 : 给定一个二叉树,找出其最大深度。
递归解法
思路:树的最大深度 = max(左子树深度, 右子树深度) + 1
1 2 3 4 5 6 7 8 int maxDepth (TreeNode* root) { if (!root) return 0 ; int leftDepth = maxDepth (root->left); int rightDepth = maxDepth (root->right); return max (leftDepth, rightDepth) + 1 ; }
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 3 / \ 9 20 / \ 15 7 调用 maxDepth(3) ├── 调用 maxDepth(9) │ ├── maxDepth(null) = 0 │ └── maxDepth(null) = 0 │ └── 返回 max(0,0)+1 = 1 ├── 调用 maxDepth(20) │ ├── maxDepth(15) = 1 │ └── maxDepth(7) = 1 │ └── 返回 max(1,1)+1 = 2 └── 返回 max(1,2)+1 = 3 答案:3
时间复杂度 :O(n),每个节点访问一次
空间复杂度 :O(h),h是树的高度(递归栈)
经典问题:判断对称二叉树
LeetCode 101 : 给定一个二叉树,检查它是否是镜像对称的。
思路
对称的条件:
根节点的值相等
左子树的左子树 = 右子树的右子树
左子树的右子树 = 右子树的左子树
1 2 3 4 5 1 / \ 2 2 ← 对称 / \ / \ 3 4 4 3 ← 对称
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool isSymmetric (TreeNode* root) { if (!root) return true ; return isMirror (root->left, root->right); } bool isMirror (TreeNode* left, TreeNode* right) { if (!left && !right) return true ; if (!left || !right) return false ; return left->val == right->val && isMirror (left->left, right->right) && isMirror (left->right, right->left); }
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 / \ 2 2 / \ / \ 3 4 4 3 isMirror(2, 2)? ├── 2->val == 2->val ✓ ├── isMirror(3, 3)? │ ├── 3->val == 3->val ✓ │ ├── isMirror(null, null) = true │ └── isMirror(null, null) = true │ └── 返回 true ├── isMirror(4, 4)? │ └── 返回 true └── 返回 true 答案:true
经典问题:二叉树的最近公共祖先
LeetCode 236 : 给定一个二叉树,找到两个节点的最近公共祖先(LCA)。
思路
递归查找 p 和 q:
如果当前节点是 p 或 q,返回当前节点
递归查找左右子树
如果 p 和 q 分别在左右子树中,当前节点就是 LCA
如果都在同一侧,返回那一侧的结果
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; }
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 3 / \ 5 1 / \ / \ 6 2 0 8 / \ 7 4 查找 p=5, q=4 的 LCA LCA(3, 5, 4) ├── LCA(5, 5, 4) │ └── root == p,返回 5 ├── LCA(1, 5, 4) │ ├── LCA(0, 5, 4) = null │ └── LCA(8, 5, 4) = null │ └── 返回 null └── left=5, right=null,返回 5 答案:节点5
经典问题:二叉树的序列化与反序列化
LeetCode 297 : 设计一个算法,将二叉树序列化为字符串,并能反序列化。
前序遍历方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Codec {public : string serialize (TreeNode* root) { if (!root) return "#" ; return to_string (root->val) + "," + serialize (root->left) + "," + serialize (root->right); } TreeNode* deserialize (string data) { queue<string> nodes; string token; for (char c : data) { if (c == ',' ) { nodes.push (token); token.clear (); } else { token += c; } } nodes.push (token); return buildTree (nodes); } private : TreeNode* buildTree (queue<string>& nodes) { string val = nodes.front (); nodes.pop (); if (val == "#" ) return nullptr ; TreeNode* node = new TreeNode (stoi (val)); node->left = buildTree (nodes); node->right = buildTree (nodes); return node; } };
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1 / \ 2 3 / \ 4 5 序列化:1,2,#,#,3,4,#,#,5,#,# 反序列化过程: 1 → 创建节点1 2 → 创建节点2,作为1的左子节点 # → 2的左子节点为空 # → 2的右子节点为空 3 → 创建节点3,作为1的右子节点 4 → 创建节点4,作为3的左子节点 ...
经典问题:从遍历结果构造二叉树
LeetCode 105 : 根据前序遍历和中序遍历构造二叉树。
思路
1 2 前序:[3, 9, 20, 15, 7] → 根-左-右,第一个是根 中序:[9, 3, 15, 20, 7] → 左-根-右,根的左边是左子树
前序的第一个元素是根
在中序中找到根的位置,左边是左子树,右边是右子树
递归构造
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 TreeNode* buildTree (vector<int >& preorder, vector<int >& inorder) { unordered_map<int , int > inorderIndex; for (int i = 0 ; i < inorder.size (); ++i) { inorderIndex[inorder[i]] = i; } return build (preorder, 0 , preorder.size () - 1 , inorder, 0 , inorder.size () - 1 , inorderIndex); } TreeNode* build (vector<int >& preorder, int preStart, int preEnd, vector<int >& inorder, int inStart, int inEnd, unordered_map<int , int >& inorderIndex) { if (preStart > preEnd) return nullptr ; int rootVal = preorder[preStart]; TreeNode* root = new TreeNode (rootVal); int rootIndex = inorderIndex[rootVal]; int leftSize = rootIndex - inStart; root->left = build (preorder, preStart + 1 , preStart + leftSize, inorder, inStart, rootIndex - 1 , inorderIndex); root->right = build (preorder, preStart + leftSize + 1 , preEnd, inorder, rootIndex + 1 , inEnd, inorderIndex); return root; }
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 前序:[3, 9, 20, 15, 7] 中序:[9, 3, 15, 20, 7] Step 1: 根 = 3 中序中 3 的位置 = 1 左子树:前序[9], 中序[9] 右子树:前序[20,15,7], 中序[15,20,7] Step 2: 构造左子树,根 = 9 Step 3: 构造右子树,根 = 20 ... 结果: 3 / \ 9 20 / \ 15 7
经典问题:验证二叉搜索树
LeetCode 98 : 判断一棵树是否是有效的二叉搜索树。
思路
利用 BST 的性质:中序遍历是严格递增的。
或者用递归:每个节点的值必须在一个合法范围内。
方法1:中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool isValidBST (TreeNode* root) { long long prev = LLONG_MIN; return inorderCheck (root, prev); } bool inorderCheck (TreeNode* node, long long & prev) { if (!node) return true ; if (!inorderCheck (node->left, prev)) return false ; if (node->val <= prev) return false ; prev = node->val; return inorderCheck (node->right, prev); }
方法2:递归范围检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool isValidBST (TreeNode* root) { return validate (root, LLONG_MIN, LLONG_MAX); } bool validate (TreeNode* node, long long minVal, long long maxVal) { if (!node) return true ; if (node->val <= minVal || node->val >= maxVal) { return false ; } return validate (node->left, minVal, node->val) && validate (node->right, node->val, maxVal); }
执行过程
1 2 3 4 5 6 7 8 9 10 11 12 13 5 / \ 1 4 / \ 3 6 validate(5, -∞, +∞) ├── validate(1, -∞, 5) = true ├── validate(4, 5, +∞) │ └── 4 不在 (5, +∞) 范围内! │ └── 返回 false 答案:false(不是有效BST)
迭代遍历(非递归)
有时候面试官会要求用迭代方式实现遍历。
前序遍历(迭代)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 vector<int > preorderTraversal (TreeNode* root) { vector<int > result; if (!root) return result; stack<TreeNode*> stk; stk.push (root); while (!stk.empty ()) { TreeNode* node = stk.top (); stk.pop (); result.push_back (node->val); if (node->right) stk.push (node->right); if (node->left) stk.push (node->left); } return result; }
中序遍历(迭代)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vector<int > inorderTraversal (TreeNode* root) { vector<int > result; stack<TreeNode*> stk; TreeNode* curr = root; while (curr || !stk.empty ()) { while (curr) { stk.push (curr); curr = curr->left; } curr = stk.top (); stk.pop (); result.push_back (curr->val); curr = curr->right; } return result; }
常见技巧总结
1. 递归三要素
终止条件 :if (!root) return ...
递归操作 :处理当前节点
递归调用 :处理左右子树
2. 遍历选择
场景
推荐遍历
求深度/高度
后序(先知道子树信息)
BST 相关
中序(有序性)
路径问题
前序(从上到下)
层级相关
层序(BFS)
序列化/反序列化
前序 + 标记空节点
3. 常见陷阱
1 2 3 4 5 6 7 8 9 10 11 int maxDepth (TreeNode* root) { return max (maxDepth (root->left), maxDepth (root->right)) + 1 ; } int maxDepth (TreeNode* root) { if (!root) return 0 ; return max (maxDepth (root->left), maxDepth (root->right)) + 1 ; }
思维导图
graph TD
A[二叉树] --> B[基本概念]
A --> C[遍历方式]
A --> D[经典问题]
B --> B1[节点定义]
B --> B2[满/完全/BST]
C --> C1[前序 根-左-右]
C --> C2[中序 左-根-右]
C --> C3[后序 左-右-根]
C --> C4[层序 BFS]
D --> D1[最大深度]
D --> D2[对称二叉树]
D --> D3[最近公共祖先]
D --> D4[验证BST]
D --> D5[序列化]
style A fill:#FFE4B5
style C fill:#90EE90
style D fill:#87CEEB
总结
核心要点
递归是处理二叉树的核心 :大部分问题都可以分解为"处理当前节点 + 递归处理子树"
掌握四种遍历 :前序、中序、后序、层序,各有应用场景
BST 的中序遍历是有序的 :很多 BST 问题可以利用这个性质
空节点要先判断 :递归的第一步往往是 if (!root) return ...
LeetCode 推荐题单
入门 :
104 最大深度
101 对称二叉树
226 翻转二叉树
进阶 :
102 层序遍历
236 最近公共祖先
105 前序+中序构造二叉树
挑战 :
124 二叉树最大路径和
297 序列化与反序列化
99 恢复BST
推荐阅读 :