自学内容网 自学内容网

数据结构之——堆

一、堆的基本概念

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

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

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

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

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

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

二、堆的存储结构与实现

(一)存储结构介绍

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

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

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

(二)实现方法详解

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

typedef struct Heap 
{
    int* data;
    int size;
    int capacity;
} Heap;

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

void initHeap(Heap* h, int capacity);
void insertHeap(Heap* h, int value);
void deleteHeap(Heap* h);

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

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

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

void destroyHeap(Heap* h) 
{
    free(h->data);
    h->size = 0;
    h->capacity = 0;
}

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

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);
}

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

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 个关键词,为大数据分析提供了有效的手段。

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


原文地址:https://blog.csdn.net/m0_71974552/article/details/143612680

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!