1. 什么是二叉树?
二叉树(Binary Tree)是计算机科学中最基础且最重要的数据结构之一。它是一种树形结构 ,其中每个节点最多有两个子节点,通常称为左子节点和右子节点。

2. 二叉树的基本概念
2.1 节点结构
每个二叉树节点通常包含三个部分:
- 数据域:存储节点的值
- 左指针:指向左子节点
- 右指针:指向右子节点
cs
typedef int BTDataType;
typedef struct BinaryTree
{
struct BinaryTree* left;
struct BinaryTree* right;
BTDataType data;
}BT;
2.2 常见术语
- 根节点:树的顶端节点
- 叶子节点:没有子节点的节点
- 深度:从根节点到该节点的路径长度
- 高度:从该节点到最远叶子节点的路径长度
- 度:节点的子节点个数(二叉树中每个节点的度 ≤ 2)
3. 二叉树的性质
二叉树具有一些重要的数学性质,这些性质是理解和分析二叉树算法的基础。
性质 1:在二叉树的第 i 层上至多有 2^(i-1) 个节点 (i ≥ 1)。
性质 2:深度为 k 的二叉树至多有 2^k - 1 个节点 (k ≥ 1)。
性质 3:对于任何一棵二叉树,如果其叶子节点数为 n0,度为 2 的节点数为 n2,则 n0 = n2 + 1 。(选择题常考)
性质 4:具有 n 个节点的完全二叉树的深度为 ⌊log2n⌋ + 1。
性质 5:对一棵有 n 个节点的完全二叉树按层序编号(从 1 开始),则对任意节点 i (1 ≤ i ≤ n) 有:
如果 i = 1,则节点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲节点是 ⌊i/2⌋。
如果 2i ≤ n,则节点 i 的左孩子是 2i;否则无左孩子。
如果 2i + 1 ≤ n,则节点 i 的右孩子是 2i + 1;否则无右孩子。
这些性质在分析二叉树算法的时间复杂度、空间复杂度以及设计高效算法时非常有用。
4. 二叉树的存储
二叉树的存储方式主要分为两种:顺序存储 和链式存储。选择哪种存储方式取决于具体的应用场景和对操作效率的要求。
4.1 顺序存储
顺序存储使用数组(或列表)来存储二叉树节点。这种存储方式特别适合完全二叉树 或满二叉树,因为可以充分利用数组空间,且能通过下标快速定位父子节点关系。
存储规则:

