1. 二叉树理论基础与核心概念
二叉树是每个节点最多有两个子节点的树结构,在算法领域应用极为广泛。理解其底层原理对掌握高效算法至关重要。
1.1 二叉树的种类与特性
在实际编码中,我们最常遇到以下几种二叉树类型:
- 满二叉树:每个节点都有0或2个子节点,且所有叶子节点都在同一层
- 完全二叉树:除最后一层外,其余层都达到最大节点数,且最后一层节点靠左排列
- 二叉搜索树(BST):左子树所有节点值小于根节点,右子树所有节点值大于根节点
- 平衡二叉搜索树(AVL):在BST基础上,保证左右子树高度差不超过1
特别提示:C++中的map、set等容器底层采用红黑树(一种自平衡二叉搜索树)实现,因此其插入、删除、查找的时间复杂度稳定在O(log n)。而unordered_map和unordered_set则基于哈希表实现,平均时间复杂度为O(1)。
1.2 二叉树的存储方式
链式存储(最常用)
cpp复制struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
这种存储方式直观清晰,每个节点通过指针连接子节点。优点是插入删除灵活,缺点是空间利用率稍低(需要存储指针)。
顺序存储(数组实现)
对于完全二叉树,可以使用数组存储:
- 下标为i的节点,其左子节点下标为2i+1
- 右子节点下标为2i+2
- 父节点下标为(i-1)/2
这种存储方式节省指针空间,适合存储完全二叉树,但对非完全二叉树会造成空间浪费。
1.3 二叉树的遍历方式
深度优先遍历(DFS)
- 前序遍历:根→左→右
- 中序遍历:左→根→右
- 后序遍历:左→右→根
广度优先遍历(BFS)
- 层序遍历:按层次从上到下,每层从左到右
不同遍历方式的选择取决于具体应用场景。例如前序遍历适合复制树结构,中序遍历适合BST获取有序序列,层序遍历适合计算树的高度等。
2. 递归遍历的实现与原理
递归是解决树问题的天然利器,因其完美匹配树的递归定义。掌握递归三要素是写出正确递归代码的关键。
2.1 递归三要素详解
-
确定递归函数的参数和返回值
- 参数:当前节点指针和存储结果的容器
- 返回值:通常为void,通过引用修改结果容器
-
确定终止条件
- 当前节点为空时返回
- 这是防止无限递归的关键
-
确定单层递归逻辑
- 根据遍历顺序决定访问节点的时机
- 必须保证左右子树的递归调用
2.2 前序遍历实现
cpp复制class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
执行过程分析:
- 访问根节点,将值存入结果
- 递归处理左子树
- 递归处理右子树
- 遇到空节点则返回
2.3 中序遍历实现
cpp复制class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
特点:对于BST,中序遍历可以得到有序序列。
2.4 后序遍历实现
cpp复制class Solution {
public:
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == nullptr) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
应用场景:后序遍历常用于释放树的内存,因为需要先处理子节点再处理父节点。
2.5 递归的时空复杂度分析
- 时间复杂度:O(n),每个节点恰好访问一次
- 空间复杂度:O(h),h为树的高度,递归栈的深度
注意事项:对于极度不平衡的树(如退化为链表),递归可能导致栈溢出。此时应考虑使用迭代法。
3. 迭代遍历的实现技巧
虽然递归简洁,但理解迭代实现能加深对遍历过程的理解,且在某些场景下更高效。
3.1 前序遍历的迭代实现
cpp复制class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->right) st.push(node->right); // 右先入栈
if (node->left) st.push(node->left); // 左后入栈
}
return result;
}
};
关键点:
- 使用栈模拟递归调用
- 右子节点先入栈,保证左子节点先处理
- 每次处理栈顶节点
3.2 中序遍历的迭代实现
cpp复制class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur || !st.empty()) {
if (cur) {
st.push(cur);
cur = cur->left; // 左
} else {
cur = st.top();
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
};
难点:需要额外指针(cur)来跟踪当前节点,处理顺序与访问顺序不一致。
3.3 后序遍历的迭代实现
cpp复制class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
result.push_back(node->val);
if (node->left) st.push(node->left);
if (node->right) st.push(node->right);
}
reverse(result.begin(), result.end());
return result;
}
};
技巧:后序遍历可以看作"中右左"顺序的逆序,因此先按类似前序的方式处理,最后反转结果。
3.4 迭代法的性能考量
- 时间复杂度:同样为O(n)
- 空间复杂度:最坏情况下O(n),但避免了递归的函数调用开销
- 适用场景:树很深时避免栈溢出,或需要精细控制遍历过程时
4. 层序遍历的实战应用
层序遍历(BFS)是解决许多二叉树问题的利器,如计算深度、寻找最大宽度等。
4.1 基本层序遍历实现
cpp复制class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
queue<TreeNode*> que;
if (root) que.push(root);
while (!que.empty()) {
int size = que.size();
vector<int> level;
for (int i = 0; i < size; ++i) {
TreeNode* node = que.front();
que.pop();
level.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(level);
}
return result;
}
};
关键点:
- 使用队列而非栈
- 每次处理一层的所有节点
- 需要记录当前层的节点数(size)
4.2 层序遍历的变种应用
- 锯齿形层序遍历:交替改变每层的遍历方向
- 右视图:只记录每层最后一个节点
- 最小深度:遇到第一个叶子节点时的层数
- 最大宽度:记录每层的节点位置信息
4.3 层序遍历的复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(w),w为树的最大宽度
实际经验:层序遍历的代码模板非常固定,掌握后可以解决LeetCode上十多道相关题目。
5. 二叉树遍历的实战技巧
5.1 递归与迭代的选择策略
- 优先递归:代码简洁,逻辑清晰时
- 考虑迭代:树很深可能栈溢出,或需要精细控制遍历过程时
- 层序遍历:需要按层次处理节点时
5.2 常见错误排查
- 忘记处理空指针:每次访问节点前应检查是否为nullptr
- 递归终止条件错误:可能导致无限递归
- 迭代法中栈/队列操作顺序错误:特别是左右子节点入栈/队列的顺序
- 混淆遍历顺序:前中后序的代码差异仅在于访问节点的时机
5.3 性能优化技巧
- 对于递归实现,考虑尾递归优化(如果编译器支持)
- 迭代法中可以预先分配结果vector的大小以减少扩容开销
- 避免在递归函数中传递大型对象,使用引用或指针
5.4 辅助数据结构选择
- 栈:适合深度优先遍历、保存历史路径
- 队列:适合广度优先遍历、按顺序处理
- 双端队列:实现锯齿形层序遍历等特殊需求
在实际编码中,我习惯先写递归版本验证思路,再根据需要改为迭代实现。对于复杂问题,可以在纸上画出调用栈或队列的状态变化,这能帮助理清思路。记住二叉树问题的核心是正确理解遍历顺序和节点访问时机,多练习几种变体能够显著提升解题能力。