跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

二叉树:从线性结构走到分叉结构,数据结构的视角开始真正变了

前面学顺序表、链表、栈、队列的时候,你会发现一个共同点: 它们本质上都还能沿着“一条线”去理解。

哪怕链表不是连续存储, 它的逻辑关系也依然是线性的:前一个、后一个。

但树不一样。

一旦进入树,关系就开始分叉了。

这也是为什么很多人第一次学二叉树时,会明显感觉数据结构像是“换了一种脑回路”: 不能再只顺着一条线思考 开始真正接触递归式结构 开始处理父子、左右、层次、子树这些概念 而二叉树恰恰就是这道门槛里最经典、也最重要的第一步。

仓库位置:

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. 最后看“递归和队列怎么分别处理它” 顺序一旦理顺,树就没有想象中那么可怕。

十二、这章真正该学到的,不只是几个函数,而是“结构性思考” 我觉得二叉树最重要的价值,不是让你背会: 前序怎么写 中序怎么写 后序怎么写 而是让你第一次真正建立一种结构性思考: 一个问题,不一定沿着一条线展开。

它也可以天然拆成“当前 + 左边 + 右边”。

这是树带给人的真正变化。

你一旦习惯了这种思维, 后面很多递归题、分治题、搜索题都会开始变得顺手。

复习时只看这几句就够了 树是非线性结构,二叉树是最基础、最重要的一种 二叉树结点通常包含数据、左孩子、右孩子 前中后序遍历的区别在于访问当前结点的时机不同 很多树题本质上都是“当前结点 + 左右子树”的递归合并 层序遍历天然适合用队列 判断完全二叉树时,经常会结合层序遍历思路 二叉树最重要的不只是代码,而是递归式结构思维

相关文章