- 将二叉树的节点按照层序遍历 的顺序依次存入数组中。
- 对于任意节点,如果其在数组中的下标为
i,则:- 其左子节点的下标为
2*i + 1。 - 其右子节点的下标为
2*i + 2。 - 其父节点的下标为
⌊(i-1)/2⌋。
- 其左子节点的下标为
优点:
- 存储紧凑,无指针开销,空间利用率高(对完全二叉树)。
- 通过下标计算即可访问父子节点,访问速度快。
- 适合存储静态二叉树或需要频繁随机访问的场景。
缺点:
- 对于非完全二叉树,数组中间会出现大量空位,造成空间浪费。
- 插入和删除节点可能涉及大量数据移动,效率较低。
4.2 链式存储
链式存储是二叉树最常用、最灵活的存储方式。每个节点通过指针(或引用)连接其左右子节点。
优点:
- 结构灵活,能高效表示任意形状的二叉树(包括普通二叉树、斜树等)。
- 插入和删除节点只需修改指针,效率高。
- 动态分配内存,无需预先确定树的最大规模。
缺点:
- 每个节点需要额外的指针空间,存储开销较大。
- 访问节点需要遍历指针链,随机访问效率低于顺序存储。
- 可能存在内存碎片问题。
4.3 存储方式的选择
在实际应用中,应根据具体需求选择合适的存储方式:
| 场景 | 推荐存储方式 | 理由 |
|---|---|---|
| 完全二叉树/满二叉树,且规模固定 | 顺序存储 | 空间利用率高,访问速度快,代码简单。 |
| 需要频繁插入、删除节点的动态二叉树 | 链式存储 | 操作灵活,无需移动大量数据。 |
| 堆(优先队列)的实现 | 顺序存储(数组) | 符合堆的完全二叉树性质,能高效进行上浮/下沉操作。 |
理解这两种存储方式是实现二叉树各种算法(如遍历、查找、插入、删除)的基础。在后续的遍历算法和面试题实现中,我们将主要使用链式存储,因为它更通用,更能体现二叉树的递归特性。
5. 二叉树的遍历算法
在二叉树的遍历中,始终要将 一棵 二叉树 分为 根节点+左子树+右子树,而子树继续分为 根节点+左子树+右子树,一直细分直至遍历结束。
在遍历算法中,主要是利用递归的思想。
这里我们还未接触二叉树的真正创建方式,在这里我们先手搓一棵二叉树来方便下面的遍历。
cs
//二叉树的创建
BT* BuyNode(BTDataType x)
{
BT* newnode = (BT*)malloc(sizeof(BT));
if (newnode == NULL)
{
perror("malloc failed!");
return 0;
}
newnode->data = x;
newnode->left = newnode->right = NULL;
return newnode;
}
BT* root = BuyNode(1);
BT* Node1 = root->left = BuyNode(2);
BT* Node2 = root->right = BuyNode(3);
BT* Node3 = Node1->left = BuyNode(4);
BT* Node5 = Node3->left = BuyNode(5);
BT* Node6 = Node3->right = BuyNode(6);
BT* Node4 = Node2->right = BuyNode(7);
5.1 深度优先遍历(DFS)
5.1.1 前序遍历(根-左-右)
cs
// 二叉树前序遍历
void PreOrder(BT* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
5.1.2 中序遍历(左-根-右)
cs
// 二叉树中序遍历
void InOrder(BT* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
5.1.3 后序遍历(左-右-根)
cs
// 二叉树后序遍历
void PostOrder(BT* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
5.2 广度优先遍历(BFS)
5.2.1 层序遍历
思路:使用队列 "先进先出" 的特性,在将 根节点 入队时,将其的左孩子和右孩子同时入队,直至队列为空,则遍历完成。
cs
//队尾入队
void QPush(Que* pst,QDataType node)
{
assert(pst);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc failed!");
return;
}
newnode->data = node;
newnode->next = NULL;
if (pst->phead ==NULL)//这里不能使用 pst->phead == pst->ptail来判断,当只有一个元素,这个条件同样成立
{
pst->phead = pst->ptail = newnode;
}
else
{
pst->ptail->next = newnode;
pst->ptail = newnode;
}
pst->size++;
}
//队头出队
void QPop(Que* pst)
{
assert(pst);
if (pst->phead->next == NULL)
{
free(pst->phead);
pst->phead = pst->ptail = NULL;
}
else
{
QNode* next = pst->phead->next;
free(pst->phead);
pst->phead = next;
}
pst->size--;
}
//队列的初始化
void QInit(Que* pst)
{
assert(pst);
pst->phead = pst->ptail = NULL;
pst->size = 0;
}
//队列的判空
bool QEmpty(Que* pst)
{
assert(pst);
return pst->size == 0;
}
//取队头元素
QDataType QTop(Que* pst)
{
assert(pst);
assert(pst->size > 0);
return pst->phead->data;
}
//二叉树的层序遍历
void LevelOrder(BT* root)
{
Que queue;
QInit(&queue);
QPush(&queue, root);
while (!QEmpty(&queue))
{
BT* cur = QTop(&queue);
QPop(&queue);
printf("%d ", cur->data);
if(cur->left != NULL)
QPush(&queue, cur->left);
if(cur->right != NULL)
QPush(&queue, cur->right);
}
}
5.3 二叉树的其他算法
5.3.1 二叉树结点个数
cs
// 二叉树结点个数
int BinaryTreeSize1(BT* root, int* psize)
{
if (root == NULL)
return 0;
else
{
++(*psize);
BinaryTreeSize1(root->left,psize);
BinaryTreeSize1(root->right, psize);
}
return *psize;
}
// 二叉树结点个数
int BinaryTreeSize2(BT* root)
{
static int size = 0; //使用静态变量时,调用第二次会持续累加
if (root == NULL)
return 0;
else
{
++size;
BinaryTreeSize2(root->left);
BinaryTreeSize2(root->right);
}
return size;
}
// 二叉树结点个数
int size = 0;//使用全局变量
int BinaryTreeSize3(BT* root)
{
if (root == NULL)
return 0;
else
{
++size;
BinaryTreeSize3(root->left);
BinaryTreeSize3(root->right);
}
return size;
}
// 二叉树结点个数
//将二叉树看做 根节点+左子树+右子树
int BinaryTreeSize4(BT* root)
{
if (root == NULL)
return 0;
return BinaryTreeSize4(root->left)+ BinaryTreeSize4(root->right)+1;
}
5.3.2二叉树叶子结点个数
cs
// 二叉树叶子结点个数
int BinaryTreeLeafSize(BT* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
5.3.3二叉树第k层结点个数
cs
// 二叉树第k层结点个数
int BinaryTreeLevelKSize(BT* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
5.3.4 二叉树查找值为x的结点
cs
// 二叉树查找值为x的结点
BT* BinaryTreeFind(BT* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BT* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
return ret1;
return BinaryTreeFind(root->right, x);
}
5.3.5 二叉树的高度
cs
//二叉树的高度
int BinaryHight(BT* root)
{
if (root == NULL)
return 0;
int LeftHight = BinaryHight(root->left);
int RightHight = BinaryHight(root->right);
return LeftHight > RightHight ? LeftHight + 1 : RightHight + 1;
}
5.3.6二叉树的销毁
cs
//二叉树的销毁
void BinaryDestroy(BT* root)
{
if (root == NULL)
return;
BinaryDestroy(root->left);
BinaryDestroy(root->right);
free(root);//函数调用之后要及时置空,或者直接使用二级指针作为参数在本函数体内将其置空
}
5. 总结
二叉树是数据结构与算法学习的基石,掌握其基本概念、遍历方法和常见变种对于编程能力的提升至关重要。通过实际编码练习来加深理解,从简单的递归遍历开始,逐步挑战更复杂的二叉树问题。