C语言实现顺序结构二叉树-堆

news2024/9/22 7:25:09

在这里插入图片描述

文章目录

  • 🎯引言
  • 👓C语言实现顺序结构二叉树-堆
    • 1.树的概念与结构
      • 1.1概念与结构
      • 1.2树的相关术语
    • 2.二叉树
      • 2.1概念与结构
      • 2.2特殊的二叉树
        • 2.2.1满二叉树
        • 2.2.2完全二叉树
    • 3.二叉树的存储结构
      • 3.1顺序存储
      • 3.2链式存储
    • 4.堆的实现
      • 4.1堆的概念与结构
      • 4.2向上调增算法
      • 4.3向下调整算法
      • 4.3堆的实现(小堆)
    • 5.堆的应用
      • 5.1堆排序
      • 5.2TOP-K问题
  • 🥇 结语

🎯引言

欢迎来到HanLop博客的C语言数据结构初阶系列。在这个系列中,我们将深入探讨各种基本的数据结构和算法,帮助您打下坚实的编程基础。在本篇文章中,我们将讲解顺序结构的二叉树,特别是堆(Heap)。堆是一种特殊的完全二叉树,它满足堆属性,即每个节点的值都大于或等于(或小于或等于)其子节点的值。堆在许多算法中起着至关重要的作用,例如优先队列的实现和堆排序。在这篇文章中,我们将介绍堆的基本概念、堆的创建和操作方法,以及其在实际编程中的应用。通过一些实际的代码示例,您将更好地掌握堆在C语言中的实现和应用,从而为后续学习更复杂的数据结构和算法打下坚实的基础。

👓C语言实现顺序结构二叉树-堆

1.树的概念与结构

1.1概念与结构

在计算机科学中,树是一种重要的非线性数据结构。树由一系列节点组成,每个节点包含一个值以及指向其他节点的链接。树具有层级结构,即每个节点可以有多个子节点,但只有一个父节点(根节点除外)。树的这种层级结构使得它在许多应用中非常有用,例如文件系统、数据库索引和表达式解析等。

之所以将它取名为树,是因为它形似一颗现实中倒挂的树,图示:

在这里插入图片描述

1.2树的相关术语

