数据结构之堆——算法与数据结构入门笔记(六)

简介: 数据结构之堆——算法与数据结构入门笔记(六)

f8b2b59133134bdbb21101207cb05bd5.png

本文是算法与数据结构的学习笔记第六篇,将持续更新,欢迎小伙伴们阅读学习。有不懂的或错误的地方,欢迎交流


引言


当涉及到高效的数据存储和检索时,堆(Heap)是一种常用的数据结构。上一篇文章中介绍了树和完全二叉树,堆就是一个完全二叉树,可以分为最大堆和最小堆两种类型。在这篇博客中,我们将深入探讨堆的概念、特点、常见应用、操作以及实现。


什么是堆?


在计算机科学中,堆是一种具有特殊属性的树形数据结构。堆通常被用来实现优先队列(Priority Queue),它允许快速地找到具有最高(或最低)优先级的元素。


堆的特点


堆的主要特点如下:


1.堆是一种完全二叉树结构,即除了最后一层外,其他层的节点都是满的,并且最后一层的节2.点从左到右依次填满,不能有间隔。在最大堆中,每个节点的值都大于或等于其子节点的值。根节点的值是最大的。在最小堆中,每个节点的值都小于或等于其子节点的值。根节点的值是最小的。

3.堆通常被表示为一个数组,可以通过索引直接访问堆中的元素,堆的根节点通常是数组中的第一个元素。

4.堆的插入和删除操作的时间复杂度都为 O ( log ⁡ n ) O(\log n)O(logn),其中 n nn 是堆中元素的数量。


堆的应用


堆在计算机科学中有广泛的应用,其中一些主要应用包括:


1.堆排序

堆排序是一种高效的排序算法,它利用堆的性质进行排序。它的时间复杂度为 O ( n log ⁡ n ) O(n\log n)O(nlogn),并且具有原地排序的特性。

2.队列

堆可以实现高效的优先级队列,允许以常数时间复杂度找到具有最高优先级的元素,并支持快速的插入和删除操作。

3.Top K 问题

在一组元素中,查找前 K 个最大(或最小)的元素是一个常见的问题。使用堆可以高效地解决这个问题,通过维护一个大小为 K 的最小堆或最大堆,可以快速地找到前 K 个元素。

4.图算法

在图算法中,堆常用于实现最短路径算法(如Dijkstra算法)和最小生成树算法(如Prim和Kruskal算法)。

5.数据流中的中位数

对于一个不断变化的数据流,查找其中的中位数也是一个常见的问题。使用两个堆(一个最大堆和一个最小堆),可以高效地实现对数据流中的中位数的查找。



为什么使用数组实现堆


用数组来实现树相关的数据结构也许看起来有点古怪,但是它在时间和空间上都是很高效的。

我们准备将上面图中的大根堆这样存储:

50, 45, 40, 20, 25, 35, 30, 10, 15 ]

就这么多!我们除了一个简单的数组以外,不需要任何额外的空间。

如果我们不允许使用指针,那么我们怎么知道哪一个节点是父节点,哪一个节点是它的子节点呢?问得好!节点在数组中的位置 index 和它的父节点以及子节点的索引之间有一个映射关系。

如果 i 是节点的索引,那么下面的公式就给出了它的父节点和子节点在数组中的位置:

parent(i) = floor((i - 1)/2)
left(i)   = 2i + 1
right(i)  = 2i + 2

注意:right(i) 就是简单的 left(i) + 1。左右节点总是处于相邻的位置。

我们将这些公式放到前面的例子中验证一下。

1690469231303.png


注意:根节点(50)没有父节点,因为 -1 不是一个有效的数组索引。同样,节点(25),(35),(30),(10)和(15)没有子节点,因为这些索引已经超过了数组的大小,所以我们在使用这些索引值的时候需要保证是有效的索引值。

复习一下,在最大堆中,父节点的值总是要大于(或者等于)其子节点的值。这意味下面的公式对数组中任意一个索引 i 都成立:

array[parent(i)] >= array[i]


可以用上面的例子来验证一下这个堆属性。

如你所见,这些公式允许我们不使用指针就可以找到任何一个节点的父节点或者子节点。


堆的基本操作


以下是堆的一些基本操作:

1.插入:将一个元素插入到堆中,并保持堆的特性。

2.删除根节点:删除堆的根节点,并保持堆的特性。

