Lesson 0404 · 树与二叉树基础

④ 数据结构与算法 — 第 4 课

上节课:链表用 next 把节点串成一条线。
这节课:每个节点可以有两个「下一节点」,从一条线变成一棵树。树是后面 BST、堆、图、搜索、DP 的基础。
三句话速览
  1. 二叉树每个节点最多两个孩子:left 和 right。
  2. 前序根左右、中序左根右、后序左右根。
  3. 层序用 queue;数组存树左 2i、右 2i+1。

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 为根的子树

注意:树和链表最大的区别是——树不会成环,而且每个节点只有一个父节点。这两条保证了从根出发能走遍整棵树,且不会绕回来。

2. 二叉树:最多两个孩子

二叉树是最常用的树:每个节点最多有 左、右两个子节点。

struct Node {
    int val;
    Node* left;    // 左孩子
    Node* right;   // 右孩子

    Node(int v) : val(v), left(nullptr), right(nullptr) {}
};

空指针 nullptr 表示「这个方向没有孩子」。注意它和链表不同:链表只有一个方向,树有两个,所以每个节点要存两个指针。

3. 手动建一棵二叉树

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。

4. 三种深度优先遍历

遍历 = 按某种顺序访问每个节点一次。二叉树的三种经典顺序只差「访问根」这一步放在哪里。

遍历顺序口诀
前序遍历 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 是访问次序,不是节点编号):

遍历123456
前序124536
中序425136
后序452631
层序123456

盯住前三个:前序第一个是根 1,后序最后一个才是根 1,中序的根 1 在中间——这就是「根放在哪」的全部区别。层序则完全按层来,先第一层、再第二层、再第三层。

递归的本质:处理一棵树 = 处理根 + 处理左子树 + 处理右子树。遇到 nullptr 就停,这就是递归的边界。

5. 层序遍历:一层一层访问

层序遍历用队列实现,先访问第 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 层,这正是队列的先进先出。换成栈就变成深度优先了。

6. 数组存二叉树

除了指针,也可以用数组存:下标 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);   // 右孩子
}

7. 完整示例

#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;
}

8. 坑点清单

9. 小测

Q1. 前序遍历的顺序是?
Q2. 二叉树的递归边界通常是什么?
Q3. 层序遍历用什么数据结构?
Q4. 已知一棵树:根 1,左 2,右 3。中序遍历结果是?

10. 动手练习

题目1:写出遍历结果

手动写出下面这棵树的前序、中序、后序、层序结果:

        A
        / \
       B   C
      / \   \
     D   E   F
题目2:洛谷 P1305 新二叉树

给定二叉树的节点关系,输出前序遍历。
这题是二叉树遍历的直接应用,做完再做 P4913 二叉树深度。
卡住时的自查顺序
  1. 先画树:根在最上面,左右孩子分两边,空的地方标 nullptr。
  2. 三个遍历都先写 if (root == nullptr) return;,再决定打印语句放哪。
  3. 前序先打印、中序夹中间、后序最后打印,背不住就把三份代码并排看。
  4. 层序检查容器:必须是 queue;push 之前判 left / right 是否为空。
  5. 数组存树检查基准:1-based 才有左 2*i、右 2*i+1;空位有没有占住。

11. 本课小结

下一课预告
0405 二叉搜索树 BST —— 给二叉树加一条规则:左小右大。查找从此每步砍掉一半,中序遍历还能直接得到有序序列。也会看到它为什么会退化成链表。

← 上一课:链表进阶 · 课程对照表

书籍 《算法 (第4版)》第 3.2 节(二叉查找树)

视频 黑马 STL 后续 / 数据结构篇