树(一):从线性到非线性的跨越

说实话,学数据结构这么久,前面链表、栈、队列都是线性结构。说白了就是一根绳子串到底。但现实世界哪有这么简单?你想想看,公司的组织架构、文件系统的目录、甚至你电脑里的文件夹,哪个不是分叉的?

这就是树结构存在的意义。我个人习惯把树叫做“有层次感的链表”。它解决了线性结构无法表达“一对多”关系的问题。

树的基本概念

树是一种非线性的数据结构。它由节点和边组成。每个节点可以有多个子节点,但只有一个父节点(根节点除外)。

几个关键术语你得记住:

  • 根节点:没有父节点的节点,一棵树只有一个根
  • 叶子节点:没有子节点的节点
  • :一个节点拥有的子节点数量
  • 深度:从根节点到该节点的路径长度
  • 高度:从该节点到最远叶子节点的路径长度

核心要点:树的定义是递归的。每个子树本身也是一棵树。这个递归思想在后面遍历时会反复用到。

我在项目中遇到过一个问题:用链表存储组织架构,结果查找某个部门的所有下属部门时,需要遍历整个链表。换成树结构后,直接递归遍历子树就搞定了。性能提升不是一星半点。

二叉树定义与性质

二叉树是树中最常见的一种。每个节点最多有两个子节点,分别叫左孩子和右孩子。你想想看,为什么是“最多两个”?因为计算机底层很多操作都是二元的,比如比较大小、真假判断。

二叉树有几个重要性质:

  • 第 i 层最多有 2^(i-1) 个节点(i≥1)
  • 深度为 k 的二叉树最多有 2^k - 1 个节点
  • 叶子节点数 = 度为2的节点数 + 1

我的经验:这些性质看起来是数学公式,但在计算内存占用、判断树是否平衡时非常实用。我曾经用这些公式估算过一棵满二叉树需要多少内存,避免了内存溢出的坑。

二叉树的存储结构

二叉树的存储有两种方式:顺序存储和链式存储。

顺序存储

用数组来存。根节点放在下标1(或0),左孩子在下标2i,右孩子在下标2i+1。这种方式适合完全二叉树,但普通二叉树会浪费大量空间。

// 顺序存储示例
int tree[100];  // tree[1] 是根节点
// tree[2] 是左孩子,tree[3] 是右孩子

链式存储

每个节点包含数据域和两个指针域。这是最常用的方式。

typedef struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
} TreeNode;

注意:链式存储时,一定要记得初始化左右指针为NULL。我曾经因为忘记初始化,导致遍历时访问了野指针,程序直接崩溃。排查了半天才发现是这个问题。

二叉树的遍历

遍历是二叉树最核心的操作。说白了就是按照某种顺序访问每个节点一次。一共有四种遍历方式。

前序遍历(根左右)

先访问根节点,再遍历左子树,最后遍历右子树。

void preOrder(TreeNode *root) {
    if (root == NULL) return;
    printf("%d ", root->data);  // 访问根
    preOrder(root->left);       // 遍历左子树
    preOrder(root->right);      // 遍历右子树
}

中序遍历(左根右)

先遍历左子树,再访问根节点,最后遍历右子树。对于二叉搜索树,中序遍历得到的是有序序列。

void inOrder(TreeNode *root) {
    if (root == NULL) return;
    inOrder(root->left);
    printf("%d ", root->data);
    inOrder(root->right);
}

后序遍历(左右根)

先遍历左子树,再遍历右子树,最后访问根节点。常用于删除树或计算表达式。

void postOrder(TreeNode *root) {
    if (root == NULL) return;
    postOrder(root->left);
    postOrder(root->right);
    printf("%d ", root->data);
}

层序遍历

从上到下,从左到右,一层一层地访问。需要借助队列来实现。

void levelOrder(TreeNode *root) {
    if (root == NULL) return;
    Queue q;
    initQueue(&q);
    enqueue(&q, root);
    
    while (!isEmpty(&q)) {
        TreeNode *node = dequeue(&q);
        printf("%d ", node->data);
        if (node->left) enqueue(&q, node->left);
        if (node->right) enqueue(&q, node->right);
    }
}

避坑指南:我曾经在层序遍历时忘记判断左右孩子是否为NULL,结果把NULL节点也入队了。循环直接死掉。记住:入队前一定要判空。

四种遍历对比

遍历方式 访问顺序 典型应用
前序 根 → 左 → 右 复制树、序列化
中序 左 → 根 → 右 二叉搜索树排序
后序 左 → 右 → 根 删除树、表达式求值
层序 逐层从左到右 求树宽度、最短路径

本章知识体系

下面这张图帮你理清本章的核心脉络。我建议你把它保存下来,复习时对照着看。

树(一) 基本概念 根节点、叶子、度、深度 二叉树定义与性质 最多2个子节点、满二叉树 存储结构 顺序存储 / 链式存储 四种遍历 前序:根左右 中序:左根右 后序:左右根 层序:逐层遍历 递归思想贯穿始终,理解递归就理解了树

个人建议:学树的时候,一定要动手画图。我当年学二叉树时,每道题都先在纸上画出树结构,然后用手模拟遍历过程。画上几十棵树,你自然就懂了。

嗯,树的内容就讲到这里。记住:树是递归的,遍历是递归的,存储也是递归的。把这个递归思想吃透了,后面的图、堆、红黑树都会轻松很多。

公众号:蓝海资料掘金营,微信deep3321