在 LeetCode、算法竞赛以及实际工程开发中,二叉树(Binary Tree)是最核心的数据结构之一。
很多初学者在刷题时,往往只会"调用"二叉树,却不真正理解:
-
TreeNode 为什么这样设计?
-
buildTree 是如何构造树的?
-
为什么需要 deleteTree?
-
如何优雅地打印一棵树?
本文将通过一个完整的 C++ 示例,系统讲解二叉树最核心的几个基础模块。
一、什么是二叉树(Binary Tree)
二叉树是一种:
每个节点最多只有两个子节点的数据结构。
通常:
-
左孩子:left
-
右孩子:right
例如:
3
/ \
9 20
/ \
15 7
这就是一棵典型二叉树。
二、TreeNode 节点结构
代码:
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) {}
};
三、TreeNode 的核心组成
一个二叉树节点通常包含三部分:
| 成员 | 含义 |
|---|---|
| val | 当前节点值 |
| left | 指向左子树 |
| right | 指向右子树 |
即:
TreeNode
├── val
├── left
└── right
四、为什么 left 和 right 是指针?
因为二叉树本质上是:
"节点之间的连接关系"
例如:
1
/ \
2 3
节点 1 需要"指向"节点 2 和节点 3。
因此:
TreeNode* left;
TreeNode* right;
用于保存子节点地址。
五、构造函数详解
1. 默认构造
TreeNode() : val(0), left(nullptr), right(nullptr) {}
生成:
值为0
左右孩子为空
2. 单参数构造
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
例如:
TreeNode* node = new TreeNode(5);
结果:
节点值 = 5
left = nullptr
right = nullptr
3. 完整构造
TreeNode(int x, TreeNode *left, TreeNode *right)
允许直接创建:
根节点 + 左子树 + 右子树
例如:
TreeNode* root =
new TreeNode(1,
new TreeNode(2),
new TreeNode(3));
得到:
1
/ \
2 3
六、buildTree:如何构建二叉树
代码:
TreeNode* buildTree(vector<int> nodes)
作用:
根据层序数组构建二叉树。
例如:
[3,9,20,null,null,15,7]
对应:
3
/ \
9 20
/ \
15 7
七、为什么使用队列 queue?
因为构建过程本质是:
层序遍历(Level Order Traversal)
需要:
先处理父节点
再处理子节点
这是典型 BFS(广度优先搜索)思想。
因此:
queue<TreeNode*> q;
用于保存"等待处理"的节点。
八、buildTree 核心流程
Step 1:创建根节点
TreeNode* root = new TreeNode(nodes[0]);
Step 2:根节点入队
q.push(root);
Step 3:循环处理队列
while (!q.empty())
每次取出一个父节点:
TreeNode* curr = q.front();
q.pop();
Step 4:构建左孩子
curr->left = new TreeNode(nodes[i]);
Step 5:构建右孩子
curr->right = new TreeNode(nodes[i]);
九、为什么用 -101 表示 null?
因为:
vector<int>
不能直接存储:
null
所以使用特殊值:
-101
表示空节点。
例如:
[1, -101, 2]
表示:
1
\
2
十、deleteTree:为什么必须释放内存?
代码:
void deleteTree(TreeNode* root)
作用:
递归释放整棵树。
因为:
new TreeNode(...)
是在堆区申请内存。
如果不释放:
会造成内存泄漏(Memory Leak)
十一、deleteTree 为什么使用后序遍历?
代码:
deleteTree(root->left);
deleteTree(root->right);
delete root;
顺序:
左 -> 右 -> 根
原因:
必须先删除子节点,再删除父节点。
否则:
父节点被释放后
无法继续访问孩子节点
十二、printTree:打印二叉树
很多时候调试二叉树非常困难。
因此我们通常会实现:
printTree(root)
这里给出一个经典层序打印。
printTree 实现
void printTree(TreeNode* root) {
if (root == nullptr) {
cout << "Empty Tree" << endl;
return;
}
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; i++) {
TreeNode* curr = q.front();
q.pop();
if (curr == nullptr) {
cout << "null ";
continue;
}
cout << curr->val << " ";
q.push(curr->left);
q.push(curr->right);
}
cout << endl;
}
}
十三、printTree 输出效果
例如:
vector<int> nodes = {3,9,20,-101,-101,15,7};
打印:
3
9 20
null null 15 7
树结构一目了然。
十四、maxDepth:求二叉树最大深度
代码:
int maxDepth(TreeNode* root)
核心思想:
当前树高度 =
max(左子树高度, 右子树高度) + 1
即:
return max(leftDepth, rightDepth) + 1;
十五、递归思想分析
例如:
3
/ \
9 20
/ \
15 7
递归会不断向下:
3
├── 9
└── 20
├── 15
└── 7
直到:
root == nullptr
返回:
0
然后逐层回溯计算高度。
十六、时间复杂度分析
buildTree
每个节点访问一次:
O(n)
deleteTree
每个节点删除一次:
O(n)
maxDepth
每个节点遍历一次:
O(n)
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
#include <algorithm>
using namespace std;
/*
=========================================================
TreeNode Definition
=========================================================
*/
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) {}
};
/*
=========================================================
Build Binary Tree
Level Order Construction
=========================================================
*/
TreeNode* buildTree(const vector<int>& nodes, int nullVal = -101)
{
if (nodes.empty() || nodes[0] == nullVal) {
return nullptr;
}
TreeNode* root = new TreeNode(nodes[0]);
queue<TreeNode*> q;
q.push(root);
int i = 1;
while (!q.empty() && i < nodes.size())
{
TreeNode* curr = q.front();
q.pop();
// left child
if (i < nodes.size() && nodes[i] != nullVal)
{
curr->left = new TreeNode(nodes[i]);
q.push(curr->left);
}
i++;
// right child
if (i < nodes.size() && nodes[i] != nullVal)
{
curr->right = new TreeNode(nodes[i]);
q.push(curr->right);
}
i++;
}
return root;
}
/*
=========================================================
Delete Binary Tree
Postorder Traversal
=========================================================
*/
void deleteTree(TreeNode* root)
{
if (root == nullptr) {
return;
}
deleteTree(root->left);
deleteTree(root->right);
delete root;
}
/*
=========================================================
Print Binary Tree
Level Order Traversal
=========================================================
*/
void printTree(TreeNode* root)
{
if (root == nullptr)
{
cout << "Empty Tree" << endl;
return;
}
queue<TreeNode*> q;
q.push(root);
while (!q.empty())
{
int size = q.size();
for (int i = 0; i < size; i++)
{
TreeNode* curr = q.front();
q.pop();
if (curr == nullptr)
{
cout << "null ";
continue;
}
cout << curr->val << " ";
q.push(curr->left);
q.push(curr->right);
}
cout << endl;
}
}
/*
=========================================================
DFS Traversal
=========================================================
*/
// preorder
void preorder(TreeNode* root)
{
if (root == nullptr) {
return;
}
cout << root->val << " ";
preorder(root->left);
preorder(root->right);
}
// inorder
void inorder(TreeNode* root)
{
if (root == nullptr) {
return;
}
inorder(root->left);
cout << root->val << " ";
inorder(root->right);
}
// postorder
void postorder(TreeNode* root)
{
if (root == nullptr) {
return;
}
postorder(root->left);
postorder(root->right);
cout << root->val << " ";
}
/*
=========================================================
BFS Traversal
=========================================================
*/
void levelOrder(TreeNode* root)
{
if (root == nullptr) {
return;
}
queue<TreeNode*> q;
q.push(root);
while (!q.empty())
{
TreeNode* curr = q.front();
q.pop();
cout << curr->val << " ";
if (curr->left)
q.push(curr->left);
if (curr->right)
q.push(curr->right);
}
}
/*
=========================================================
Tree Height
=========================================================
*/
int maxDepth(TreeNode* root)
{
if (root == nullptr) {
return 0;
}
return max(
maxDepth(root->left),
maxDepth(root->right)
) + 1;
}
/*
=========================================================
Count Nodes
=========================================================
*/
int countNodes(TreeNode* root)
{
if (root == nullptr) {
return 0;
}
return countNodes(root->left)
+ countNodes(root->right)
+ 1;
}
/*
=========================================================
Search Value
=========================================================
*/
bool search(TreeNode* root, int target)
{
if (root == nullptr) {
return false;
}
if (root->val == target) {
return true;
}
return search(root->left, target)
|| search(root->right, target);
}
/*
=========================================================
Example Main
=========================================================
*/
int main()
{
vector<int> nodes =
{
3,
9,
20,
-101,
-101,
15,
7
};
TreeNode* root = buildTree(nodes);
cout << "Print Tree:" << endl;
printTree(root);
cout << endl;
cout << "Preorder: ";
preorder(root);
cout << endl;
cout << "Inorder: ";
inorder(root);
cout << endl;
cout << "Postorder: ";
postorder(root);
cout << endl;
cout << "LevelOrder: ";
levelOrder(root);
cout << endl;
cout << "MaxDepth: ";
cout << maxDepth(root) << endl;
cout << "Node Count: ";
cout << countNodes(root) << endl;
cout << "Search 15: ";
cout << search(root, 15) << endl;
deleteTree(root);
return 0;
}
十七、二叉树中的经典思想
这份代码实际上已经涵盖了:
| 思想 | 体现 |
|---|---|
| BFS | buildTree |
| DFS | maxDepth |
| 后序遍历 | deleteTree |
| 递归 | maxDepth/deleteTree |
| 队列 | buildTree/printTree |
| 指针 | TreeNode |
这也是 LeetCode 二叉树题目的核心基础。
十八、总结
本文系统讲解了:
-
TreeNode 节点设计
-
buildTree 层序建树
-
deleteTree 内存释放
-
printTree 层序打印
-
maxDepth 最大深度
理解这些内容后,你已经具备:
独立完成绝大多数二叉树基础题目的能力。
二叉树真正困难的部分并不是"写代码",而是:
-
理解递归
-
理解树结构
-
理解 DFS 与 BFS
当你真正理解:
树 = 递归结构
很多复杂题目都会变得清晰。