在深入学习树的数据结构之前,了解相关术语是非常重要的。以下是一些关键术语及其定义:

  1. 根节点(Root Node):树的顶层节点,没有父节点。每棵树只有一个根节点。
  2. 子节点(Child Node):由另一个节点(父节点)指向的节点。
  3. 父节点(Parent Node):指向一个或多个子节点的节点。
  4. 兄弟节点(Sibling Node):具有相同父节点的节点。
  5. 叶子节点(Leaf Node):没有子节点的节点,位于树的最底层。
  6. 分支节点(Internal Node):至少有一个子节点的节点。
  7. 路径(Path):从一个节点到另一个节点所经过的节点序列。
  8. 边(Edge):连接两个节点的线,表示父子关系。
  9. 路径长度(Path Length):路径上所包含的边的数量。
  10. 深度(Depth):节点到根节点的路径长度。根节点的深度为0。
  11. 高度(Height):节点到叶子节点的最长路径长度。叶子节点的高度为0。
  12. 层级(Level):树中所有深度相同的节点组成的集合。根节点在第1层,根节点的子节点在第2层,以此类推。
  13. 子树(Subtree):以某个节点为根的其他所有节点及其连接的节点构成的树。
  14. 度(Degree):节点的度是其子节点的数量,树的度是所有节点中最大的度。
  15. 森林(Forest):由多棵互不相交的树组成的集合。
  16. 祖先节点(Ancestor Node):一个节点的所有前代节点
  17. 子孙节点(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. 每个节点最多有两个子节点

每个节点最多有两个子节点,分别称为左子节点和右子节点。这是二叉树区别于其他树结构的一个重要特点。

  1. 递归定义

二叉树具有递归的性质。每个子树本身也是一个二叉树。即,每个节点及其子节点形成的结构也可以看作是一棵二叉树。

  1. 子树有序

在二叉树中,节点的左子树和右子树是有序的。

2.2特殊的二叉树

2.2.1满二叉树

满二叉树的概念:

满二叉树(Full Binary Tree) 是一种特殊类型的二叉树,其中每个节点要么没有子节点(即叶子节点),要么有两个子节点。换句话说,在满二叉树中,所有非叶子节点都有两个子节点。

图示:
在这里插入图片描述

相关数学计算(根层次为1)

  1. 节点数量

    • 如果一棵满二叉树的高度为 h,则总节点数量 N 可以通过以下公式计算: N = 2 h − 1 N = 2^h - 1 N=2h1 则满二叉树深度为: 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} 2i1(第 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++2h1

      使用等比数列求和公式:

      N = 2 h − 1 2 − 1 = 2 h − 1 N = \frac{2^h - 1}{2 - 1} = 2^h - 1 N=212h1=2h1

  2. 叶子节点数量

    • 在满二叉树中,叶子节点的数量 L 与分支节点的数量 I 之间的关系为: L = I + 1 L = I + 1 L=I+1
    • 解释:由于每个非叶子节点都有两个子节点,因此总的子节点数量是 2 × I 2 \times I 2×I,而每个叶子节点都不产生新的子节点,因此树的叶子节点数是分支节点数加1。
  3. 每层节点数

    • 在满二叉树中,第 i 层的节点数为 2 i − 1 2^{i-1} 2i1 ,其中 i 从1开始。
    • 解释:第1层只有1个节点(根节点),第2层有2个节点,第3层有4个节点,以此类推。
2.2.2完全二叉树

完全二叉树详解(根层次从1开始)

完全二叉树(Complete Binary Tree) 是一种特殊类型的二叉树,具有以下特点:

  1. 层次性质:
    • 除了最后一层外,完全二叉树的每一层都是满的。
    • 最后一层的节点从左至右排列,没有空缺。

图示:

在这里插入图片描述

数学公式解释

  1. 节点数量
    • 完全二叉树中,如果高度为 h(根节点层次从1开始),则节点总数 N 介于 2 h − 1 和 2 h − 1 2^{h-1} 和 2^h −1 2h12h1之间。
    • 具体公式为: 2 h − 1 ≤ N ≤ 2 h − 1 2^{h-1} \leq N \leq 2^h −1 2h1N2h1其中,h 是完全二叉树的高度。
  2. 树的高度
    • 树的高度 h 与节点数量 N 的关系可以表示为: h = ⌈ l o g ⁡ 2 ( N + 1 ) ⌉ h=⌈log⁡2(N+1)⌉ h=log⁡2(N+1)⌉
    • 解释:这是因为在完全二叉树中,每一层的节点数是2的幂次。其中N看成是满二叉树时的节点个数,因为满二叉树和完全二叉树只是最后一层的节点个数有所不同,但高度都是相同的
  3. 最后一层的节点数
    • 假设最后一层是第 h 层,则最后一层的节点数 L 可以通过以下公式计算: L = N − ( 2 h − 1 − 1 ) L= N - (2^{h-1} - 1) L=N(2h11)
    • 解释:总节点数 N 减去满二叉树中 h−1 层的节点数,即为最后一层的节点数。

示例

考虑以下完全二叉树:

       A
      / \
     B   C
    / \ / \
   D  E F

在这棵树中:

  • 层次性质

    • 除了最后一层外,所有层的节点都是满的。
    • 最后一层的节点集中在最左边(D、E、F)。
  • 节点数量

    • 总节点数为6。
  • 树的高度

    • 使用公式 h = ⌈ l o g ⁡ 2 ( N + 1 ) ⌉ : h=⌈log⁡2(N+1)⌉: h=log⁡2(N+1)⌉

      h = ⌈ l o g ⁡ 2 ( 6 + 1 ) ⌉ = ⌈ l o g ⁡ 2 ( 7 ) ⌉ = 3 h=⌈log⁡2(6+1)⌉=⌈log⁡2(7)⌉=3 h=log⁡2(6+1)⌉=log⁡2(7)⌉=3

  • 最后一层的节点数

    • 使用公式 L = N − ( 2 h − 1 − 1 ) : L=N - (2^{h-1} - 1): L=N(2h11)

      L = 6 − ( 2 3 − 1 − 1 ) = 6 − 3 = 3 L= 6 - (2^{3-1} - 1) = 6 - 3 = 3 L=6(2311)=63=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 2i1(若根节点在索引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) 是一种特殊的完全二叉树,具有以下性质:

  1. 堆性质:
    • 对于任何一个节点 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 2i1(若根节点在索引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* a1HeapDatatype* a2 指向要交换的两个元素。
  • 操作:
    • 使用一个临时变量 temp 进行交换操作,确保 a1a2 之间的值交换成功。

向上调整算法

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最小值的问题。

算法思路

  1. 初始化一个大小为K的小顶堆:将前K个元素插入堆中。
  2. 遍历剩余元素:
    • 如果当前元素大于堆顶元素,则用当前元素替换堆顶元素,并进行堆的调整,使得堆顶仍然是堆中最小的元素。
  3. 最终堆中的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博客,期待在下一篇文章中继续与您探讨更多有趣且实用的编程知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1944961.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI+HPC 部署优化面试范围分享

背景 最近几年生成式AI技术和自动驾驶技术发展发展很快&#xff0c;这些行业对于算法的运行效率有很高的要求&#xff0c;尤其一个模型在训练完成后运行到设备上&#xff0c;需要大量的工作&#xff0c;包括模型的剪枝、蒸馏、压缩、量化、算子优化、系统优化等。 对于传统的…

浏览器的最大并发数(http1.1)

HTTP/1.1&#xff1a;每个资源请求通常需要单独的TCP连接&#xff0c;尽管支持Keep-Alive机制&#xff0c;允许在同一个TCP连接上连续发送多个请求。但通常浏览器限制并发TCP连接数&#xff08;例如&#xff0c;每个域名最多6个并发连接&#xff09;。 HTTP/2&#xff1a;引入…

C++:函数对象和函数调用运算符(函数、函数指针、lambda函数对象、bind创建的对象、重载了函数调用符的类对象)

函数对象和函数调用运算符 A.What&#xff08;函数对象&#xff09; 如果类定义了函数调用运算符&#xff0c;则该类的对象称为函数对象 其中重载的运算符operator()被称为函数调用运算符 B.Which&#xff08;有哪些可调用函数对象&#xff09; 函数&#xff1a;一般函数&…

UE4-打包游戏,游戏模式,默认关卡

一.打包游戏 注意windows系统无法打包苹果系统的执行包&#xff0c;只能使用苹果系统打包。 打包完之后是一个.exe文件。 打包要点&#xff1a; 1.确定好要操控的角色和生成位置。 2.设置默认加载的关卡和游戏模式。 在这个界面可以配置游戏的默认地图和游戏的模式&#xff0c;…

springboot+webSocket对接chatgpt

webSocket对接参考 话不多说直接上代码 WebSocket package com.student.config;import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springfram…

C#知识|账号管理系统:实现修改管理员登录密码

哈喽,你好啊,我是雷工! 本节主要记录实现修改管理员登录密码的后端逻辑及相关功能,以下为学习笔记。 01 实现逻辑 ①:首先输入原密码,验证,验证通过然后可以输入新密码进行修改; ②:新密码修改为了避免输入失误导致输入的密码与自己以为修改的密码不符的情况,增加了…

Langchain核心模块与实战[7]:专业级Prompt工程调教LLM[输入输出接口、提示词模板与例子选择器的协同工程]

Langchain核心模块与实战[7]:专业级Prompt工程调教LLM[输入输出接口、提示词模板与例子选择器的协同工程] 1. 大模型IO接口 任何语言模型应用的核心元素是…模型的输入和输出。LangChain提供了与任何语言模型进行接口交互的基本组件。 提示 prompts : 将模型输入模板化、动态…

**卷积神经网络典型CNN**

LeNet&#xff1a;最早用于数字识别的CNN AlexNet&#xff1a;2012年ILSVRC比赛冠军&#xff0c;远超第二名的CNN&#xff0c;比LeNet更深&#xff0c;用多层小卷积叠加来替换单个的大卷积 ZF Net&#xff1a;2013ILSVRC冠军 GoogleNet&#xff1a;2014ILSVRC冠军 VGGNet&a…

go语言Gin框架的学习路线(十一)

目录 GORM的CRUD教程 更新操作 更新所有字段 更新指定字段 使用 Select 和 Omit 更新 无 Hooks 更新 批量更新 删除操作 删除记录 批量删除 软删除 物理删除 示例代码 GORM的CRUD教程 CRUD 是 "Create, Read, Update, Delete"&#xff08;创建、查询、…

百度,有道,谷歌翻译API

API翻译 百度&#xff0c;有道&#xff0c;谷歌API翻译&#xff08;只针对中英相互翻译&#xff09;,其他语言翻译需要对应from&#xff0c;to的code 百度翻译 package fills.tools.translate; import java.util.ArrayList; import java.util.HashMap; import java.util.Lis…

windows服务器启动apache失败,提示请通过cmd命令行启动:net start apache

Windows Server 2012 R2服务器突然停止运行apche&#xff0c;启动apache失败&#xff0c;提示请通过cmd命令行启动:net start apache 1.报错截图&#xff1a; 进入服务里输入命令启动也不行&#xff0c;提示由于登录失败而无法启动服务。 2.问题原因&#xff1a; 服务器www用…

数据库(MySQL)-DQL数据查询语言

DQL(Data Query Language 数据查询语言)的用途是查询数据库数据&#xff0c;如select语句。其中&#xff0c;可以根据表的结构和关系分为单表查询和多表联查。 单表查询 单表查询&#xff1a;针对数据库中的一张数据表进行查询 全字段查询 语法&#xff1a;select 字段名 fro…

User Allocation In MEC: A DRL Approach 论文笔记

论文&#xff1a;ICWS 2021 移动边缘计算中的用户分配&#xff1a;一种深度强化学习方法 代码地址&#xff1a;使用强化学习在移动边缘计算环境中进行用户分配 目录 Ⅰ.Introduction II. MOTIVATION-A.验证假设的观察结果 II. MOTIVATION-A Motivating Example 数据驱动…

我在百科荣创企业实践——简易函数信号发生器(5)

对于高职教师来说,必不可少的一个任务就是参加企业实践。这个暑假,本人也没闲着,报名参加了上海市电子信息类教师企业实践。7月8日到13日,有幸来到美丽的泉城济南,远离了上海的酷暑,走进了百科荣创科技发展有限公司。在这短短的一周时间里,我结合自己的教学经验和企业的…

C#,.NET常见算法

1.递归算法 1.1.C#递归算法计算阶乘的方法 using System;namespace C_Sharp_Example {public class Program{/// <summary>/// 阶乘&#xff1a;一个正整数的阶乘Factorial是所有小于以及等于该数的正整数的积&#xff0c;0的阶乘是1&#xff0c;n的阶乘是n&#xff0…

【学习笔记】无人机系统(UAS)的连接、识别和跟踪(五)-无人机跟踪

目录 引言 5.3 无人机跟踪 5.3.1 无人机跟踪模型 5.3.2 无人机位置报告流程 5.3.3 无人机存在监测流程 引言 3GPP TS 23.256 技术规范&#xff0c;主要定义了3GPP系统对无人机&#xff08;UAV&#xff09;的连接性、身份识别、跟踪及A2X&#xff08;Aircraft-to-Everyth…

Samtec技术科普小课堂 | 一文入门射频连接器~

【摘要/前言】 在本文中&#xff0c;我们将回到基础知识&#xff0c;了解一下什么是射频连接器。如果您是信号完整性专家&#xff0c;请点击阅读原文访问我们的网站视频&#xff0c;通过我们的网络研讨会视频了解教科书上可能找不到的知识。 如果您是电气工程领域的新手&#…

Prometheus 监控Tomcat等java应用的状态

5月应用服务出现问题&#xff0c;当别的小伙伴问我&#xff0c;有没有Tomcat等应用状态的监控的时候&#xff0c;我有点儿尴尬。所以赶紧抽空部署一下。 在配置之前&#xff0c;就当已经会安装jdk和tomcat了。 一、下载jmx_exporter #linux下 cd /usr/local/prometheus wget …

新增ClamAV病毒扫描功能、支持Java和Go运行环境,1Panel开源面板v1.10.12版本发布

2024年7月19日&#xff0c;现代化、开源的Linux服务器运维管理面板1Panel正式发布了v1.10.12版本。 在这一版本中&#xff0c;1Panel新增了多项实用功能。社区版方面&#xff0c;1Panel新增ClamAV病毒扫描功能、支持Java和Go运行环境&#xff0c;同时1Panel还新增了文件编辑器…

浪潮GS企业管理软件GetChildFormAndEntityList存在反序列化漏洞

一、漏洞简介 浪潮GS 面向大中型集团企业采用SOA 架构和先进开放的GSP 应用中间件开发,形成了集团管控13 大领域15 大行业60余个细分行业的解决方案。在管理方面,浪潮GS 有效帮助企业有效实现财务集中管理、资金集中管理、资产集中管理、供应链集中管理,从而达到集团信息的…