数据结构之——堆

一、堆的基本概念

堆是计算机科学中一类特殊数据结构的统称。它是一种完全二叉树,即除了最后一层外,其他层都是满的,并且最后一层的节点从左到右依次排列。在堆中,节点的值与父节点的值有特定的关系,从而分为最大堆和最小堆。

最大堆的特性是对于每个节点,它的值都大于或等于其子节点的值。这意味着根节点的值是整个堆中的最大值。例如,在一个整数最大堆中,如果根节点的值为 100,那么它的两个子节点的值都小于或等于 100。

最小堆则相反,对于每个节点,它的值都小于或等于其子节点的值。根节点的值是整个堆中的最小值。

堆的这种特殊结构使得它在许多算法中都有重要的应用。例如,在优先队列的实现中,堆可以快速地插入和删除元素,同时保持队列的优先级顺序。在堆排序算法中,堆可以用来对数据进行高效的排序。

堆的实现通常使用数组来表示完全二叉树。对于数组中的每个位置 i,其左子节点的位置为 2i + 1,右子节点的位置为 2i + 2,父节点的位置为 (i - 1) / 2。这种表示方法使得堆的操作可以在数组上高效地进行。

总之,堆作为一种特殊的数据结构,具有完全二叉树的结构和特定的节点值关系,在计算机科学中有着广泛的应用。

二、堆的存储结构与实现

(一)存储结构介绍

堆可以被看作是一棵树的数组对象。在存储结构上,堆与普通树有一些区别。普通树的存储方式可以有多种,如链式存储、数组存储等。而堆通常采用数组来存储完全二叉树,这样可以利用数组的索引关系快速定位节点的父子关系。

孩子兄弟表示法是一种表示树结构的方式。在这种表示法中,每个节点有两个指针,分别指向其第一个孩子节点和下一个兄弟节点。相比之下,堆采用数组存储时,通过特定的索引计算可以快速找到节点的父子关系。例如,对于数组中的位置 i,其左子节点的位置为 2i+1,右子节点的位置为 2i+2,父节点的位置为 (i-1)/ 2。

堆的这种存储结构使得在进行插入、删除等操作时,可以更高效地进行节点位置的调整,而不需要像链式存储那样进行复杂的指针操作。

(二)实现方法详解

1.结构体创建:可以定义一个结构体来表示堆,结构体中通常包含一个数组用于存储堆中的元素,以及一个表示堆大小的变量。

cpp 复制代码
typedef struct Heap 
{
    int* data;
    int size;
    int capacity;
} Heap;

2.函数声明:声明一些用于操作堆的函数,如初始化堆、插入元素、删除元素等。

cpp 复制代码
void initHeap(Heap* h, int capacity);
void insertHeap(Heap* h, int value);
void deleteHeap(Heap* h);

3.初始化:初始化堆时,分配一定大小的内存空间给数组,并设置堆的初始大小为 0。

cpp 复制代码
void initHeap(Heap* h, int capacity) 
{
    h->data = (int*)malloc(capacity * sizeof(int));
    h->size = 0;
    h->capacity = capacity;
}

4.销毁:在不需要堆时,释放堆所占用的内存空间。

cpp 复制代码
void destroyHeap(Heap* h) 
{
    free(h->data);
    h->size = 0;
    h->capacity = 0;
}

5.插入:插入元素时,首先将元素添加到堆的末尾,然后通过向上调整函数来维护堆的性质。

cpp 复制代码
void insertHeap(Heap* h, int value) 
{
    if (h->size == h->capacity) 
    {
        // 堆已满,需要进行扩容操作
        h->capacity *= 2;
        h->data = (int*)realloc(h->data, h->capacity * sizeof(int));
    }
    h->data[h->size++] = value;
    // 向上调整
    upAdjust(h);
}

向上调整函数的作用是在插入元素后,从插入位置开始向上调整堆,以确保满足堆的性质。具体实现方式是比较当前节点与父节点的值,如果不满足堆的性质,则交换它们的值,然后继续向上调整,直到满足堆的性质为止。

cpp 复制代码
void upAdjust(Heap* h) 
{
    // childIndex 初始化为堆中当前最后一个元素的索引
    int childIndex = h->size - 1;
    // parentIndex 为 childIndex 对应的父节点索引
    int parentIndex = (childIndex - 1) / 2;
    // 将最后一个元素的值保存在 temp 中,用于后续的调整操作
    int temp = h->data[childIndex];
    // 当 childIndex 大于 0(即还没有到达根节点)并且 temp 小于父节点的值时,进行向上调整
    while (childIndex > 0 && temp < h->data[parentIndex]) 
    {
        // 将父节点的值赋给子节点
        h->data[childIndex] = h->data[parentIndex];
        // 更新 childIndex 为父节点的索引
        childIndex = parentIndex;
        // 重新计算新的父节点索引
        parentIndex = (childIndex - 1) / 2;
    }
    // 将 temp(即最初的最后一个元素的值)赋给调整后的位置
    h->data[childIndex] = temp;
}

三、堆的特性与区别

(一)数据结构堆与内存堆区差异

在计算机科学中,数据结构中的堆与内存分配中的堆是不同的概念。

数据结构中的堆是一种特殊的数据结构,通常是完全二叉树的形式,分为最大堆和最小堆。它具有特定的节点值关系,如最大堆中每个节点的值都大于或等于其子节点的值。数据结构堆的特点是可以快速进行插入和删除操作,同时保持特定的顺序。例如,在优先队列的实现中,堆可以快速地插入和删除元素,同时保持队列的优先级顺序。

而内存分配中的堆区是指动态分配内存的区域。在程序运行时,程序员可以通过编程语言提供的函数在堆区分配内存空间。与栈区不同,堆区的内存分配和释放需要程序员手动管理。如果不及时释放不再使用的内存,可能会导致内存泄漏。

