二叉树基础知识及其遍历方法
1. 二叉树简介
二叉树是一种树形数据结构,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。二叉树广泛应用于计算机科学,包括数据存储、搜索算法和表达式解析等场景。以下是一些常见的二叉树类型及其特征:
1.1 完全二叉树
- 定义: 完全二叉树是一种每层都被完全填满的二叉树,除了最后一层,节点在左侧连续。
- 特征:
- 所有节点都被填满。
- 最后一层的节点从左到右填充。
1
/ \
2 3
/ \ /
4 5 6
1.2 满二叉树
- 定义: 满二叉树是一种每个节点都有0或2个子节点的二叉树。
- 特征:
- 每个内部节点都有两个子节点。
- 所有叶子节点位于同一层。
1
/ \
2 3
/ \ / \
4 5 6 7
1.3 二叉搜索树 (BST)
- 定义: 二叉搜索树是一种特殊的二叉树,其中每个节点的值大于其左子树的所有节点的值,并小于其右子树的所有节点的值。
- 特征:
- 支持高效的查找、插入和删除操作(平均O(log n)时间复杂度)。
5
/ \
3 8
/ \ \
2 4 9
1.4 平衡二叉树
- 定义: 平衡二叉树是一种特殊的二叉树,其左右子树的高度差不超过1。
- 特征:
- 保持高度平衡,以保证高效的操作时间。
4
/ \
2 6
/ \ / \
1 3 5 7
2.二叉树遍历分类
2.1 深度优先遍历(DFS)
- 特点:沿树的深度方向优先遍历到叶子节点后再回溯,递归或迭代均可实现。
- 种类:
- 前序遍历(Preorder):根节点 -> 左子树 -> 右子树
- 中序遍历(Inorder):左子树 -> 根节点 -> 右子树
- 后序遍历(Postorder):左子树 -> 右子树 -> 根节点
- 实现方式:
- 递归:最主流,代码简洁。
- 迭代:需要使用栈(模拟递归调用过程)。
2.2 广度优先遍历(BFS)
- 特点:按照树的层次,从上到下逐层遍历,每一层从左到右依次访问节点。
- 实现方式:
- 迭代:使用队列(主流方法)。
- 递归:也可以实现,但通常较复杂且不常用。
2.3 实现代码对比
DFS:前序、中序、后序遍历
- 递归实现:
// 前序遍历
void preorder(TreeNode* root) {
if (!root) return;
cout << root->val << " ";
preorder(root->left);
preorder(root->right);
}
// 中序遍历
void inorder(TreeNode* root) {
if (!root) return;
inorder(root->left);
cout << root->val << " ";
inorder(root->right);
}
// 后序遍历
void postorder(TreeNode* root) {
if (!root) return;
postorder(root->left);
postorder(root->right);
cout << root->val << " ";
}
- 迭代实现:
void preorderIter(TreeNode* root) {
if (!root) return;
stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
cout << node->val << " ";
if (node->right) st.push(node->right); // 右子节点先入栈
if (node->left) st.push(node->left); // 左子节点后入栈
}
}
// 中序遍历
void inorderIter(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* curr = root;
while (curr || !st.empty()) {
while (curr) {
st.push(curr);
curr = curr->left;
}
curr = st.top();
st.pop();
cout << curr->val << " ";
curr = curr->right;
}
}
// 后序遍历
void postorderIter(TreeNode* root) {
if (!root) 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();
}
}
BFS:层序遍历
- 迭代实现(主流):
void levelOrder(TreeNode* root) {
if (!root) return;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
cout << node->val << " ";
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
- 递归实现(不常用):
void levelOrderHelper(TreeNode* node, int level, vector<vector<int>>& res) {
if (!node) return;
if (res.size() == level) res.push_back({});
res[level].push_back(node->val);
levelOrderHelper(node->left, level + 1, res);
levelOrderHelper(node->right, level + 1, res);
}
void levelOrder(TreeNode* root) {
vector<vector<int>> res;
levelOrderHelper(root, 0, res);
for (const auto& level : res) {
for (int val : level) cout << val << " ";
}
}
2.4 总结
- DFS:前序、中序、后序,递归是主流,迭代需要用栈。
- BFS:层序遍历,迭代是主流,使用队列,递归方式较少用。
- 推荐:
- 简单问题:DFS使用递归实现。
- 大规模树或更高效:DFS使用迭代,BFS使用队列迭代。
3. DFS遍历不同递归方法
二叉树的遍历是指按照某种顺序访问树中每个节点的过程。最常用的遍历方法有前序遍历、中序遍历和后序遍历。
3.1 前序遍历
- 顺序: 访问根节点 ➔ 遍历左子树 ➔ 遍历右子树。
- 代码实现:
/**
* Definition for a binary tree node.
*/
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
void traversal(TreeNode* root, vector<int>& result) {
if (root == NULL) { return; }
result.push_back(root->val); // 访问根节点
traversal(root->left, result); // 遍历左子树
traversal(root->right, result); // 遍历右子树
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if(root==nullptr){
return res;
}
res.push_back(root->val);
vector<int> left = preorderTraversal(root->left);
res.insert(res.end(),left.begin(),left.end());
vector<int> right = preorderTraversal(root->right);
res.insert(res.end(),right.begin(),right.end());
return res;
}
};
3.2 中序遍历
- 顺序: 遍历左子树 ➔ 访问根节点 ➔ 遍历右子树。
- 代码实现:
class Solution {
public:
void traversal(TreeNode* root, vector<int>& result) {
if (root == NULL) return;
traversal(root->left, result); // 遍历左子树
result.push_back(root->val); // 访问根节点
traversal(root->right, result); // 遍历右子树
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if(root==nullptr) return res;
vector<int> left = inorderTraversal(root->left);
res.insert(res.end() , left.begin() , left.end());
res.push_back(root->val);
vector<int> right = inorderTraversal(root->right);
res.insert(res.end() , right.begin() , right.end());
return res;
}
};
3.3 后序遍历
- 顺序: 遍历左子树 ➔ 遍历右子树 ➔ 访问根节点。
- 代码实现:
class Solution {
public:
void traversal(TreeNode* root, vector<int>& result) {
if (root == NULL) return;
traversal(root->left, result); // 遍历左子树
traversal(root->right, result); // 遍历右子树
result.push_back(root->val); // 访问根节点
}
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
struct TreeNode{
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if(root==nullptr) return res;
vector<int> left = postorderTraversal(root->left);
res.insert(res.end(), left.begin(), left.end());
vector<int> right = postorderTraversal(root->right);
res.insert(res.end(), right.begin(), right.end());
res.push_back(root->val);
return res;
}
};
3.4前中后序遍历区别
前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点,绝不仅仅是三个顺序不同的 List:
前序位置的代码在刚刚进入一个二叉树节点的时候执行;
后序位置的代码在将要离开一个二叉树节点的时候执行;
中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作。
遇到一道二叉树的题目时的通用思考过程是:
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse
函数配合外部变量来实现。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。
划重点:仔细观察,前中后序位置的代码,能力依次增强。
前序位置的代码只能从函数参数中获取父节点传递来的数据。
中序位置的代码不仅可以获取参数数据,还可以获取到左子树通过函数返回值传递回来的数据。
后序位置的代码最强,不仅可以获取参数数据,还可以同时获取到左右子树通过函数返回值传递回来的数据。
所以,某些情况下把代码移到后序位置效率最高;有些事情,只有后序位置的代码能做。
4. 结论
二叉树是计算机科学中的基本数据结构之一,理解其各种形式和遍历方法是学习数据结构和算法的基础。通过学习和实现不同类型的树以及它们的遍历方式,能够为进一步深入研究树形结构和相关算法打下坚实的基础。希望这篇文章能够帮助你更好地理解二叉树及其遍历方法。