文章目录
🎯引言
欢迎来到HanLop博客的C语言数据结构初阶系列。在这个系列中,我们将深入探讨各种基本的数据结构和算法,帮助您打下坚实的编程基础。在本篇文章中,我们将讲解顺序结构的二叉树,特别是堆(Heap)。堆是一种特殊的完全二叉树,它满足堆属性,即每个节点的值都大于或等于(或小于或等于)其子节点的值。堆在许多算法中起着至关重要的作用,例如优先队列的实现和堆排序。在这篇文章中,我们将介绍堆的基本概念、堆的创建和操作方法,以及其在实际编程中的应用。通过一些实际的代码示例,您将更好地掌握堆在C语言中的实现和应用,从而为后续学习更复杂的数据结构和算法打下坚实的基础。
👓C语言实现顺序结构二叉树-堆
1.树的概念与结构
1.1概念与结构
在计算机科学中,树是一种重要的非线性数据结构。树由一系列节点组成,每个节点包含一个值以及指向其他节点的链接。树具有层级结构,即每个节点可以有多个子节点,但只有一个父节点(根节点除外)。树的这种层级结构使得它在许多应用中非常有用,例如文件系统、数据库索引和表达式解析等。
之所以将它取名为树,是因为它形似一颗现实中倒挂的树,图示:
1.2树的相关术语
在深入学习树的数据结构之前,了解相关术语是非常重要的。以下是一些关键术语及其定义:
- 根节点(Root Node):树的顶层节点,没有父节点。每棵树只有一个根节点。
- 子节点(Child Node):由另一个节点(父节点)指向的节点。
- 父节点(Parent Node):指向一个或多个子节点的节点。
- 兄弟节点(Sibling Node):具有相同父节点的节点。
- 叶子节点(Leaf Node):没有子节点的节点,位于树的最底层。
- 分支节点(Internal Node):至少有一个子节点的节点。
- 路径(Path):从一个节点到另一个节点所经过的节点序列。
- 边(Edge):连接两个节点的线,表示父子关系。
- 路径长度(Path Length):路径上所包含的边的数量。
- 深度(Depth):节点到根节点的路径长度。根节点的深度为0。
- 高度(Height):节点到叶子节点的最长路径长度。叶子节点的高度为0。
- 层级(Level):树中所有深度相同的节点组成的集合。根节点在第1层,根节点的子节点在第2层,以此类推。
- 子树(Subtree):以某个节点为根的其他所有节点及其连接的节点构成的树。
- 度(Degree):节点的度是其子节点的数量,树的度是所有节点中最大的度。
- 森林(Forest):由多棵互不相交的树组成的集合。
- 祖先节点(Ancestor Node):一个节点的所有前代节点
- 子孙节点(Descendant Node):一个节点的所有后代节点
注:树的高度或深度就是树中节点的最大层级
例子说明
考虑以下二叉树:
A
/ \
B C
/ \ \
D E F
在这棵树中:
- 根节点是A。
- B和C是A的子节点,A是B和C的父节点。
- D和E是B的子节点,B是D和E的父节点。
- F是C的子节点,C是F的父节点。
- D、E和F是叶子节点,因为它们没有子节点。
- A是第1层,B和C是第2层,D、E和F是第3层。
- 节点B的深度是2,节点D的深度是3。
- 节点D的高度是0,节点B的高度是1,节点A的高度是2。
- 整棵树的高度是3。
2.二叉树
2.1概念与结构
二叉树是一种特殊的树形数据结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。
图示:
二叉树的特点:
1. 每个节点最多有两个子节点
每个节点最多有两个子节点,分别称为左子节点和右子节点。这是二叉树区别于其他树结构的一个重要特点。
- 递归定义
二叉树具有递归的性质。每个子树本身也是一个二叉树。即,每个节点及其子节点形成的结构也可以看作是一棵二叉树。
- 子树有序
在二叉树中,节点的左子树和右子树是有序的。
2.2特殊的二叉树
2.2.1满二叉树
满二叉树的概念:
满二叉树(Full Binary Tree) 是一种特殊类型的二叉树,其中每个节点要么没有子节点(即叶子节点),要么有两个子节点。换句话说,在满二叉树中,所有非叶子节点都有两个子节点。
图示:
相关数学计算(根层次为1)
-
节点数量:
-
如果一棵满二叉树的高度为 h,则总节点数量 N 可以通过以下公式计算: N = 2 h − 1 N = 2^h - 1 N=2h−1 则满二叉树深度为: h = ⌈ l o g 2 ( N + 1 ) ⌉ h = \lceil log_2 (N+1) \rceil h=⌈log2(N+1)⌉
-
解释:树的层数为 h,因为根节点层次从1开始,层数是 h(从第1层到第 h 层)。每层的节点数是 2 i − 1 2^{i-1} 2i−1(第 i 层),因此:
N = 2 0 + 2 1 + 2 2 + ⋯ + 2 h − 1 N = 2^0 + 2^1 + 2^2 + \cdots + 2^{h-1} N=20+21+22+⋯+2h−1
使用等比数列求和公式:
N = 2 h − 1 2 − 1 = 2 h − 1 N = \frac{2^h - 1}{2 - 1} = 2^h - 1 N=2−12h−1=2h−1
-
-
叶子节点数量:
- 在满二叉树中,叶子节点的数量 L 与分支节点的数量 I 之间的关系为: L = I + 1 L = I + 1 L=I+1
- 解释:由于每个非叶子节点都有两个子节点,因此总的子节点数量是 2 × I 2 \times I 2×I,而每个叶子节点都不产生新的子节点,因此树的叶子节点数是分支节点数加1。
-
每层节点数:
- 在满二叉树中,第 i 层的节点数为 2 i − 1 2^{i-1} 2i−1 ,其中 i 从1开始。
- 解释:第1层只有1个节点(根节点),第2层有2个节点,第3层有4个节点,以此类推。
2.2.2完全二叉树
完全二叉树详解(根层次从1开始)
完全二叉树(Complete Binary Tree) 是一种特殊类型的二叉树,具有以下特点:
- 层次性质:
- 除了最后一层外,完全二叉树的每一层都是满的。
- 最后一层的节点从左至右排列,没有空缺。
图示:
数学公式解释
- 节点数量:
- 完全二叉树中,如果高度为 h(根节点层次从1开始),则节点总数 N 介于 2 h − 1 和 2 h − 1 2^{h-1} 和 2^h −1 2h−1和2h−1之间。
- 具体公式为: 2 h − 1 ≤ N ≤ 2 h − 1 2^{h-1} \leq N \leq 2^h −1 2h−1≤N≤2h−1其中,h 是完全二叉树的高度。
- 树的高度:
- 树的高度 h 与节点数量 N 的关系可以表示为: h = ⌈ l o g 2 ( N + 1 ) ⌉ h=⌈log2(N+1)⌉ h=⌈log2(N+1)⌉
- 解释:这是因为在完全二叉树中,每一层的节点数是2的幂次。其中N看成是满二叉树时的节点个数,因为满二叉树和完全二叉树只是最后一层的节点个数有所不同,但高度都是相同的
- 最后一层的节点数:
- 假设最后一层是第 h 层,则最后一层的节点数 L 可以通过以下公式计算: L = N − ( 2 h − 1 − 1 ) L= N - (2^{h-1} - 1) L=N−(2h−1−1)
- 解释:总节点数 N 减去满二叉树中 h−1 层的节点数,即为最后一层的节点数。
示例
考虑以下完全二叉树:
A
/ \
B C
/ \ / \
D E F
在这棵树中:
-
层次性质:
- 除了最后一层外,所有层的节点都是满的。
- 最后一层的节点集中在最左边(D、E、F)。
-
节点数量:
- 总节点数为6。
-
树的高度:
-
使用公式 h = ⌈ l o g 2 ( N + 1 ) ⌉ : h=⌈log2(N+1)⌉: h=⌈log2(N+1)⌉:
h = ⌈ l o g 2 ( 6 + 1 ) ⌉ = ⌈ l o g 2 ( 7 ) ⌉ = 3 h=⌈log2(6+1)⌉=⌈log2(7)⌉=3 h=⌈log2(6+1)⌉=⌈log2(7)⌉=3
-
-
最后一层的节点数:
-
使用公式 L = N − ( 2 h − 1 − 1 ) : L=N - (2^{h-1} - 1): L=N−(2h−1−1):
L = 6 − ( 2 3 − 1 − 1 ) = 6 − 3 = 3 L= 6 - (2^{3-1} - 1) = 6 - 3 = 3 L=6−(23−1−1)=6−3=3
-
3.二叉树的存储结构
3.1顺序存储
顺序存储是一种利用数组存储二叉树节点的方法。节点的存储位置遵循一定的规则,使得树的结构可以通过数组的索引来表示。
特点
- 空间利用率高:适用于完全二叉树或接近完全二叉树的情况,因为节点位置与数组索引一一对应。
- 方便的节点访问:可以直接通过数组索引访问节点,时间复杂度为 O(1)。
存储规则
- 根节点存储在数组的第一个位置(索引为0)。
- 对于数组中位置为 i的节点:
- 左子节点的位置为 2 i + 1 2i+1 2i+1(若根节点在索引0处)。
- 右子节点的位置为 2 i + 2 2i+2 2i+2(若根节点在索引0处)。
- 父节点的位置为 ⌊ i − 1 2 ⌋ \left\lfloor \frac{i-1}{2} \right\rfloor ⌊2i−1⌋(若根节点在索引0处)。
图示:
优缺点
- 优点:
- 直接访问节点,速度快。
- 适用于完全二叉树或接近完全二叉树的情况。
- 缺点:
- 对于非完全二叉树,会浪费存储空间,因为数组中会有空闲位置。
- 插入和删除操作复杂,需要调整数组中节点的位置。
3.2链式存储
链式存储是一种利用链表存储二叉树节点的方法。每个节点包含数据和指向其左子节点和右子节点的指针。
特点
- 灵活的内存使用:适用于任何形状的二叉树,不会浪费存储空间。
- 插入和删除操作方便:只需要调整指针,不需要移动大量数据。
存储结构
每个节点包含以下内容:
- 数据域:存储节点的数据。
- 左指针域:指向左子节点。
- 右指针域:指向右子节点。
示例
考虑以下二叉树:
A
/ \
B C
/ \ \
D E F
链式存储法的节点结构为:
struct TreeNode {
char data;
struct TreeNode *left;
struct TreeNode *right;
};
优缺点
- 优点:
- 内存利用率高,适用于任何形状的二叉树。
- 插入和删除操作灵活,只需调整指针。
- 缺点:
- 访问节点的时间复杂度为 O(h),其中 hhh 是树的高度。
- 需要额外的存储空间来存储指针。
对比与选择
- 顺序存储适用于完全二叉树或接近完全二叉树的情况,具有快速的节点访问速度,但在非完全二叉树中会浪费存储空间。
- 链式存储适用于任何形状的二叉树,具有灵活的内存使用和方便的插入、删除操作,但节点访问速度较慢,需要额外的指针存储空间。
4.堆的实现
4.1堆的概念与结构
堆(Heap) 是一种特殊的完全二叉树,具有以下性质:
- 堆性质:
- 对于任何一个节点 iii,堆中节点的值总是满足:其父节点的值小于等于(最小堆)或大于等于(最大堆)其子节点的值。
根据堆性质,堆分为两种类型:
- 最小堆(Min-Heap):根节点的值是所有节点中最小的,每个父节点的值都小于等于其子节点的值。
- 最大堆(Max-Heap):根节点的值是所有节点中最大的,每个父节点的值都大于等于其子节点的值。
堆的结构
堆是一种完全二叉树,因此它可以使用数组进行顺序存储。节点在数组中的位置遵循一定的规则:
- 根节点存储在数组的第一个位置(索引0)。
- 对于数组中位置为 i的节点:
- 左子节点的位置为 2 i + 1 2i+1 2i+1(若根节点在索引0处)。
- 右子节点的位置为 2 i + 2 2i+2 2i+2(若根节点在索引0处)。
- 父节点的位置为 ⌊ i − 1 2 ⌋ \left\lfloor \frac{i-1}{2} \right\rfloor ⌊2i−1⌋(若根节点在索引0处)。
示例:
- 父节点=(1-1)/2=0
- 左孩子节点=2*1+1=3
- 右孩子节点=2*1+2=4
堆的实现:
4.2向上调增算法
(以小堆为列,父节点的值都小于其子节点)
当我们插入在堆的尾部插入一个小于根节点的数据时,我们怎么样把这个插入后的结构继续维持成小堆呢?
图解:
向上调整算法是为了在插入新元素后恢复堆的性质。以下是详细的代码解析:
// 向上调整算法
typedef int HeapDatatype;
void Swap(HeapDatatype* a1, HeapDatatype* a2)
{
HeapDatatype temp = *a1;
*a1 = *a2;
*a2 = temp;
}
void AdjustUp(HeapDatatype* a, int child) {
// 若孩子节点为 i,则父节点为 (i-1)/2
int parent = (child - 1) / 2;
// 当 child 不在根节点且 child 的值小于其父节点的值时,进行调整
while (child > 0) {
// 如果当前 child 的值小于其父节点的值,则交换这两个节点
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
// 更新 child 和 parent 的位置,继续向上调整
child = parent;
parent = (child - 1) / 2;
} else {
// 如果当前 child 的值不小于其父节点的值,则调整完毕,跳出循环
break;
}
}
}
代码解析
parent = (child - 1) / 2;
:计算当前节点的父节点位置。while (child > 0)
:只要 child 不是根节点(索引0)就继续调整。if (a[child] < a[parent])
:如果 child 的值小于其父节点的值,交换它们的位置。child = parent; parent = (child - 1) / 2;
:更新 child 和 parent 的位置,继续向上调整。else { break; }
:如果 child 的值不小于其父节点的值,则调整完毕,跳出循环。
4.3向下调整算法
在使用向下调整算法维持小堆的时候,必须保证根节点的左右子树都为小堆
图解:
向下调整算法是为了在删除堆顶元素后恢复堆的性质。以下是详细的代码解析:
// 向下调整算法
typedef int HeapDatatype;
void Swap(HeapDatatype* a1, HeapDatatype* a2)
{
HeapDatatype temp = *a1;
*a1 = *a2;
*a2 = temp;
}
void AdjustDown(HeapDatatype* a, int n, int parent) {
// 若父节点为 i,则左孩子为 2i+1
int child = parent * 2 + 1;
// 当 child 在堆的范围内时,继续调整
while (child < n) {
// 找到左右孩子节点中较小的那一个
if (child + 1 < n && a[child + 1] < a[child]) {
child++;
}
// 如果当前 child 的值小于其父节点的值,则交换这两个节点
if (a[child] < a[parent]) {
Swap(&a[child], &a[parent]);
// 更新 parent 和 child 的位置,继续向下调整
parent = child;
child = parent * 2 + 1;
} else {
// 如果当前 child 的值不小于其父节点的值,则调整完毕,跳出循环
break;
}
}
}
代码解析
child = parent * 2 + 1;
:计算当前节点的左孩子位置。while (child < n)
:只要 child 在堆的范围内就继续调整。if (child + 1 < n && a[child + 1] < a[child])
:如果右孩子存在且右孩子的值小于左孩子的值,选择右孩子。if (a[child] < a[parent])
:如果 child 的值小于其父节点的值,交换它们的位置。parent = child; child = parent * 2 + 1;
:更新 parent 和 child 的位置,继续向下调整。else { break; }
:如果 child 的值不小于其父节点的值,则调整完毕,跳出循环。
4.3堆的实现(小堆)
Heap.h源码
//Heap.h文件中
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
typedef int HeapDatatype;
typedef struct Heap
{
HeapDatatype* arr;
int size;
int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* hp);
//堆的销毁
void HeapDestory(Heap* hp);
//交换数据
void Swap(HeapDatatype* a1, HeapDatatype* a2);
//堆的插入
void HeapPush(Heap* hp, HeapDatatype x);
//取堆顶的数据
HeapDatatype HeapTop(Heap* hp);
//判空
bool HeapEmpty(Heap* hp);
//求堆的存储的数量
int HeapSize(Heap* hp);
//删除堆顶的数据
void HeapPop(Heap* hp);
//向上调整算法
void AdjustUp(HeapDatatype* a, int child);
//向下调整算法 左右子树都是小堆 对堆顶进行向下调整算法
void AdjustDown(HeapDatatype* a, int n, int parent);
Heap.c源码
//Heap.c
#include "Heap.h"
//堆的初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->arr = NULL;
hp->capacity = hp->size = 0;
}
void Swap(HeapDatatype* a1, HeapDatatype* a2)
{
HeapDatatype temp = *a1;
*a1 = *a2;
*a2 = temp;
}
//向上调整算法
void AdjustUp(HeapDatatype* a, int child)
{
//若孩子节点为i 则parent=(i-1)/2
//若父节点为i 则左孩子为 2i+1 友孩子为2i+2
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;
}
}
}
//向下调整算法
void AdjustDown(HeapDatatype* 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;
}
}
}
//堆的插入
void HeapPush(Heap* hp, HeapDatatype x)
{
assert(hp);
//检查容量
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HeapDatatype* temp = (HeapDatatype*)realloc(hp->arr,sizeof(HeapDatatype) * newcapacity);
if (temp == NULL)
{
perror("malloc fail");
exit(1);
}
hp->capacity = newcapacity;
hp->arr = temp;
}
hp->arr[hp->size] = x;
//插入完数据要保证堆的性质,进行向下或者向上调整算法
AdjustUp(hp->arr, hp->size);
hp->size++;
}
//判空
bool HeapEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
//取堆顶数据
HeapDatatype HeapTop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
return hp->arr[0];
}
//求堆的存储的数量
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
//删除堆顶的数据
void HeapPop(Heap* hp)
{
assert(hp);
assert(!HeapEmpty(hp));
//将堆顶数据和最后一个数据交换
Swap(&hp->arr[0], &hp->arr[hp->size-1]);
hp->size--;
//对堆顶进行向下调整算法
AdjustDown(hp->arr, hp->size, 0);
}
//堆的销毁
void HeapDestory(Heap* hp)
{
assert(hp);
free(hp->arr);
hp->arr = NULL;
hp->capacity = hp->size = 0;
}
函数实现详解:
堆的初始化
void HeapInit(Heap* hp) {
assert(hp);
hp->arr = NULL;
hp->capacity = hp->size = 0;
}
- 功能: 初始化堆结构。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 将数组指针设置为
NULL
,表示当前没有分配任何内存。 - 将堆的容量和大小初始化为0。
- 将数组指针设置为
元素交换函数
void Swap(HeapDatatype* a1, HeapDatatype* a2) {
HeapDatatype temp = *a1;
*a1 = *a2;
*a2 = temp;
}
- 功能: 交换两个元素的值。
- 参数:
HeapDatatype* a1
和HeapDatatype* a2
指向要交换的两个元素。 - 操作:
- 使用一个临时变量
temp
进行交换操作,确保a1
和a2
之间的值交换成功。
- 使用一个临时变量
向上调整算法
void AdjustUp(HeapDatatype* 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;
}
}
}
- 功能: 向上调整堆,保证堆的性质。
- 参数:
HeapDatatype* a
是数组指针,int child
是当前需要调整的孩子节点索引。 - 操作:
- 计算父节点索引
(child - 1) / 2
。 - 当孩子节点存在且其值小于父节点时,交换孩子节点与父节点的值,并更新孩子和父节点的索引,继续向上调整。
- 如果孩子节点不小于父节点,结束调整。
- 计算父节点索引
向下调整算法
void AdjustDown(HeapDatatype* 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;
}
}
}
- 功能: 向下调整堆,保证堆的性质。
- 参数:
HeapDatatype* a
是数组指针,int n
是堆的大小,int parent
是当前需要调整的父节点索引。 - 操作:
- 计算左孩子节点索引
parent * 2 + 1
。 - 在左右孩子节点中找到较小的一个孩子节点,如果孩子节点值小于父节点值,交换它们。
- 更新父节点和孩子节点索引,继续向下调整。
- 如果孩子节点不小于父节点,结束调整。
- 计算左孩子节点索引
堆的插入
void HeapPush(Heap* hp, HeapDatatype x) {
assert(hp);
if (hp->size == hp->capacity) {
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
HeapDatatype* temp = (HeapDatatype*)realloc(hp->arr, sizeof(HeapDatatype) * newcapacity);
if (temp == NULL) {
perror("malloc fail");
exit(1);
}
hp->capacity = newcapacity;
hp->arr = temp;
}
hp->arr[hp->size] = x;
AdjustUp(hp->arr, hp->size);
hp->size++;
}
- 功能: 向堆中插入新元素。
- 参数:
Heap* hp
指向堆结构体的指针,HeapDatatype x
是要插入的元素。 - 操作:
- 检查是否需要扩展堆的容量,如果当前容量不足,分配新的内存空间。
- 将新元素放在堆的末尾位置。
- 通过向上调整算法恢复堆的性质。
- 增加堆的大小。
判空
bool HeapEmpty(Heap* hp) {
assert(hp);
return hp->size == 0;
}
- 功能: 判断堆是否为空。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 返回堆的大小是否为0,若为0则堆为空。
获取堆顶元素
HeapDatatype HeapTop(Heap* hp) {
assert(hp);
assert(!HeapEmpty(hp));
return hp->arr[0];
}
- 功能: 获取堆顶元素。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 返回堆顶元素,堆顶元素始终是数组的第一个元素。
获取堆的大小
int HeapSize(Heap* hp) {
assert(hp);
return hp->size;
}
- 功能: 获取堆中元素的数量。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 返回堆的大小。
删除堆顶元素
void HeapPop(Heap* hp) {
assert(hp);
assert(!HeapEmpty(hp));
Swap(&hp->arr[0], &hp->arr[hp->size - 1]);
hp->size--;
AdjustDown(hp->arr, hp->size, 0);
}
- 功能: 删除堆顶元素。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 将堆顶元素与最后一个元素交换。
- 减少堆的大小。
- 通过向下调整算法恢复堆的性质。
销毁堆
void HeapDestory(Heap* hp) {
assert(hp);
free(hp->arr);
hp->arr = NULL;
hp->capacity = hp->size = 0;
}
- 功能: 释放堆所占用的内存,并将堆的指针和大小重置为初始状态。
- 参数:
Heap* hp
指向堆结构体的指针。 - 操作:
- 释放堆数组的内存。
- 将堆的数组指针设为
NULL
,容量和大小设为0。
5.堆的应用
5.1堆排序
堆排序是一种基于堆(通常是二叉堆)的排序算法,具有O(n log n)的时间复杂度。它包括两个主要步骤:建堆和排序。下面是详细的堆排序实现及解释。
堆排序代码
void HeapSort(int* arr, int n) {
// 通过向下调整算法建堆
// 向下调整建堆时间复杂度O(n)
int i;
for (i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(arr, n, i);
}
// 升序建大堆
// 降序建小堆
while (n > 0) {
Swap(&arr[0], &arr[n - 1]);
n--;
AdjustDown(arr, n, 0);
}
}
建堆阶段
for (i = (n - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(arr, n, i);
}
- 目标: 构建一个大堆或小堆(看向下调整算法是如何写的)。
- 操作:
- 从最后一个非叶子节点开始,向上逐个节点进行向下调整。
- 计算方式
(n - 1 - 1) / 2
确定最后一个非叶子节点索引。 - 调用
AdjustDown
函数,从最后一个非叶子节点向根节点方向调整,确保每个子树都满足堆的性质。 - 这个过程的时间复杂度是O(n)。
排序阶段
while (n > 0) {
Swap(&arr[0], &arr[n - 1]);
n--;
AdjustDown(arr, n, 0);
}
- 目标: 排序数组。
- 操作:
- 将堆顶元素(小堆最小值或大堆最大值)与当前堆的最后一个元素交换。
- 减少堆的大小(忽略已经排序的最后一个元素)。
- 对新的堆顶元素进行向下调整,恢复堆的性质。
- 重复上述步骤,直到所有元素都排序完成。
5.2TOP-K问题
TOP-K问题是指从一个数据流或一个大的数据集合中,找出前K个最大或最小的元素。堆是一种有效的数据结构,可以很好地解决这个问题。使用小顶堆可以解决TOP-K最大值的问题,而使用大顶堆可以解决TOP-K最小值的问题。
算法思路
- 初始化一个大小为K的小顶堆:将前K个元素插入堆中。
- 遍历剩余元素:
- 如果当前元素大于堆顶元素,则用当前元素替换堆顶元素,并进行堆的调整,使得堆顶仍然是堆中最小的元素。
- 最终堆中的K个元素即为前K个最大的元素。
这种方法的时间复杂度是O(N log K),其中N是数据集合的大小,K是需要找出的前K个元素。
小顶堆实现TOP-K问题
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 交换函数
void Swap(int* a1, int* a2) {
int temp = *a1;
*a1 = *a2;
*a2 = temp;
}
// 向下调整算法
void AdjustDown(int* arr, int n, int parent) {
int child = parent * 2 + 1;
while (child < n) {
// Find the smaller child
if (child + 1 < n && arr[child + 1] < arr[child]) {
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
} else {
break;
}
}
}
// Function to find top K elements using min-heap
void TopK(int* arr, int n, int k) {
if (k <= 0 || n <= 0) return;
// Step 1: 初始化一个大小为K的小顶堆。
int* heap = (int*)malloc(sizeof(int) * k);
//Step 2:使用前K个元素构建初始堆。
for (int i = 0; i < k; ++i) {
heap[i] = arr[i];
}
for (int i = (k - 2) / 2; i >= 0; --i) {
AdjustDown(heap, k, i);
}
// Step 3: 遍历剩余元素,如果当前元素大于堆顶元素,用当前元素替换堆顶元素并进行堆调整。
for (int i = k; i < n; ++i) {
if (arr[i] > heap[0]) {
heap[0] = arr[i];
AdjustDown(heap, k, 0);
}
}
// Step 4:最终堆中包含前K个最大的元素,输出这些元素。
printf("Top %d elements are: ", k);
for (int i = 0; i < k; ++i) {
printf("%d ", heap[i]);
}
printf("\n");
free(heap);
}
int main() {
int arr[] = {3, 2, 1, 5, 6, 4, 8, 9, 10, 7};
int n = sizeof(arr) / sizeof(arr[0]);
int k = 4;
TopK(arr, n, k);
return 0;
}
TopK函数
void TopK(int* arr, int n, int k) {
if (k <= 0 || n <= 0) return;
int* heap = (int*)malloc(sizeof(int) * k);
for (int i = 0; i < k; ++i) {
heap[i] = arr[i];
}
for (int i = (k - 2) / 2; i >= 0; --i) {
AdjustDown(heap, k, i);
}
for (int i = k; i < n; ++i) {
if (arr[i] > heap[0]) {
heap[0] = arr[i];
AdjustDown(heap, k, 0);
}
}
printf("Top %d elements are: ", k);
for (int i = 0; i < k; ++i) {
printf("%d ", heap[i]);
}
printf("\n");
free(heap);
}
- 功能: 找到数组中前K个最大的元素。
- 参数:
int* arr
: 数组指针。int n
: 数组的大小。int k
: 要找的前K个元素。
- 操作:
- 初始化一个大小为K的小顶堆。
- 使用前K个元素构建初始堆。
- 遍历剩余元素,如果当前元素大于堆顶元素,用当前元素替换堆顶元素并进行堆调整。
- 最终堆中包含前K个最大的元素,输出这些元素。
🥇 结语
通过本篇文章的学习,相信您已经对顺序结构二叉树——堆有了深入的了解。我们探讨了堆的基本概念、如何在C语言中创建和操作堆,以及其在实际编程中的应用。掌握堆的实现和应用不仅可以提升您的数据结构知识,还能为解决实际问题提供强有力的工具。希望这些知识能为您在编程道路上提供帮助,并为后续学习其他复杂的数据结构和算法打下坚实的基础。感谢您关注HanLop博客,期待在下一篇文章中继续与您探讨更多有趣且实用的编程知识。