数据结构:二叉树

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. 总结

二叉树是数据结构与算法学习的基石,掌握其基本概念、遍历方法和常见变种对于编程能力的提升至关重要。通过实际编码练习来加深理解,从简单的递归遍历开始,逐步挑战更复杂的二叉树问题。

相关推荐
2401_862880821 小时前
数据结构 --- 算法
数据结构·算法
wuminyu1 小时前
JDK21中FFM api的upcall回调机制解析
java·linux·c语言·jvm·c++
个 人 练 习 生2 小时前
数据结构链表:带头双向循环链表
c语言·数据结构·经验分享·学习·其他·链表
不灭的黄金瞳1232 小时前
编译与链接
c语言
我变成萤火虫8 小时前
河南萌新联赛2026第(四)场:南阳理工学院
数据结构·c++·算法·贪心算法·stl·动态规划
疯狂打码的少年16 小时前
【数据结构】图的遍历:深度优先搜索(DFS)
数据结构·笔记·算法·深度优先
智者知已应修善业18 小时前
【51单片机数码管逐位显示1-8余位不亮】2024-10-23
c语言·经验分享·笔记·嵌入式硬件·51单片机
caimouse19 小时前
ReactOS 图形系统分析(47):GDI 批处理 — gdibatch.c
c语言·开发语言
土司大王19 小时前
LeetCode hot100——除了自身以外数组的乘积
数据结构·算法·leetcode