二叉树深度解析:从基础概念到算法实现与应用
一、二叉树的本质定义与核心特性
(一)递归定义与逻辑结构
二叉树是一种 严格有序的树结构,其递归定义为:
- 空树:不含任何结点的集合,是二叉树的特殊形态。
- 非空二叉树:由以下三部分组成:
- 根结点(Root):唯一的顶层结点,没有父结点。
- 左子树(Left Subtree):以根结点左子结点为根的二叉树。
- 右子树(Right Subtree):以根结点右子结点为根的二叉树。
关键特性:
- 度的限制:每个结点最多有 2 个子结点(度为 0、1 或 2),不存在度大于 2 的结点。
- 有序性:左右子树顺序严格区分,例如仅有左子结点的树与仅有右子结点的树视为不同结构。
- 五种基本形态:
(二)结点关系与术语
- 父结点与子结点:若结点 A 的子结点为 B,则 A 是 B 的父结点,B 是 A 的左 / 右子结点。
- 兄弟结点:具有相同父结点的结点互称兄弟,如 B 和 C 是兄弟。
- 叶子结点与分支结点:
- 叶子结点:度为 0 的结点(无任何子结点)。
- 分支结点:度为 1 或 2 的结点(至少有一个子结点)。
- 层次与高度:
- 层次:根结点为第 1 层,子结点为第 2 层,依此类推。
- 高度:树中结点的最大层次,如高度为 3 的二叉树最多有 7 个结点(满二叉树)。
二、特殊二叉树:满二叉树与完全二叉树的对比分析
(一)满二叉树(Perfect Binary Tree)
- 数学定义:高度为 h 的满二叉树,每层结点数达到最大值 2h−1,结点总数为 2h−1。
- 结构特点:
- 所有叶子结点位于最后一层(第 h 层)。
- 不存在度为 1 的结点,每个分支结点都有左右两个子结点。
- 示例(高度 3):
结点数:2*3−1=7,每层结点数分别为 1、2、4。
(二)完全二叉树(Complete Binary Tree)
- 定义:高度为 h、结点数为 n 的二叉树,其结点与高度为 h 的满二叉树中前 n 个结点一一对应(按层序编号)。
- 结构特点:
- 前 h−1 层是满二叉树,第 h 层结点从左到右连续排列,不允许中间有空缺。
- 满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树。
- 示例(高度 3,结点数 5):
与满二叉树前 5 个结点(1,2,3,4,5)位置一致,第 3 层仅有左子结点。
(三)核心区别对比
特性 | 满二叉树 | 完全二叉树 |
---|---|---|
结点分布 | 每层满结点 | 前 h−1 层满,第 h 层左连续 |
度为 1 的结点 | 不存在(必为 0 或 2) | 最多一个,且只能在最后一层 |
结点数公式 | 2^h-1 | n∈[2^(h−1),2^h−1] |
三、存储结构:顺序存储 vs 链式存储的适用场景与实现细节
(一)顺序存储(数组实现,适合完全二叉树)
- 存储逻辑:用一维数组按层序存储结点,下标对应结点编号:
- 根结点:下标 0
- 结点 i 的左子结点:2i+1(若存在)
- 结点 i 的右子结点:2i+2(若存在)
- 结点 i 的父结点:(i−1)/2(i>0 时)
- 示例(完全二叉树):
数组存储:[1, 2, 3, 4, 5]
- 非完全二叉树的空间浪费:
若树结构为单链(仅有左子结点),高度为 h 的树需 2h−1 个存储空间,实际仅用 h 个,空间利用率低。
(二)链式存储(二叉链表,适合任意二叉树)
- 结点结构:
typedef int BTDataType; typedef struct BinaryTreeNode { struct BinaryTreeNode* left; // 左子结点指针 struct BinaryTreeNode* right; // 右子结点指针 BTDataType val; // 数据域 } BTNode;
- 创建与连接结点:
// 创建新结点 BTNode* BuyBTNode(BTDataType val) { BTNode* newNode = (BTNode*)malloc(sizeof(BTNode)); if (!newNode) { perror("malloc failed"); return NULL; } newNode->val = val; newNode->left = newNode->right = NULL; // 初始左右子结点为空 return newNode; } // 构建二叉树示例:1为根,左子2,右子4,2的左子3,4的左子5、右子6 BTNode* CreateTree() { BTNode* n1 = BuyBTNode(1); BTNode* n2 = BuyBTNode(2); BTNode* n3 = BuyBTNode(3); BTNode* n4 = BuyBTNode(4); BTNode* n5 = BuyBTNode(5); BTNode* n6 = BuyBTNode(6); n1->left = n2; n1->right = n4; // 根结点连接左右子 n2->left = n3; // 2号结点仅有左子 n4->left = n5; n4->right = n6; // 4号结点有左右子 return n1; // 返回根结点 }
- 优缺点:
- 优点:动态灵活,无空间浪费,适合频繁插入 / 删除。
- 缺点:需额外指针空间(每个结点两个指针),实现复杂度高于顺序存储。
四、遍历方式:递归遍历与层序遍历的实现与原理
(一)深度优先遍历(DFS,递归实现)
1. 前序遍历(根→左→右)
- 访问顺序:先访问根结点,再递归前序遍历左子树,最后递归前序遍历右子树。
- 代码实现:
void PreOrder(BTNode* root) { if (root == NULL) { printf("N "); // 空结点标记为"N" return; } printf("%d ", root->val); // 访问根 PreOrder(root->left); // 递归左子树 PreOrder(root->right); // 递归右子树 }
- 示例输出(树结构同上):
1 2 3 N N N 4 5 N N 6 N N
(注:N 表示空结点,实际应用中可省略,此处为展示结构)
2. 中序遍历(左→根→右)
- 访问顺序:先递归中序遍历左子树,再访问根结点,最后递归中序遍历右子树。
- 代码实现:
void InOrder(BTNode* root) { if (root == NULL) { printf("N "); return; } InOrder(root->left); // 递归左子树 printf("%d ", root->val); // 访问根 InOrder(root->right); // 递归右子树 }
- 示例输出:
N 3 N 2 N 1 N 5 N 4 N 6 N
(实际非空结点中序:3 2 1 5 4 6)
3. 后序遍历(左→右→根)
- 访问顺序:先递归后序遍历左子树,再递归后序遍历右子树,最后访问根结点。
- 代码实现:
void PostOrder(BTNode* root) { if (root == NULL) { printf("N "); return; } PostOrder(root->left); // 递归左子树 PostOrder(root->right); // 递归右子树 printf("%d ", root->val); // 访问根 }
- 示例输出:
N N 3 N N 2 N N 5 N 6 4 1
(实际非空结点后序:3 2 5 6 4 1)
(二)广度优先遍历(BFS,层序遍历)
- 实现原理:借助队列,按层从上到下、每层从左到右访问结点。
- 代码步骤:
- 根结点入队。
- 循环取出队首结点,访问后将其左右子结点(若存在)入队。
- 直到队列为空。
- 代码实现(假设队列已实现):
void LevelOrder(BTNode* root) { Queue q; QueueInit(&q); // 初始化队列 if (root) QueuePush(&q, root); // 根结点入队(非空时) while (!QueueEmpty(&q)) { BTNode* node = QueueFront(&q); // 取队首结点 QueuePop(&q); // 出队 printf("%d ", node->val); // 访问结点 // 左右子结点入队 if (node->left) QueuePush(&q, node->left); if (node->right) QueuePush(&q, node->right); } QueueDestroy(&q); // 销毁队列,释放内存 }
- 示例输出(树结构同上):
1 2 4 3 5 6
五、堆:基于完全二叉树的高效数据结构
(一)堆的定义与性质
- 定义:满足 “堆性质” 的完全二叉树,分为两种类型:
- 大根堆:父结点值 ≥ 子结点值(根结点为最大值)。
- 小根堆:父结点值 ≤ 子结点值(根结点为最小值)。
- 存储结构:用数组实现,下标对应完全二叉树结点编号。
- 堆性质:对于数组下标 i,有:
- 大根堆:a[i]≥a[2i+1] 且 a[i]≥a[2i+2](若子结点存在)。
- 小根堆:a[i]≤a[2i+1] 且 a[i]≤a[2i+2]。
(二)核心操作:向上调整与向下调整
1. 向上调整(插入新结点时恢复堆性质)
- 场景:新结点插入堆尾(数组末尾),可能破坏堆性质,需与父结点比较并交换。
- 算法步骤:
- 计算新结点下标 child,父结点 parent=(child−1)/2。
- 若 child>0 且新结点值大于父结点(大根堆),交换两者,更新 child=parent,重复直至堆性质满足。
- 代码实现(大根堆):
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; // 已满足堆性质,停止 } } }
2. 向下调整(删除堆顶时恢复堆性质)
- 场景:堆顶元素删除后,将堆尾元素移至堆顶,需与子结点比较并交换。
- 算法前提:左右子树已满足堆性质。
- 算法步骤:
- 设当前结点为 parent,左子结点 child=2parent+1。
- 找到左右子结点中较大者(大根堆),若较大子结点值大于父结点,交换两者,更新 parent=child,重复直至堆性质满足。
- 代码实现(大根堆):
void AdjustDown(HPDataType* a, int n, int parent) { int child = 2 * parent + 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 = 2 * parent + 1; } else { break; // 已满足堆性质,停止 } } }
(三)堆排序:高效的原地排序算法
- 步骤(升序排序,构建大根堆):
- 建堆:对数组从最后一个分支结点开始,自底向上调用
AdjustDown
,时间复杂度 O(n)。 - 排序:每次将堆顶(最大值)与堆尾元素交换,堆大小减 1,对新堆顶调用
AdjustDown
,重复直至堆空。
- 建堆:对数组从最后一个分支结点开始,自底向上调用
- 代码框架:
void HeapSort(int* a, int n) { // 1. 建大根堆 for (int i = (n-1-1)/2; i >= 0; i--) { // 最后一个分支结点的父结点 AdjustDown(a, n, i); } // 2. 排序:交换堆顶与堆尾,调整堆 int end = n - 1; while (end > 0) { Swap(&a[0], &a[end]); // 最大值放到末尾 AdjustDown(a, end, 0); // 调整前end个元素为堆 end--; } }
- 时间复杂度:建堆 O(n) + 排序 O(nlogn),总复杂度 O(nlogn)。
六、二叉树的重要性质与经典问题求解
(一)核心性质推导
1. 叶子结点数公式:n0=n2+1
- 推导过程:
设二叉树中度为 0、1、2 的结点数分别为 n0、n1、n2,总结点数 n=n0+n1+n2。
二叉树边数 E=n−1(每个结点除根外有且仅有一个父结点),同时边数等于各结点度之和:E=0⋅n0+1⋅n1+2⋅n2。
联立得:n0+n1+n2−1=n1+2n2,化简得 n0=n2+1。
2. 完全二叉树高度计算
- 公式:高度 h=⌊log2n⌋+1,其中 n 为结点数。
- 推导:高度为 h 的完全二叉树结点数范围:2h−1≤n≤2h−1,取对数得 h−1≤log2n<h,故 h=⌊log2n⌋+1。
(二)经典例题解析
例题 1:某二叉树有 399 个结点,其中 199 个度为 2 的结点,求叶子结点数。
- 解答:根据 n0=n2+1=199+1=200
例题 2:判断完全二叉树(层序序列 ABCDEFGH)的前序序列。
- 分析:完全二叉树结构如下:
前序遍历顺序:A → B → D → H → E → C → F → G
七、实战应用:判断完全二叉树与销毁二叉树
(一)判断完全二叉树(层序遍历法)
- 算法思路:
- 层序遍历二叉树,遇到第一个空结点后,后续结点必须全为空。
- 用队列记录结点,当某结点为空时,标记 “已进入空结点阶段”,后续若再遇到非空结点,返回 false。
- 代码实现:
bool IsCompleteBinaryTree(BTNode* root) { Queue q; QueueInit(&q); if (root) QueuePush(&q, root); // 根结点入队(非空时) bool hasNull = false; // 是否已遇到空结点 while (!QueueEmpty(&q)) { BTNode* node = QueueFront(&q); QueuePop(&q); if (node == NULL) { hasNull = true; // 标记进入空结点阶段 } else { if (hasNull) { // 空结点之后出现非空结点,非完全二叉树 QueueDestroy(&q); return false; } // 非空结点,左右子结点入队(空结点也需入队,用于标记) QueuePush(&q, node->left); QueuePush(&q, node->right); } } QueueDestroy(&q); return true; }
(二)销毁二叉树(递归释放内存)
- 注意事项:二叉树结点通过动态内存分配创建,需递归释放每个结点及其子树。
- 代码实现:
void DestroyBinaryTree(BTNode** root) { if (*root == NULL) return; // 空树直接返回 // 先销毁左子树和右子树 DestroyBinaryTree(&(*root)->left); DestroyBinaryTree(&(*root)->right); // 释放当前结点内存 free(*root); *root = NULL; // 防止野指针 }
八、总结与拓展方向
(一)核心价值
二叉树是理解树结构的基础,其递归定义和层次特性支撑了大量算法:
- 理论层面:满二叉树与完全二叉树的数学性质为算法分析提供依据。
- 应用层面:堆排序、TOP-K 问题、文件系统目录结构、编译器语法树等均依赖二叉树思想。
(二)拓展学习建议
- 算法题实践:
对称二叉树、二叉树的中序遍历 、 二叉树的前序遍历 、相同的树 、 另一棵树的子树
- 数据结构进阶:
- 二叉搜索树(BST)、AVL 树、红黑树(自平衡二叉树)。
- 哈夫曼树(带权路径最短的二叉树,用于压缩算法)。
- 工程实现:
- 用 C++ 模板实现链式二叉树,支持动态插入、删除、遍历。
- 用 Python 实现堆结构,解决实际场景中的 TOP-K 问题。
附录
二叉树
//Queue.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//typedef int QDataType;
typedef struct BinaryTreeNode* QDataType;
typedef struct QListNode
{
struct QListNode* next;
QDataType data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* front;
QNode* rear;
}Queue;
// 初始化队列
void QueueInit(Queue* q);
// 队尾入队列
void QueuePush(Queue* q, QDataType data);
// 队头出队列
void QueuePop(Queue* q);
// 获取队列头部元素
QDataType QueueFront(Queue* q);
// 获取队列队尾元素
QDataType QueueBack(Queue* q);
// 获取队列中有效元素个数
int QueueSize(Queue* q);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q);
// 销毁队列
void QueueDestroy(Queue* q);
//Tree.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
#include<math.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyBTNode(BTDataType x);
void PreOrder(BTNode* root);
void InOrder(BTNode* root);
void PostOrder(BTNode* root);
int BTNSize(BTNode* root);
int BTLSize(BTNode* root);
int BTLKSize(BTNode* root, int k);
int BTDepth(BTNode* root);
BTNode* BTFind(BTNode* root, BTDataType x);
void BTDestroy(BTNode**root);
void LevelOrder(BTNode* root);
bool BTComplete(BTNode* root);
//Queue.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"
// 初始化队列
void QueueInit(Queue* q)
{
q->front = q->rear = NULL;
}
// 队尾入队列
void QueuePush(Queue* q, QDataType data)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail1");
exit(1);
}
newnode->data = data;
newnode->next = NULL;
if (q->front == NULL)
{
q->front = q->rear = newnode;
}
else
{
q->rear->next = newnode;
q->rear = newnode;
}
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0
bool QueueEmpty(Queue* q)
{
assert(q);
return q->front == NULL;
}
// 队头出队列
void QueuePop(Queue* q)
{
assert(!QueueEmpty(q));
if (q->front == q->rear)
{
free(q->front);
q->front = q->rear = NULL;
}
else
{
QNode* tmp = q->front->next;
free(q->front);
q->front = tmp;
}
}
// 获取队列头部元素
QDataType QueueFront(Queue* q)
{
assert(!QueueEmpty(q));
return q->front->data;
}
// 获取队列队尾元素
QDataType QueueBack(Queue* q)
{
assert(!QueueEmpty(q));
return q->rear->data;
}
// 获取队列中有效元素个数
int QueueSize(Queue* q)
{
assert(q);
int count = 0;
QNode* tmp = q->front;
while (tmp != NULL)
{
count++;
tmp = tmp->next;
}
return count;
}
// 销毁队列
void QueueDestroy(Queue* q)
{
assert(q);
QNode* pcur = q->front;
while (q->front)
{
QNode* tmp = q->front->next;
free(q->front);
q->front = tmp;
}
q->front = q->rear = NULL;
}
//Tree.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Tree.h"
#include"Queue.h"
BTNode* BuyBTNode(BTDataType x)
{
BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));
if (tmp == NULL)
{
perror("malloc fail");
exit(1);
}
tmp->data = x;
tmp->left = tmp->right = NULL;
return tmp;
}
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return ;
}
printf("%c ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return ;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return ;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
int BTNSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return 1 + BTNSize(root->left) + BTNSize(root->right);
}
int BTLSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if ((root->left == NULL) && (root->right == NULL))
{
return 1;
}
return BTLSize(root->left) + BTLSize(root->right);
}
int BTLKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BTLKSize(root->left, k--) + BTLKSize(root->right, k--);
}
int BTDepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int ld = BTDepth(root->left);
int rd = BTDepth(root->right);
return 1 + (ld > rd ? ld : rd);
}
BTNode* BTFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* left=BTFind(root->left,x);
if (left)
{
return left;
}
BTNode* right=BTFind(root->right,x);
if (right)
{
return right;
}
return NULL;
}
void BTDestroy(BTNode** root)
{
if (root == NULL)
{
return;
}
BTDestroy(&((*root)->left));
BTDestroy(&((*root)->right));
free(*root);
*root = NULL;
}
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
QueuePop(&q);
printf("%c ", top->data);
if (top->left)
QueuePush(&q, top->left);
if (top->right)
QueuePush(&q, top->right);
}
QueueDestroy(&q);
}
bool BTComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
QueuePop(&q);
if (top == NULL)
{
break;
}
QueuePush(&q, top->left);
QueuePush(&q, top->right);
}
while (!QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
QueuePop(&q);
if (top != NULL)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
堆
//Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;
int capacity;
}HP;
// 堆的初始
void HPInit(HP* ph);
// 堆的销毁
void HPDestroy(HP* ph);
// 堆的插入
void HPPush(HP* ph, HPDataType x);
// 堆的删除
void HPPop(HP* ph);
// 取堆顶的数据
HPDataType HeapTop(HP* ph);
// 堆的数据个数
int HPSize(HP* ph);
// 堆的判空
int HPEmpty(HP* ph);
//void HPPrint(HP* ph);
void HPPrint(HPDataType* arr, int size);
void HPjustUp(HPDataType* arr, int child);
//void HPjustDown(HPDataType* arr, int n);
void HPjustDown(HPDataType* arr, int parent, int n);
void Swap(HPDataType* a, HPDataType* b);
//Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void HPInit(HP* ph)
{
assert(ph);
ph->arr = NULL;
ph->capacity = ph->size = 0;
}
void HPDestroy(HP* ph)
{
assert(ph);
if (ph->arr == NULL)
free(ph->arr);
ph->arr = NULL;
ph->capacity = ph->size = 0;
}
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void HPjustUp(HPDataType* arr,int child)
{
int parent = (child - 1) / 2;
while (child>0)
{
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* ph, HPDataType x)
{
assert(ph);
if (ph->capacity == ph->size)
{
int newcapacity = ph->capacity == 0 ? 1 : 2 * ph->capacity;
HPDataType* tmp = (HPDataType*)realloc(ph->arr, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("relloc fail");
exit(1);
}
ph->arr = tmp;
ph->capacity = newcapacity;
}
ph->arr[ph->size] = x;
HPjustUp(ph->arr, ph->size);
++ph->size;
}
int HPEmpty(HP* ph)
{
assert(ph);
return ph->size==0;
}
void HPjustDown(HPDataType* arr,int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child] > arr[child + 1])
{
child++;
}
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HPPop(HP* ph)
{
assert(!HPEmpty(ph));
Swap(&ph->arr[0], &ph->arr[ph->size-1]);
ph->size--;
HPjustDown(ph->arr,0 , ph->size);
}
HPDataType HeapTop(HP* ph)
{
assert(!HPEmpty(ph));
return ph->arr[0];
}
int HPSize(HP* ph)
{
assert(!HPEmpty(ph));
return ph->size;
}
//void HPPrint(HP* ph)
//{
// assert(!HPEmpty(ph));
// int tmp = 0;
// while (tmp<ph->size)
// {
// printf("%d ", ph->arr[tmp]);
// tmp++;
// }
// printf("\n");
//}
void HPPrint(HPDataType* arr, int size)
{
int tmp = 0;
while (tmp < size)
{
printf("%d ", arr[tmp]);
tmp++;
}
printf("\n");
}