与数据结构中的栈相比,数据结构堆是一种树形结构,而栈是一种线性结构。栈遵循后进先出(LIFO)的原则,而堆没有特定的进出顺序,主要是根据堆的性质进行插入和删除操作。

内存分配中的栈区通常用于存储局部变量和函数调用信息,它的内存分配和释放由编译器自动管理,速度较快。而堆区的内存分配相对较慢,但是可以分配较大的内存空间。

(二)与普通树的区别

在节点顺序方面,普通树的节点顺序没有特定的要求,可以是任意的。而堆是一种完全二叉树,除了最后一层外,其他层都是满的,并且最后一层的节点从左到右依次排列。此外,堆中的节点值与父节点的值有特定的关系,最大堆中每个节点的值都大于或等于其子节点的值,最小堆则相反。

在内存占用方面,普通树的存储方式可以有多种,如链式存储、数组存储等。不同的存储方式内存占用情况不同。而堆通常采用数组来存储完全二叉树,这种存储方式可以利用数组的索引关系快速定位节点的父子关系,内存占用相对较为紧凑。

在平衡方面,普通树可以是平衡的,也可以是不平衡的。而堆虽然是完全二叉树,但不一定是平衡的。最大堆和最小堆的平衡是通过节点值的关系来维持的,而不是通过树的高度来平衡。

总的来说,堆与普通树在节点顺序、内存占用和平衡等方面都存在不同之处。这些不同之处使得堆在特定的算法和应用中具有独特的优势。

四、堆的应用场景

(一)优先级队列

优先级队列是一种抽象数据类型,其中每个元素都有一个优先级,高优先级的元素先出队。堆非常适合实现优先级队列,因为它可以快速地插入和删除元素,同时保持队列的优先级顺序。例如,在 Java 中,PriorityQueue就是用堆实现的优先级队列。在定时任务轮训场景中,可以将任务按照优先级放入优先级队列,高优先级的任务先执行。在合并有序小文件场景中,可以将每个小文件的最小元素放入优先级队列,每次取出最小元素写入新文件,从而实现多个有序小文件的合并。

(二)求 Top K 值

利用大顶堆或小顶堆可以找出数组中的最大或最小的前 K 个数。如果要找出最大的前 K 个数,可以使用小顶堆。首先将数组的前 K 个数构建一个小顶堆,然后从第 K + 1 个数开始遍历数组,如果当前元素大于堆顶元素,则将堆顶元素弹出,然后将当前元素插入堆中。遍历结束后,堆中的元素就是最大的前 K 个数。同理,如果要找出最小的前 K 个数,可以使用大顶堆。

(三)求中位数

用大顶堆和小顶堆维护数据可以求出中位数。首先创建一个大顶堆和一个小顶堆,将数据依次添加到两个堆中。如果当前数据小于大顶堆的堆顶元素,则将其添加到大顶堆中;否则,将其添加到小顶堆中。然后平衡两个堆,使得大顶堆的元素个数和小顶堆的元素个数之差不超过 1。如果两个堆的元素个数相等,则中位数为两个堆顶元素的平均值;如果大顶堆的元素个数比小顶堆多 1,则中位数为大顶堆的堆顶元素。

(四)大数据量日志统计搜索排行榜

在大数据量日志统计搜索排行榜中,可以结合散列表和堆来实现。首先,使用散列表统计每个关键词出现的次数。然后,将关键词和出现次数作为一个元组放入一个列表中。接着,使用小顶堆来维护出现次数最多的前 K 个关键词。每次插入一个新的元组时,如果堆的大小小于 K,则直接插入堆中;如果堆的大小等于 K 且新元组的出现次数大于堆顶元组的出现次数,则将堆顶元组弹出,然后插入新元组。这样,堆中的元组就是出现次数最多的前 K 个关键词。

五、总结与展望

堆作为一种特殊的数据结构,具有诸多鲜明的特点。首先,它以完全二叉树的形式呈现,这种结构使得堆在存储和操作上具有一定的优势。通过特定的节点值关系,分为最大堆和最小堆,能够快速进行插入和删除操作,同时保持特定的顺序。

在应用场景方面,堆的表现极为出色。在优先级队列中,它能够确保高优先级的元素先出队,为各种任务调度和资源分配提供了高效的解决方案。例如在定时任务轮训和合并有序小文件场景中,优先级队列的应用大大提高了工作效率。

求 Top K 值也是堆的一个重要应用。通过大顶堆或小顶堆,可以快速找出数组中的最大或最小的前 K 个数,为数据分析和处理提供了有力的工具。

在求中位数方面,大顶堆和小顶堆的配合使用,能够准确地求出中位数,为统计分析提供了便捷的方法。

在大数据量日志统计搜索排行榜中,结合散列表和堆,可以快速找出出现次数最多的前 K 个关键词,为大数据分析提供了有效的手段。

总之,堆在数据结构中具有重要的地位。它的高效性和灵活性使其在各种算法和应用中发挥着关键作用。随着计算机技术的不断发展,尤其是在大数据和实时处理需求不断增长的背景下,堆的应用前景将更加广阔。未来,我们可以期待堆在更多领域的创新应用,为解决复杂的计算问题提供更加高效的解决方案。

相关推荐
Echo_NGC22371 小时前
【神经视频编解码NVC】传统神经视频编解码完全指南:从零读懂 AI 视频压缩的基石
人工智能·深度学习·算法·机器学习·视频编解码
会员果汁1 小时前
leetcode-动态规划-买卖股票
算法·leetcode·动态规划
奋进的芋圆1 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——二进制求和
算法·leetcode·哈希算法·散列表·结构与算法
sxlishaobin2 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20052 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉2 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国2 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882483 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言