3.获取根节点:获取堆的根节点的值,通常是堆中最大或最小的值。

4.堆化(Heapify):对一个无序的数组进行堆化操作,将其转换为一个堆。


C语言


以下是使用C语言实现堆(包括创建堆、插入数据、删除根结点、获取根节点和堆化等基础操作)的示例代码:

#include <stdio.h>
#define MAX_HEAP_SIZE 100
typedef struct {
    int heap[MAX_HEAP_SIZE];
    int size;
} Heap;
void initializeHeap(Heap *h) {
    h->size = 0;
}
void insert(Heap *h, int value) {
    if (h->size >= MAX_HEAP_SIZE) {
        printf("Heap is full.\n");
        return;
    }
    int i = h->size;
    h->heap[i] = value;
    h->size++;
    // 调整堆的结构
    while (i > 0 && h->heap[(i - 1) / 2] < h->heap[i]) {
        int temp = h->heap[i];
        h->heap[i] = h->heap[(i - 1) / 2];
        h->heap[(i - 1) / 2] = temp;
        i = (i - 1) / 2;
    }
}
int removeRoot(Heap *h) {
    if (h->size <= 0) {
        printf("Heap is empty.\n");
        return -1;
    }
    int root = h->heap[0];
    h->size--;
    h->heap[0] = h->heap[h->size];
    // 调整堆的结构
    int i = 0;
    while (2 * i + 1 < h->size) {
        int leftChild = 2 * i + 1;
        int rightChild = 2 * i + 2;
        int largerChild = leftChild;
        if (rightChild < h->size && h->heap[rightChild] > h->heap[leftChild]) {
            largerChild = rightChild;
        }
        if (h->heap[i] >= h->heap[largerChild]) {
            break;
        }
        int temp = h->heap[i];
        h->heap[i] = h->heap[largerChild];
        h->heap[largerChild] = temp;
        i = largerChild;
    }
    return root;
}
void heapify(Heap *h, int arr[], int n) {
    initializeHeap(h);
    // 将数组元素逐个插入堆中
    for (int i = 0; i < n; i++) {
        insert(h, arr[i]);
    }
}
int getRoot(Heap *h) {
    if (h->size <= 0) {
        printf("Heap is empty.\n");
        return -1;
    }
    return h->heap[0];
}
int main() {
    Heap h;
    initializeHeap(&h);
    insert(&h, 5);
    insert(&h, 2);
    insert(&h, 10);
    insert(&h, 8);
    int root = removeRoot(&h);
    printf("Root: %d\n", root);
    int arr[] = {9, 4, 7, 1, 3};
    int arrSize = sizeof(arr) / sizeof(arr[0]);
    heapify(&h, arr, arrSize);
    printf("Root: %d\n", getRoot(&h));
    return 0;
}


结论


堆是一种重要的数据结构,它提供了高效的数据存储和检索方式。我们可以使用数组来实现堆,并实现插入、删除和堆化等操作。堆在排序、优先队列、Top K 问题、图算法以及中位数查找等方面具有广泛的应用。

希望这篇博客能够帮助你理解堆的概念、应用和实现。

相关文章
|
11天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
26 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
69 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
29天前
|
存储 算法 Java
Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性
Java Set因其“无重复”特性在集合框架中独树一帜。本文解析了Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定数据结构和算法确保元素唯一性,并提供了最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的hashCode()与equals()方法。
32 4
|
1月前
|
算法 API 计算机视觉
人脸识别笔记(一):通过yuface调包(参数量54K更快更小更准的算法) 来实现人脸识别
本文介绍了YuNet系列人脸检测算法的优化和使用,包括YuNet-s和YuNet-n,以及通过yuface库和onnx在不同场景下实现人脸检测的方法。
33 1
|
1月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
57 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
67 1
|
1月前
|
算法
❤️算法笔记❤️-(每日一刷-160、相交链表)
❤️算法笔记❤️-(每日一刷-160、相交链表)
17 1
|
1月前
|
数据可视化 搜索推荐 Python
Leecode 刷题笔记之可视化六大排序算法:冒泡、快速、归并、插入、选择、桶排序
这篇文章是关于LeetCode刷题笔记,主要介绍了六大排序算法(冒泡、快速、归并、插入、选择、桶排序)的Python实现及其可视化过程。
13 0