
个人主页 : 流年如梦
专栏 : 《零基础轻松入门C语言》 《数据结构:从入门到掌握》
文章目录
- 一.数的基础概念
- 二.二叉树
- 三.二叉树的存储结构
- [四.顺序结构二叉树 --> 堆](#四.顺序结构二叉树 --> 堆)
- 五.堆的应用
-
- 5.1堆排序
- 5.2Top-K问题
-
- 5.2.1了解Top-K问题
- [5.2.2最优解法 --> 堆](#5.2.2最优解法 --> 堆)
- 5.2.3代码实现
- 六.链式二叉树(递归是其核心)
- 七.二叉树的遍历
-
- [7.1前序遍历(根 --> 左 --> 右)](#7.1前序遍历(根 --> 左 --> 右))
- [7.2中序遍历(左 --> 根 --> 右)](#7.2中序遍历(左 --> 根 --> 右))
- [7.3后序遍历(左 --> 右 --> 根)](#7.3后序遍历(左 --> 右 --> 根))
- 7.4层序遍历(借助队列)
- 八.二叉树接口(递归是其核心)
-
- 8.1求结点总数
- [8.2 求叶子数](#8.2 求叶子数)
- [8.3 求树高度](#8.3 求树高度)
- [8.4 第k层结点数](#8.4 第k层结点数)
- [8.5 查找值为 x 的结点](#8.5 查找值为 x 的结点)
- [8.6 销毁二叉树](#8.6 销毁二叉树)
- 🎯总结
- ⚠️易错点
一.数的基础概念
1.1什么是树
树是由n(n≥0)个结点组成的层次关系非线性集合
- 有且仅有一个根节点,无前驱
- 除根外,其他结点分成互不相交的子树
- 树是递归定义的
- N个结点的树有
N-1条边
现实中的二叉树

1.2树的常用术语
| 术语 | 意思 |
|---|---|
| 父结点/双亲结点 | 若⼀个结点含有子结点,则这个结点称为其子结点的父结点 |
| 子结点/孩子结点 | ⼀个结点含有的⼦树的根结点称为该结点的⼦结点 |
| 结点的度 | ⼀个结点有⼏个孩子,他的度就是多少 |
| 树的度 | ⼀棵树中,最大的结点的度称为树的度 |
| 叶子结点/终端结点 | 度为0的结点称为叶结点 |
| 分支结点/非终端结点 | 度不为0的结点 |
| 兄弟结点 | 具有相同父结点的结点互称为兄弟结点(亲兄弟) |
| 结点的层次 | 从根开始定义起,根为第1层,根的⼦结点为第2层,以此类推 |
| 树的高度或深度 | 树中结点的最大层次 |
| 结点的祖先 | 从根到该结点所经分⽀上的所有结点 |
| 路径 | ⼀条从树中任意节点出发,沿⽗节点 --> 子节点连接,达到任意节点的序列 |
| 子孙 | 以某结点为根的子树中任⼀结点都称为该结点的子孙 |
| 森林 | 由m(m>0)棵互不相交的树的集合称为森林 |
1.3树的表示(孩子兄弟表示法)
孩子兄弟表示法 是树的表示的其中一种,优势在于用二叉树结构表示普通树,统一实现逻辑
c
struct Node
{
int data;
struct Node* Child;
struct Node* Brother;
};
二.二叉树
2.1概念
- 每个结点最多2个孩子
- 子树分左右、有序
- 五种基本形态:空树、只有根、只有左、只有右、左右都有
2.2特殊二叉树
2.2.1满二叉树
- 每一层结点数都达到最大
- 高度
h--> 结点总数2^h − 1
如下图所示:

2.2.2完全二叉树
- 从满二叉树编号 1~n 一一对应,最后一层连续靠左
- 满二叉树是特殊的完全二叉树
如下图所示:

再看看这棵二叉树是不是完全二叉树:

显而易见,这棵二叉树不是完全二叉树,即非完全二叉树 ,因为它的最后一层不是从左向右依次排序
2.3性质
- 第
i层最多2^(i-1)个结点 - 高度
h最多2^h − 1个结点 - 叶子数 n0 = 度为2结点数 n2 + 1
- 完全二叉树高度 h = log₂(n+1) 向上取整
- 数组下标 i:
左孩子:2*i+1
右孩子:2*i+2
父节点:(i-1)/2
三.二叉树的存储结构
3.1顺序存储(底层是数组)
- 适合完全二叉树
- 非完全二叉树会空间浪费
- 典型应用:堆
3.2链式存储(二叉链)
最常用,灵活、无浪费
c
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
四.顺序结构二叉树 --> 堆
堆是完全二叉树,它满足:
大根堆 :父
≥孩子
小根堆 :父≤孩子
4.1堆的结构定义
c
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
} HP;
4.2堆的初始化
c
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
4.3堆的销毁
c
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
4.4向上调整法
c
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
// 大根堆
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
🧐分析:从孩子向上找父,不满足堆则交换,直到满足堆或到根
4.5堆插入
c
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcap = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcap * sizeof(HPDataType));
if (!tmp)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newcap;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
🧐分析:扩容 --> 尾插 --> 向上调整
4.6向下调整法(用于删除堆顶)
👉前提是左右子树都是堆
c
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 选更大的孩子
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
🧐分析:选出较大孩子,交换下沉,直到满足堆
4.7删除堆顶
c
void HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
🧐分析:堆顶与最后交换 --> 删除最后 --> 向下调整,以保证堆结构不被破坏
4.8取堆顶、判空以及大小
c
HPDataType HPTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HPSize(HP* php)
{
assert(php);
return php->size;
}
五.堆的应用
5.1堆排序
-
升序(从小到大)--> 建大堆 :
每次把最大值放到末尾,最终从低到高有序
-
降序(从大到小)--> 建小堆 :
每次把最小值放到末尾,最终从高到低有序
-
时间复杂度:
O(NlogN) -
建堆复杂度:
O(N)
如下所示(以建大堆例):
c
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
5.2Top-K问题
5.2.1了解Top-K问题
从海量数据或大量数据 中,找出最大的K个或最小的K个
例如:
- 从1亿个数里找出最大的100个
- 从1000万个商品里找出销量前10名
- 从日志里找出访问量最高的5个IP
5.2.2最优解法 --> 堆
核心思想:
找前K大 --> 建小堆
找前K小 --> 建大堆
- 小根堆堆顶是堆里最小的
- 遍历所有数据,比堆顶大就替换它,再调整
- 最后堆里剩下的就是 最大的K个数
其中时间复杂度为O(NlogK)
5.2.3代码实现
(1)交换函数
c
void Swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
(2)小堆向下调整(找前K大)
c
void AdjustDown(int* a, int n, int parent) {
int child = parent * 2 + 1;
while (child < n) {
//小堆
if (child + 1 < n && a[child + 1] < a[child]) {
child++;
}
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
(3)主函数
c
//找出N个数中最大的K个
void PrintTopK(int* a, int n, int k)
{
assert(a);
assert(k > 0);
//开一个K大小的数组(开辟空间)
int* minHeap = (int*)malloc(sizeof(int) * k);
if (minHeap == NULL)
{
perror("malloc fail");
return;
}
//用前K个数据初始化数组
for (int i = 0; i < k; i++)
{
minHeap[i] = a[i];
}
//建小堆
for (int i = (k - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(minHeap, k, i);
}
//剩下的 N-K 个数据依次比较
for (int i = k; i < n; ++i)
{
//比堆顶大,替换堆顶,再向下调整
if (a[i] > minHeap[0])
{
minHeap[0] = a[i];
AdjustDown(minHeap, k, 0);
}
}
printf("最大的 %d 个元素:", k);
for (int i = 0; i < k; ++i)
{
printf("%d ", minHeap[i]);
}
printf("\n");
free(minHeap);
}
int main() {
int arr[] = {3, 5, 9, 1, 7, 2, 8, 10, 4, 6};
int n = sizeof(arr) / sizeof(arr[0]);
int k = 3;
TopK(arr, n, k);
return 0;
}
运行结果:

六.链式二叉树(递归是其核心)
6.1创建节点
c
BTNode* BuyNode(int x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
6.2建一棵二叉树(手动创建)
c
BTNode* CreateTree()
{
BTNode* n1 = BuyNode(1);
BTNode* n2 = BuyNode(2);
BTNode* n3 = BuyNode(3);
BTNode* n4 = BuyNode(4);
BTNode* n5 = BuyNode(5);
BTNode* n6 = BuyNode(6);
n1->left = n2;
n1->right = n4;
n2->left = n3;
n4->left = n5;
n4->right = n6;
return n1;
}
七.二叉树的遍历
7.1前序遍历(根 --> 左 --> 右)
c
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
🧐分析:先访问根,再递归左,左递归结束后,再递归右
7.2中序遍历(左 --> 根 --> 右)
c
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
7.3后序遍历(左 --> 右 --> 根)
c
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
7.4层序遍历(借助队列)
c
void LevelOrder(BTNode* root)
{
if (!root) return;
Queue q;
QueueInit(&q);
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);
}
🧐分析 :从上到下、从左到右,必须用队列实现
八.二叉树接口(递归是其核心)
8.1求结点总数
c
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 :
BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
8.2 求叶子数
c
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
8.3 求树高度
c
int BinaryTreeDepth(BTNode* root)
{
if (root == NULL) return 0;
int left = BinaryTreeDepth(root->left);
int right = BinaryTreeDepth(root->right);
return left > right ? left + 1 : right + 1;
}
8.4 第k层结点数
c
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (!root) return 0;
if (k == 1) return 1;
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
8.5 查找值为 x 的结点
c
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (!root) return NULL;
if (root->data == x) return root;
BTNode* left = BinaryTreeFind(root->left, x);
if (left) return left;
BTNode* right = BinaryTreeFind(root->right, x);
if (right) return right;
return NULL;
}
8.6 销毁二叉树
c
void BinaryTreeDestroy(BTNode** root)
{
if (*root == NULL) return;
BinaryTreeDestroy(&(*root)->left);
BinaryTreeDestroy(&(*root)->right);
free(*root);
*root = NULL;
}
🎯总结
- 二叉树是递归定义的非线性结构,最多 2 个孩子,有序
- 顺序存储适合完全二叉树,典型实现堆
- 链式存储最常用,二叉链结构简单高效
- 堆的核心是向上或向下调整 ,插入删除
O(logN) - 堆可用于堆排序、Top-K、优先级队列
- 链式二叉树依靠递归实现绝大多数接口
- 四种遍历:前 / 中 / 后 / 层序
- 时间复杂度:遍历类
O(N),堆操作O(logN) - 堆排序关键:升序建大堆,降序建小堆
⚠️易错点
- 归没有终止条件导致栈溢出
- 堆插入或删除忘记调整,破坏堆结构
- 求树高误用减法,应该取大
+1- 层序遍历忘记判空
- 销毁 二叉树不置空,形成野指针
- 完全二叉树下标计算错误
- 遍历顺序混淆:前序根最先,后序根最后
- 堆排序升序或降序搞反:升序大堆,降序小堆
👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步