1. 二叉树遍历基础概念
二叉树是一种非常重要的非线性数据结构,在计算机科学中有着广泛的应用。理解二叉树的遍历方式是掌握树结构操作的基础。二叉树的遍历指的是按照某种顺序访问树中的所有节点,确保每个节点都被访问且仅被访问一次。
1.1 为什么需要多种遍历方式
不同的遍历顺序会产生不同的节点访问序列,这些序列在不同场景下各有优势:
- 先序遍历:适合需要优先处理父节点再处理子节点的场景,如复制整个树结构
- 中序遍历:对二叉搜索树会得到有序序列,常用于排序和搜索操作
- 后序遍历:适用于需要先处理子节点再处理父节点的场景,如计算子树特征值
1.2 二叉树节点定义
在C++中,我们通常这样定义二叉树节点:
cpp复制struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
这个简单的结构体包含:
val:存储节点的值left:指向左子节点的指针right:指向右子节点的指针
2. 先序遍历(Preorder Traversal)
2.1 递归实现
递归实现是最直观的方式,完美体现了"分而治之"的思想:
cpp复制void preorderRecursive(TreeNode* root) {
if (root == nullptr) return;
// 访问根节点
cout << root->val << " ";
// 遍历左子树
preorderRecursive(root->left);
// 遍历右子树
preorderRecursive(root->right);
}
注意事项:递归实现虽然简洁,但当树的高度很大时可能导致栈溢出。对于极端不平衡的树(如退化成链表),递归深度可能达到O(n)。
2.2 迭代实现
使用栈来模拟递归调用栈:
cpp复制void preorderIterative(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
cout << node->val << " ";
// 注意:先右后左,因为栈是LIFO
if (node->right) st.push(node->right);
if (node->left) st.push(node->left);
}
}
这种实现的时间复杂度为O(n),空间复杂度在最坏情况下也是O(n)(当树退化成链表时)。
2.3 莫里斯遍历
莫里斯遍历的巧妙之处在于它利用了树中的空指针来存储临时信息,实现了O(1)的额外空间:
cpp复制void preorderMorris(TreeNode* root) {
TreeNode* curr = root;
while (curr != nullptr) {
if (curr->left == nullptr) {
cout << curr->val << " ";
curr = curr->right;
} else {
// 找到前驱节点
TreeNode* predecessor = curr->left;
while (predecessor->right != nullptr &&
predecessor->right != curr) {
predecessor = predecessor->right;
}
if (predecessor->right == nullptr) {
cout << curr->val << " "; // 仅此位置与中序不同
predecessor->right = curr;
curr = curr->left;
} else {
predecessor->right = nullptr;
curr = curr->right;
}
}
}
}
实用技巧:莫里斯遍历虽然节省空间,但会临时修改树的结构,不适合多线程环境或需要保持树结构不变的场景。
3. 中序遍历(Inorder Traversal)
3.1 递归实现
cpp复制void inorderRecursive(TreeNode* root) {
if (root == nullptr) return;
inorderRecursive(root->left);
cout << root->val << " ";
inorderRecursive(root->right);
}
3.2 迭代实现
cpp复制void inorderIterative(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* curr = root;
while (curr != nullptr || !st.empty()) {
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}
curr = st.top();
st.pop();
cout << curr->val << " ";
curr = curr->right;
}
}
3.3 中序遍历的应用
中序遍历二叉搜索树会得到一个有序序列,这个特性有很多实用价值:
cpp复制// 验证二叉搜索树
bool isValidBST(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* curr = root;
TreeNode* prev = nullptr;
while (curr != nullptr || !st.empty()) {
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}
curr = st.top();
st.pop();
if (prev != nullptr && curr->val <= prev->val) {
return false;
}
prev = curr;
curr = curr->right;
}
return true;
}
4. 后序遍历(Postorder Traversal)
4.1 递归实现
cpp复制void postorderRecursive(TreeNode* root) {
if (root == nullptr) return;
postorderRecursive(root->left);
postorderRecursive(root->right);
cout << root->val << " ";
}
4.2 迭代实现
后序遍历的迭代实现较为复杂,这里展示两种常见方法:
方法一:使用两个栈
cpp复制void postorderTwoStacks(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st1, st2;
st1.push(root);
while (!st1.empty()) {
TreeNode* node = st1.top();
st1.pop();
st2.push(node);
if (node->left) st1.push(node->left);
if (node->right) st1.push(node->right);
}
while (!st2.empty()) {
cout << st2.top()->val << " ";
st2.pop();
}
}
方法二:使用一个栈和标记指针
cpp复制void postorderOneStack(TreeNode* root) {
if (root == nullptr) return;
stack<TreeNode*> st;
TreeNode* curr = root;
TreeNode* lastVisited = nullptr;
while (curr != nullptr || !st.empty()) {
if (curr != nullptr) {
st.push(curr);
curr = curr->left;
} else {
TreeNode* peekNode = st.top();
if (peekNode->right != nullptr && lastVisited != peekNode->right) {
curr = peekNode->right;
} else {
cout << peekNode->val << " ";
lastVisited = peekNode;
st.pop();
}
}
}
}
4.3 后序遍历的应用
后序遍历特别适合需要先处理子节点再处理父节点的场景:
cpp复制// 计算二叉树的高度
int treeHeight(TreeNode* root) {
if (root == nullptr) return 0;
int leftHeight = treeHeight(root->left);
int rightHeight = treeHeight(root->right);
return max(leftHeight, rightHeight) + 1;
}
// 释放二叉树内存
void deleteTree(TreeNode* root) {
if (root == nullptr) return;
deleteTree(root->left);
deleteTree(root->right);
delete root;
}
5. 遍历方法的选择与性能比较
5.1 时间复杂度分析
所有遍历方法的时间复杂度都是O(n),因为每个节点都需要访问一次。
5.2 空间复杂度比较
| 方法类型 | 递归 | 迭代(栈) | 莫里斯遍历 |
|---|---|---|---|
| 空间复杂度 | O(h) | O(h)~O(n) | O(1) |
其中h是树的高度,n是节点总数。
5.3 实际应用建议
- 简单场景:优先考虑递归实现,代码简洁易理解
- 大数据量:使用迭代实现避免栈溢出风险
- 空间敏感:考虑莫里斯遍历,但要注意它会修改树结构
- 多线程环境:避免使用莫里斯遍历
6. 常见问题与调试技巧
6.1 遍历顺序混淆
常见错误是混淆三种遍历的节点访问顺序。记忆技巧:
- 先序:根→左→右
- 中序:左→根→右
- 后序:左→右→根
"先"、"中"、"后"指的是根节点的访问时机。
6.2 栈溢出问题
当处理大型树时,递归可能导致栈溢出。解决方法:
- 改用迭代实现
- 增加系统栈大小(不推荐)
- 使用尾递归优化(C++标准不保证会优化)
6.3 空指针检查
所有遍历实现都必须注意空指针检查,特别是在访问left和right指针前。
6.4 内存泄漏
使用new创建的树节点要记得delete。后序遍历特别适合用于树的销毁操作。
7. 扩展应用实例
7.1 根据遍历序列重建二叉树
给定中序和前序/后序遍历序列,可以唯一确定一棵二叉树:
cpp复制TreeNode* buildTree(vector<int>& inorder, vector<int>& preorder) {
if (inorder.empty()) return nullptr;
int rootVal = preorder[0];
TreeNode* root = new TreeNode(rootVal);
auto rootPos = find(inorder.begin(), inorder.end(), rootVal);
int leftSize = rootPos - inorder.begin();
vector<int> leftIn(inorder.begin(), rootPos);
vector<int> rightIn(rootPos + 1, inorder.end());
vector<int> leftPre(preorder.begin() + 1, preorder.begin() + 1 + leftSize);
vector<int> rightPre(preorder.begin() + 1 + leftSize, preorder.end());
root->left = buildTree(leftIn, leftPre);
root->right = buildTree(rightIn, rightPre);
return root;
}
7.2 非递归的树深度计算
使用后序遍历的迭代方法计算树深度:
cpp复制int maxDepthIterative(TreeNode* root) {
if (root == nullptr) return 0;
stack<pair<TreeNode*, int>> st;
st.push({root, 1});
int maxDepth = 0;
while (!st.empty()) {
auto [node, depth] = st.top();
st.pop();
maxDepth = max(maxDepth, depth);
if (node->right) st.push({node->right, depth + 1});
if (node->left) st.push({node->left, depth + 1});
}
return maxDepth;
}
7.3 层序遍历与遍历转换
虽然不属于深度优先遍历,但层序遍历(广度优先)也是常见操作:
cpp复制vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if (root == nullptr) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size();
vector<int> currentLevel;
for (int i = 0; i < levelSize; ++i) {
TreeNode* node = q.front();
q.pop();
currentLevel.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
result.push_back(currentLevel);
}
return result;
}
在实际开发中,理解这些遍历方式的特性和实现细节,能够帮助我们更高效地处理树形结构数据。不同的遍历顺序就像是不同的"视角"来观察同一棵树,掌握它们可以让我们在解决树相关问题时更加得心应手。