④ 数据结构与算法 — 第 4 课
next 把节点串成一条线。left 和 right。2i、右 2i+1。 1 ← 根 root
/ \
2 3 ← 2、3 是 1 的孩子
/ \ \
4 5 6 ← 4、5、6 是叶子
| 术语 | 含义 | 例子 |
|---|---|---|
| 根 root | 最上面的节点 | 1 |
| 父节点 parent | 上面直接连着它的节点 | 2 的父节点是 1 |
| 子节点 child | 下面直接连着它的节点 | 1 的子节点是 2、3 |
| 叶子 leaf | 没有子节点的节点 | 4、5、6 |
| 深度 depth | 从根往下数第几层 | 根深度 1 |
| 子树 subtree | 某个节点和它下面所有节点 | 以 2 为根的子树 |
注意:树和链表最大的区别是——树不会成环,而且每个节点只有一个父节点。这两条保证了从根出发能走遍整棵树,且不会绕回来。
二叉树是最常用的树:每个节点最多有 左、右两个子节点。
struct Node {
int val;
Node* left; // 左孩子
Node* right; // 右孩子
Node(int v) : val(v), left(nullptr), right(nullptr) {}
};
空指针 nullptr 表示「这个方向没有孩子」。注意它和链表不同:链表只有一个方向,树有两个,所以每个节点要存两个指针。
Node* root = new Node(1);
root->left = new Node(2);
root->right = new Node(3);
root->left->left = new Node(4);
root->left->right = new Node(5);
root->right->right = new Node(6);
这就建出了本节课开头的那棵树。注意 root->right->left 没有赋值,所以 3 只有右孩子 6,左孩子是 nullptr。
遍历 = 按某种顺序访问每个节点一次。二叉树的三种经典顺序只差「访问根」这一步放在哪里。
| 遍历 | 顺序 | 口诀 |
|---|---|---|
| 前序遍历 preorder | 根 → 左 → 右 | 根左右 |
| 中序遍历 inorder | 左 → 根 → 右 | 左根右 |
| 后序遍历 postorder | 左 → 右 → 根 | 左右根 |
void preorder(Node* root) {
if (root == nullptr) return;
cout << root->val << " "; // 先访问根
preorder(root->left); // 再左
preorder(root->right); // 再右
}
void inorder(Node* root) {
if (root == nullptr) return;
inorder(root->left); // 先左
cout << root->val << " "; // 再根
inorder(root->right); // 再右
}
void postorder(Node* root) {
if (root == nullptr) return;
postorder(root->left); // 先左
postorder(root->right); // 再右
cout << root->val << " "; // 最后根
}
用上面那棵树(1 为根,2、3 是孩子,4、5 在 2 下,6 在 3 的右边):
前序:1 2 4 5 3 6
中序:4 2 5 1 3 6
后序:4 5 2 6 3 1
层序:1 2 3 4 5 6
把结果按「第几个被访问」拆开,同一棵小树的四种顺序一目了然(表头 1–6 是访问次序,不是节点编号):
| 遍历 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 前序 | 1 | 2 | 4 | 5 | 3 | 6 |
| 中序 | 4 | 2 | 5 | 1 | 3 | 6 |
| 后序 | 4 | 5 | 2 | 6 | 3 | 1 |
| 层序 | 1 | 2 | 3 | 4 | 5 | 6 |
盯住前三个:前序第一个是根 1,后序最后一个才是根 1,中序的根 1 在中间——这就是「根放在哪」的全部区别。层序则完全按层来,先第一层、再第二层、再第三层。
nullptr 就停,这就是递归的边界。
层序遍历用队列实现,先访问第 1 层,再第 2 层、第 3 层……
#include <queue>
void levelorder(Node* root) {
if (root == nullptr) return;
queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* cur = q.front();
q.pop();
cout << cur->val << " ";
if (cur->left != nullptr) q.push(cur->left);
if (cur->right != nullptr) q.push(cur->right);
}
}
上面那棵树的层序结果:
1 2 3 4 5 6
为什么用队列?因为层序是「先遇到的先处理」——第 2 层没处理完,不会轮到第 3 层,这正是队列的先进先出。换成栈就变成深度优先了。
除了指针,也可以用数组存:下标 i 的左孩子是 2*i,右孩子是 2*i+1。
int tree[100] = {0, 1, 2, 3, 4, 5, 0, 6};
// 下标 1 是根
// 1 的左孩子是 2,右孩子是 3
// 2 的左孩子是 4,右孩子是 5
// 3 的左孩子是下标 6,但 tree[6] 是 0,表示空
// 3 的右孩子是下标 7,tree[7] = 6
注意:用数组存树,空位置要用 0 或其他特殊值占位,否则左右关系会错。上面 tree[6] = 0 就是给 3 的左孩子留的空位——如果没有它,tree[7] = 6 的位置就全错位了。
void preorderArray(int i) {
if (tree[i] == 0) return;
cout << tree[i] << " ";
preorderArray(2 * i); // 左孩子
preorderArray(2 * i + 1); // 右孩子
}
#include <iostream>
using namespace std;
struct Node {
int val;
Node* left;
Node* right;
Node(int v) : val(v), left(nullptr), right(nullptr) {}
};
// ==== 三种深度优先遍历 ====
void preorder(Node* root) {
if (!root) return;
cout << root->val << " ";
preorder(root->left);
preorder(root->right);
}
void inorder(Node* root) {
if (!root) return;
inorder(root->left);
cout << root->val << " ";
inorder(root->right);
}
void postorder(Node* root) {
if (!root) return;
postorder(root->left);
postorder(root->right);
cout << root->val << " ";
}
// ==== 建树并输出 ====
int main() {
Node* root = new Node(1);
root->left = new Node(2);
root->right = new Node(3);
root->left->left = new Node(4);
root->left->right = new Node(5);
root->right->right = new Node(6);
preorder(root); cout << endl;
inorder(root); cout << endl;
postorder(root); cout << endl;
return 0;
}
if (root == nullptr) return; 是三个遍历的第一行,漏了就是空指针崩溃。2*i / 2*i+1 是 1-based 的公式,从 0 开始要整套换。2*i 就会指到别的子树去。q.push(root):空树时后面取 cur->val 会崩。 A
/ \
B C
/ \ \
D E F
P4913 二叉树深度。
nullptr。if (root == nullptr) return;,再决定打印语句放哪。push 之前判 left / right 是否为空。2*i、右 2*i+1;空位有没有占住。nullptr 表示。2*i、右 2*i+1,空位要占位。