← 目录 / 算法文档 · 模块十一 树与二叉树 / 11.2 二叉树的存储

11.2 二叉树的存储

两种把二叉树装进程序里的办法:用指针连起来的链式存储,和利用下标关系的数组式存储。

本页目录
① 为什么需要专门的存储方式

回忆一下之前学过的两种基础结构:数组是一段连续的空间,靠下标直接定位;链表靠指针把节点一个个串起来,每个节点多存一个"指向下一个节点"的指针。但二叉树是"一对多"的结构——一个节点最多要连着两个子节点,普通数组和普通链表都没法直接照搬,需要专门设计存储方式。

常见的做法有两种:链式存储——每个节点额外存两个指针,分别指向左子节点和右子节点,思路上是链表的自然延伸;数组式存储——只利用下标之间的数学关系,不需要额外的指针空间,但只对"完全二叉树"(11.1 节讲过的性质)好用。下面分别来看。

② 链式存储:结构体与指针

链式存储是最通用、平时写代码最常见的写法:每个节点是一个结构体,除了存自己的值,再存两个指针 leftright,分别指向左子节点和右子节点——如果某个子节点不存在,对应指针就是 nullptr

C++ · 二叉树节点的结构体定义
1struct TreeNode
2{
3 int val; // 节点自己存的值
4 TreeNode* left; // 指向左子节点,没有就是 nullptr
5 TreeNode* right; // 指向右子节点,没有就是 nullptr
6 TreeNode(int v) : val(v), left(nullptr), right(nullptr) {} // 构造函数:新节点默认没有子节点
7};

手动搭建 11.1 节那棵示例树(根节点 1,左子节点 2、右子节点 32 的子节点是 453 的左子节点是 6),代码就是把每个指针手动接上:

C++ · 手动搭建示例树
1TreeNode* root = new TreeNode(1); // 先建根节点
2root->left = new TreeNode(2); // 1 的左子节点是 2
3root->right = new TreeNode(3); // 1 的右子节点是 3
4root->left->left = new TreeNode(4); // 2 的左子节点是 4
5root->left->right = new TreeNode(5); // 2 的右子节点是 5
6root->right->left = new TreeNode(6); // 3 的左子节点是 6,右子节点留空(nullptr)
📖
判断一个节点是不是叶子:只要 node->left == nullptr && node->right == nullptr,就说明它没有任何子节点,是叶子节点——这个判断在后面写遍历、写递归终止条件时会反复用到。
③ 数组式存储:下标里的秘密

如果一棵二叉树恰好是完全二叉树(11.1 节讲过:除最后一层外都填满,最后一层从左到右紧凑排列),就可以不用指针,只靠下标之间的数学关系把整棵树存进一个普通数组里——把树按"从上到下、同一层从左到右"的顺序编号,编号就是数组下标。

按层编号后存进数组,箭头是"父节点 → 子节点"的下标关系
2·0+1=1 2·0+2=2 2·1+1=3 2·1+2=4 1 [0] 2 [1] 3 [2] 4 [3] 5 [4] 6 [5]
同一棵示例树,按层编号后依次是 1,2,3,4,5,6,正好对应下标 0~5橙色箭头是下标 0(节点 1)指向它两个子节点的下标;蓝色箭头是下标 1(节点 2)指向它两个子节点的下标——子节点的下标,永远等于"父节点下标 × 2"再加 1 或加 2。
💡
下标从 0 开始时的公式:节点下标为 i,那么:
左子节点下标 = 2*i + 1  右子节点下标 = 2*i + 2  父节点下标 = (i - 1) / 2(整数除法)
验证一下:节点 2 在下标 1,左子节点下标应该是 2*1+1=3(节点 4 ✓),右子节点下标是 2*1+2=4(节点 5 ✓)。
⚠️
非完全二叉树用数组存会浪费大量空间:比如一棵"只有右子节点、一直往右斜下去"的树(一共 5 个节点,深度却有 4 层)。按上面的编号规则,右子节点的下标要跳到 2*i+2,越往下跳得越远——中间全是没有节点、却必须留出来的空位。这样一棵树最坏情况下可能需要开到几十甚至上百的数组长度,才能存下 5 个节点。这也是为什么数组式存储只适合完全二叉树(比如以后要学的"堆"就是完全二叉树,非常适合用数组存),普通二叉树通常还是用链式存储。
④ 两种方式怎么选

两种存储方式各有取舍,实际做题时按下面的表格来判断:

对比项链式存储数组式存储
适用范围任意二叉树只适合完全二叉树
额外空间每个节点多存 2 个指针不需要额外指针,但可能有空位浪费
找父/子节点靠指针,O(1)靠下标计算,O(1),甚至更快
动态增删节点灵活,new/delete 即可不方便,插入可能要整体挪动
常见场景普通二叉树、二叉搜索树(11.4 节)堆、线段树这类结构天然完全的场景
⑤ 常见陷阱

把本节内容汇总成几条最容易踩坑的规则:

新建节点忘记初始化指针:如果结构体没有写构造函数,new TreeNode 出来的 leftright 是未初始化的"野指针",直接判断 if (node->left == nullptr) 或访问它都可能出错。务必像上面代码那样写一个构造函数,把 leftright 显式设成 nullptr
数组式存储的下标公式要看清"从 0 开始"还是"从 1 开始":本节公式是下标从 0 开始的版本(左=2i+1,右=2i+2)。如果数组下标习惯从 1 开始存(下标 0 空着不用),公式会变成左=2i,右=2i+1,父=i/2——两套公式不要混用,混用会导致找错父子节点。
忘记判断数组下标是否越界:用公式算出左右子节点的下标后,要先检查这个下标是否小于数组实际存的节点个数,再去访问——否则可能读到"本不存在的子节点"对应的位置,把空位当成了真实节点。
🏆
接下来:下一节(11.3)讲二叉树最核心的操作——遍历:前序、中序、后序(三种基于递归的深度优先遍历)和层序遍历(借助队列,思路和之前学过的 BFS 一脉相承)。