前面学顺序表、链表、栈、队列的时候,你会发现一个共同点: 它们本质上都还能沿着“一条线”去理解。
哪怕链表不是连续存储, 它的逻辑关系也依然是线性的:前一个、后一个。
但树不一样。
一旦进入树,关系就开始分叉了。
这也是为什么很多人第一次学二叉树时,会明显感觉数据结构像是“换了一种脑回路”: 不能再只顺着一条线思考 开始真正接触递归式结构 开始处理父子、左右、层次、子树这些概念 而二叉树恰恰就是这道门槛里最经典、也最重要的第一步。
仓库位置:
D:\Users\30335\c++技术栈学习\data-structure\二叉树
核心文件:
test.c
Queue.h
Queue.c
一、为什么树会让人感觉“突然不线性了” 树和前面的结构最大的不一样在于: 一个结点后面,不一定只跟着一个结点。
它可能分叉成多个方向。
所以你不能再单纯用“前驱/后继”去描述全部关系了。
树里要开始习惯这些概念: 根结点 父结点 孩子结点 兄弟结点 叶子结点 子树 层次 高度 这些词刚开始看着很多, 其实本质上都在描述一件事: 谁挂在谁下面。
一旦你把树理解成“层次关系结构”,这些概念就会顺很多。
二、二叉树为什么是树里最值得先学的一种 二叉树的定义很简单: 每个结点最多有两个孩子,分别叫左孩子和右孩子。
它之所以重要,不是因为现实里的树都长这样, 而是因为它在“足够简单”和“足够强大”之间取得了非常好的平衡。
它简单到: 结点结构直观 左右关系清楚 很适合递归思考 它又强大到: 很多后续结构都和它有关 遍历方式非常经典 堆、搜索树、平衡树等内容都和它一脉相承 所以二叉树不是“树的一个小分支”, 它几乎就是整个树结构学习的主干入口。
三、二叉树的结点长什么样 仓库里的结点定义很典型:
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
一个二叉树结点只做三件事: 存数据 记左孩子 记右孩子 你可以发现,它和单链表结点的最大不同就在于: 链表只指向一个方向,二叉树结点要同时维护两个分支。
从这里开始,“线性关系”就正式进入“分叉关系”了。
四、二叉树最基础的建立方式,为什么往往是手工连结点 仓库里给了一棵测试树的构造过程:
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
BTNode* node7 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
node5->right = node7;
return node1;
}
这种写法虽然不适合做通用输入, 但特别适合教学和测试。
因为它能非常直观地把“树到底是怎么连出来的”展示给你看。
你会发现,树的本质并不神秘: 它本质上仍然是一堆结点,只不过结点之间的连接方式变复杂了。
五、为什么二叉树特别适合递归 这一点是树结构最重要的思维基础。
二叉树非常适合递归,不是因为“大家都喜欢这么写”, 而是因为它本身就是递归定义的: 一棵二叉树可以为空 也可以由根结点 + 左子树 + 右子树构成 换句话说: 一棵树的问题,天然可以拆成“当前结点 + 左子树 + 右子树”的问题。
这就是递归和树为什么那么契合。
所以当你看到前序、中序、后序遍历时, 本质上不是在背三套模板, 而是在熟悉“当前结点什么时候处理”这件事。
六、前序、中序、后序遍历,到底差在哪 仓库里的三种遍历都很标准。
前序遍历
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
特点是: 先处理当前结点,再处理左右子树。
中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
特点是: 先左,再当前,再右。
后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
特点是: 先左右子树,最后才处理当前结点。
所以这三种遍历真正的区别,不在“递归有没有用”, 而在于: 访问当前结点的时机不同。
这句话一旦吃透,遍历就不容易混。
七、树的很多统计问题,本质上都是“递归拆解” 课件和仓库里这类问题特别多: 求结点总数 求叶子结点个数 求第 K 层结点个数 求树高 你会发现,这些函数几乎都能写成同一种递归思路: 1. 先想空树怎么办 2. 再想当前结点要贡献什么 3. 最后递归到左右子树去合并结果 比如结点总数:
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
这里非常典型: 空树贡献 0 当前结点贡献 1 总数 = 左子树 + 右子树 + 当前结点 这就是树题最常见的递归套路。
八、为什么树高问题特别能体现“递归感觉” 仓库里的树高函数是这样写的:
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftheight = TreeHeight(root->left);
int rightheight = TreeHeight(root->right);
return fmax(leftheight, rightheight) + 1;
}
这段代码特别值得体会。
它不是在“遍历每个结点然后顺手算一下高度”, 而是在表达一个非常清楚的结构性结论: 一棵树的高度 = 左右子树高度的较大值 + 当前层 这就是递归在树里的魅力。
不是机械地“函数调自己”,而是在复用结构定义本身。
九、为什么层序遍历和前中后序不是一类东西 很多人第一次学树遍历时,会把层序遍历也当成“另一种递归遍历”。
其实不是。
层序遍历最自然的理解方式,不是递归,而是: 按层推进。
仓库里的实现就是典型的队列写法:
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
}
这里的节奏特别像: 当前队头先处理 处理完,把它的下一层孩子按顺序补到队尾 再继续处理下一个 这其实就是典型的“流程推进”,所以它天然适合队列。
十、完全二叉树判断,为什么也经常和队列一起出现 仓库里的完全二叉树判断思路是: 先层序遍历 一旦遇到空结点,就进入“后续必须全空”的阶段 如果后面又遇到非空结点,就不是完全二叉树 这套思路特别像在检查: 层序序列里,空缺出现之后,后面还能不能再冒出实体结点。
而这个“按层顺序检查”的过程,本来就非常适合队列。
所以你会慢慢发现: 前中后序遍历靠递归 层序遍历靠队列 完全二叉树判断也常常借助队列 这不是偶然,而是结构和算法在自然匹配。
十一、二叉树这章最容易混的,不是代码,而是概念层级 很多人学树的时候,容易把几件事搅在一起: 树的概念 二叉树的概念 遍历方式 递归写法 层序遍历 其实最好的学习顺序应该是: 1. 先接受“树是非线性结构” 2. 再理解“二叉树是树里最基础的一种” 3. 再去看“结点之间怎么连” 4. 最后看“递归和队列怎么分别处理它” 顺序一旦理顺,树就没有想象中那么可怕。
十二、这章真正该学到的,不只是几个函数,而是“结构性思考” 我觉得二叉树最重要的价值,不是让你背会: 前序怎么写 中序怎么写 后序怎么写 而是让你第一次真正建立一种结构性思考: 一个问题,不一定沿着一条线展开。
它也可以天然拆成“当前 + 左边 + 右边”。
这是树带给人的真正变化。
你一旦习惯了这种思维, 后面很多递归题、分治题、搜索题都会开始变得顺手。
复习时只看这几句就够了 树是非线性结构,二叉树是最基础、最重要的一种 二叉树结点通常包含数据、左孩子、右孩子 前中后序遍历的区别在于访问当前结点的时机不同 很多树题本质上都是“当前结点 + 左右子树”的递归合并 层序遍历天然适合用队列 判断完全二叉树时,经常会结合层序遍历思路 二叉树最重要的不只是代码,而是递归式结构思维
