堆(Heap)的原理与C++实现

news2025/2/6 21:43:10

1. 什么是堆?

堆(Heap)是一种特殊的树形数据结构,通常用于实现优先队列。堆可以分为两种类型:

  • 最大堆(Max Heap):每个节点的值都大于或等于其子节点的值。
  • 最小堆(Min Heap):每个节点的值都小于或等于其子节点的值。

堆通常是一个完全二叉树,这意味着除了最后一层,其他层都是完全填满的,并且最后一层的节点都尽可能地靠左排列。

2. 堆的性质

  • 完全二叉树:堆是一个完全二叉树,这意味着它可以用数组来高效地表示。
  • 堆序性质:在最大堆中,父节点的值总是大于或等于其子节点的值;在最小堆中,父节点的值总是小于或等于其子节点的值。

Tips: 堆是完全二叉树,并非二叉搜索树

在数据结构中,完全二叉树二叉搜索树是两种常见的树形结构,它们虽然都属于二叉树的范畴,但在定义、性质和应用场景上有显著的区别。下面我们将详细分析它们的区别。

特性完全二叉树二叉搜索树
定义节点从上到下、从左到右依次填充左子树 < 根节点 < 右子树
有序性不一定有序中序遍历结果有序
结构要求必须是完全填充的(最后一层靠左)无特殊结构要求,只需满足有序性
查找效率不支持高效查找支持高效查找(平衡时 O(log n))
插入和删除通常用于堆操作,不支持动态插入删除支持动态插入和删除
应用场景堆、优先队列查找、排序、数据库索引
数组表示可以用数组高效表示通常用指针或引用表示

完全二叉树示例

        10
       /  \
      5    15
     / \   /
    2   7 12
  • 节点从上到下、从左到右依次填充。
  • 最后一层的节点靠左排列。

二叉搜索树示例

        10
       /  \
      5    15
     / \   / \
    2   7 12 20
  • 左子树的所有节点值小于根节点,右子树的所有节点值大于根节点。
  • 中序遍历结果为 [2, 5, 7, 10, 12, 15, 20],是一个有序序列。

3. 堆的操作

堆的主要操作包括:

  • 插入(Insert):将一个新元素插入堆中,并保持堆的性质。
  • 删除(Delete):删除元素,并保持堆的性质。
  • 查询(Query):查询堆顶元素
  • 构建堆(Build Heap):将一个无序数组构建成一个堆。

4. 堆的实现

堆通常使用数组来实现。在从数组下标0开始存储的堆,对于一个索引为 i 的节点:

  • 其父节点的索引为 (i - 1) / 2
  • 其左子节点的索引为 2 * i + 1
  • 其右子节点的索引为 2 * i + 2

4.1 堆的性质维护

堆的插入过程

假设我们有一个最大堆,初始堆为:[100, 19, 36, 17, 3, 25, 1, 2, 7],其对应的完全二叉树结构如下(用数组表示):

        100
      /     \
    19       36
   /  \     /  \
  17   3   25   1
 / \
2  7

插入一个新元素40
将新元素添加到堆的末尾:堆的数组表示为 [100, 19, 36, 17, 3, 25, 1, 2, 7, 40],对应的完全二叉树结构如下:

     100
    /      \
   19      36
  /   \    / \
 17    3  25  1
/ \    /
2  7  40
  1. 向上调整(上浮):从新插入的节点开始,与其父节点比较。如果当前节点的值大于父节点的值,则交换它们的位置。
  • 40 的父节点是 3,40 > 3,交换它们的位置:
        100
      /      \
     19       36
    /  \     /  \
   17   40  25   1
  / \   /
 2  7  3
  • 40 的新父节点是 19,40 > 19,交换它们的位置:
      100
    /      \
   40       36
  /  \     /  \
17   19  25   1
/ \   /
2  7 3
  • 40 的新父节点是 100,40 < 100,停止调整。
  • 最终,插入 40 后的堆为:[100, 40, 36, 17, 19, 25, 1, 2, 7, 3]

总结:堆在插入元素后,需要进行上浮(up)操作,是不断与父节点比较,若父节点小于当前节点,则交换位置。具体代码实现示例如下:

//存储下标从1开始,以大根堆为例
void heap_up(int idx){
    while(idx != 1){
        int parent = idx >> 1;
        if(heap[parent] < heap[idx]){
            swap(heap[parent], heap[idx]);
            idx = parent;
        }
        else{
            break;
        }
    }
}
堆的删除过程

假设我们从上述堆中删除最大值(堆顶元素 100)。

  1. 将堆顶元素与最后一个元素交换:交换 100 和 3 的位置,得到 [3, 40, 36, 17, 19, 25, 1, 2, 7, 100],然后删除最后一个元素(100),得到 [3, 40, 36, 17, 19, 25, 1, 2, 7]。这是因为我们在用数组存储堆的时候,头部元素的删除面临整个数组的移动,相当消耗计算资源,于是我们选择将头部元素和尾部元素进行交换,进行删除尾部,再调整堆
  2. 向下调整(下沉):从堆顶开始,比较当前节点与其子节点的值,将当前节点与较大的子节点交换,直到满足堆的性质。
  • 当前堆顶是 3,其子节点是 40 和 36,40 > 36,选择 40 与 3 交换得到:
      40
    /      \
   3       36
  /  \     /  \
17   19  25   1
/ \  
2  7 
  • 3 的新位置是左子树的根,其子节点是 17 和 19,19 > 17,选择 19 与 3 交换:
      40
    /      \
   19       36
  /  \     /  \
17   3  25   1
/ \  
2  7 
  • 最终,删除 100 后的堆为:[40, 19, 36, 17, 3, 25, 1, 2, 7]
void heap_down(int idx){
    int size = top;
    while(1){
        int leftChild = idx * 2;
        int rightChild = idx * 2 + 1;
        int largest = idx;
        if(leftChild < size && heap[largest] < heap[leftChild]){
            largest = leftChild;
        }
        if(rightChild < size && heap[largest] < heap[rightChild]){
            largest = rightChild;
        }
        if (largest != idx) {
            swap(heap[idx], heap[largest]);
            idx = largest;
        } else {
            break;
        }
    }
}

4.2 C++ 实现最大堆

这里展示一个封装成对象的大根堆

#include <iostream>
#include <vector>
#include <algorithm>

class MaxHeap {
private:
    std::vector<int> heap;

    void heapifyUp(int index) {
        while (index > 0) {
            int parentIndex = (index - 1) / 2;
            if (heap[index] > heap[parentIndex]) {
                std::swap(heap[index], heap[parentIndex]);
                index = parentIndex;
            } else {
                break;
            }
        }
    }

    void heapifyDown(int index) {
        int size = heap.size();
        while (true) {
            int leftChild = 2 * index + 1;
            int rightChild = 2 * index + 2;
            int largest = index;

            if (leftChild < size && heap[leftChild] > heap[largest]) {
                largest = leftChild;
            }
            if (rightChild < size && heap[rightChild] > heap[largest]) {
                largest = rightChild;
            }

            if (largest != index) {
                std::swap(heap[index], heap[largest]);
                index = largest;
            } else {
                break;
            }
        }
    }

public:
    void insert(int value) {
        heap.push_back(value);
        heapifyUp(heap.size() - 1);
    }

    int extractMax() {
        if (heap.empty()) {
            throw std::out_of_range("Heap is empty");
        }
        int maxValue = heap[0];
        heap[0] = heap.back();
        heap.pop_back();
        heapifyDown(0);
        return maxValue;
    }

    void buildHeap(const std::vector<int>& array) {
        heap = array;
        for (int i = (heap.size() / 2) - 1; i >= 0; --i) {
            heapifyDown(i);
        }
    }

    void printHeap() const {
        for (int value : heap) {
            std::cout << value << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MaxHeap maxHeap;
    maxHeap.insert(10);
    maxHeap.insert(20);
    maxHeap.insert(15);
    maxHeap.insert(30);
    maxHeap.insert(40);

    std::cout << "Max Heap: ";
    maxHeap.printHeap();

    std::cout << "Extracted Max: " << maxHeap.extractMax() << std::endl;
    std::cout << "Max Heap after extraction: ";
    maxHeap.printHeap();

    std::vector<int> array = {5, 3, 8, 1, 9};
    maxHeap.buildHeap(array);
    std::cout << "Max Heap built from array: ";
    maxHeap.printHeap();

    return 0;
}

4.2 代码解析

  • heapifyUp:用于在插入新元素后,从下往上调整堆,确保堆的性质。
  • heapifyDown:用于在删除堆顶元素后,从上往下调整堆,确保堆的性质。
  • insert:将新元素插入堆中,并调用 heapifyUp 进行调整。
  • extractMax:删除并返回堆顶元素,然后调用 heapifyDown 进行调整。
  • buildHeap:将一个无序数组构建成一个堆。

5. 堆的应用

  • 优先队列:堆是实现优先队列的理想数据结构,因为可以快速获取和删除最大或最小元素。
  • 堆排序:堆排序是一种基于堆的比较排序算法,时间复杂度为 O(n log n)。
  • Dijkstra算法:在图的单源最短路径算法中,堆用于高效地选择下一个要处理的节点。

6. 总结

堆是一种非常高效的数据结构,特别适用于需要频繁获取最大或最小元素的场景。通过数组实现堆,可以充分利用其完全二叉树的性质,使得插入、删除和构建堆的操作都能在 O(log n) 的时间内完成。

7.练习

ACWing模拟堆
这个题相较普通的堆,增添了一个需要维护的对象,就是这个数字是第几次插入的。所以我们需要额外维护两个数组posinv_pos,分别表示第k个插入的数在堆数组中的索引,和堆数组中第i个数是第几个插入的,完整代码如下:

#include <iostream>
#include <cstring>
#include <string>
using namespace std;
const int maxn = 1e5 + 7;
int heap[maxn], top;
int pos[maxn], inv_pos[maxn];
void heap_swap(int a, int b){
    swap(heap[a], heap[b]);
    swap(pos[inv_pos[a]], pos[inv_pos[b]]);
    swap(inv_pos[a], inv_pos[b]);
}
void down(int idx){
    while(1){
        int leftChild = idx * 2;
        int rightChild = idx * 2 + 1;
        int smallest = idx;
        if(leftChild <= top && heap[leftChild] < heap[smallest])
            smallest = leftChild;
        if(rightChild <= top && heap[rightChild] < heap[smallest])
            smallest = rightChild;
        if(idx != smallest) {
            heap_swap(idx, smallest);
            idx = smallest;
        }
        else
            break;
    }
}
void up(int idx){
    while(idx != 1){
        int parent = idx >> 1;
        if(heap[parent] > heap[idx]){
            heap_swap(idx, parent);
            idx = parent;
        }
        else{
            break;
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin>>n;
    string op;
    int x, k, cnt = 0;
    while(n--){
        cin>>op;
        if(op == "I"){
            cin>>x;
            heap[++top] = x;
            pos[++cnt] = top;
            inv_pos[top] = cnt;
            up(top);
        }
        else if(op == "PM"){
            cout<<heap[1]<<endl;
        }
        else if(op == "DM"){
            heap_swap(1, top);
            top--;
            down(1);
        }
        else if(op == "D"){
            cin>>k;
            int now = pos[k];
            heap_swap(now, top);
            top --;
            down(now);
            up(now);
        }
        else if(op == "C"){
            cin>>k>>x;
            heap[pos[k]] = x;
            up(pos[k]);
            down(pos[k]);
        }
    }
    return 0;
}

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

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

相关文章

C++六大默认成员函数

C六大默认成员函数 默认构造函数默认析构函数RAII技术RAII的核心思想优点示例应用场景 默认拷贝构造深拷贝和浅拷贝 默认拷贝赋值运算符移动构造函数&#xff08;C11起&#xff09;默认移动赋值运算符&#xff08;C11起&#xff09;取地址及const取地址操作符重载取地址操作符重…

3D图形学与可视化大屏:什么是片段着色器,有什么作用。

一、片段着色器的概念 在 3D 图形学中&#xff0c;片段着色器&#xff08;Fragment Shader&#xff09;是一种在图形渲染管线中负责处理片段&#xff08;像素&#xff09;的程序。它的主要任务是确定每个像素的颜色和其他属性&#xff0c;如透明度、深度等。片段着色器是可编程…

人类心智逆向工程:AGI的认知科学基础

文章目录 引言:为何需要逆向工程人类心智?一、逆向工程的定义与目标1.1 什么是逆向工程?1.2 AGI逆向工程的核心目标二、认知科学的四大支柱与AGI2.1 神经科学:大脑的硬件解剖2.2 心理学:心智的行为建模2.3 语言学:符号与意义的桥梁2.4 哲学:意识与自我模型的争议三、逆向…

低代码系统-产品架构案例介绍、蓝凌(十三)

蓝凌低代码系统&#xff0c;依旧是从下到上&#xff0c;从左至右的顺序。 技术平台h/iPaas 指低层使用了哪些技术&#xff0c;例如&#xff1a;微服务架构&#xff0c;MySql数据库。个人认为&#xff0c;如果是市场的主流&#xff0c;就没必要赘述了。 新一代门户 门户设计器&a…

Autosar-以太网是怎么运行的?(Davinci配置部分)

写在前面&#xff1a; 入行一段时间了&#xff0c;基于个人理解整理一些东西&#xff0c;如有错误&#xff0c;欢迎各位大佬评论区指正&#xff01;&#xff01;&#xff01; 目录 1.Autosar ETH通讯软件架构 2.Ethernet MCAL配置 2.1配置对应Pin属性 2.2配置TXD引脚 2.3配…

洛谷网站: P3029 [USACO11NOV] Cow Lineup S 题解

题目传送门&#xff1a; P3029 [USACO11NOV] Cow Lineup S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 前言&#xff1a; 这道题的核心问题是在一条直线上分布着不同品种的牛&#xff0c;要找出一个连续区间&#xff0c;使得这个区间内包含所有不同品种的牛&#xff0c;…

STM32 ADC模数转换器

ADC简介 ADC&#xff08;Analog-Digital Converter&#xff09;模拟-数字转换器 ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量&#xff0c;建立模拟电路到数字电路的桥梁 12位逐次逼近型ADC&#xff0c;1us转换时间 输入电压范围&#xff1a;0~3.3V&#xff0…

结合深度学习、自然语言处理(NLP)与多准则决策的三阶段技术框架,旨在实现从消费者情感分析到个性化决策

针对电商个性化推荐场景的集成机器学习和稳健优化三阶段方案。 第一阶段:在线评论数据处理&#xff0c;利用深度学习和自然语言处理技术进行特征挖掘&#xff0c;进而进行消费者情感分析&#xff0c;得到消费者偏好 在第一阶段&#xff0c;我们主要关注如何通过深度学习和自然语…

机器学习8-卷积和卷积核

机器学习7-卷积和卷积核 卷积与图像去噪卷积的定义与性质定义性质卷积的原理卷积步骤卷积的示例与应用卷积的优缺点优点缺点 总结 高斯卷积核卷积核尺寸的设置依据任务类型考虑数据特性实验与调优 高斯函数标准差的设置依据平滑需求结合卷积核尺寸实际应用场景 总结 图像噪声与…

SpringBoot使用 easy-captcha 实现验证码登录功能

文章目录 一、 环境准备1. 解决思路2. 接口文档3. redis下载 二、后端实现1. 引入依赖2. 添加配置3. 后端代码实现4. 前端代码实现 在前后端分离的项目中&#xff0c;登录功能是必不可少的。为了提高安全性&#xff0c;通常会加入验证码验证。 easy-captcha 是一个简单易用的验…

DIY Shell:探秘进程构建与命令解析的核心原理

个人主页&#xff1a;chian-ocean 文章专栏-Linux 前言&#xff1a; Shell&#xff08;外壳&#xff09;是一个操作系统的用户界面&#xff0c;它提供了一种方式&#xff0c;使得用户能够与操作系统进行交互。Shell 是用户与操作系统之间的桥梁&#xff0c;允许用户通过命令行…

数据库备份、主从、集群等配置

数据库备份、主从、集群等配置 1 MySQL1.1 docker安装MySQL1.2 主从复制1.2.1 主节点配置1.2.2 从节点配置1.2.3 创建用于主从同步的用户1.2.4 开启主从同步1.2.4 主从同步验证 1.3 主从切换1.3.1 主节点设置只读&#xff08;在192.168.1.151上操作&#xff09;1.3.2 检查主从数…

(回溯递归dfs 电话号码的字母组合 remake)leetcode 17

只找边界条件和非边界条件&#xff0c;剩下的交给数学归纳法就行&#xff0c;考虑子问题的重复性 [class Solution {vector<string>str { "","","abc","def","ghi","jkl","mno","pqrs"…

Redis --- 使用zset处理排行榜和计数问题

在处理计数业务时&#xff0c;我们一般会使用一个数据结构&#xff0c;既是集合又可以保证唯一性&#xff0c;所以我们会选择Redis中的set集合&#xff1a; 业务逻辑&#xff1a; 用户点击点赞按钮&#xff0c;需要再set集合内判断是否已点赞&#xff0c;未点赞则需要将点赞数1…

响应式编程_04Spring 5 中的响应式编程技术栈_WebFlux 和 Spring Data Reactive

文章目录 概述响应式Web框架Spring WebFlux响应式数据访问Spring Data Reactive 概述 https://spring.io/reactive 2017 年&#xff0c;Spring 发布了新版本 Spring 5&#xff0c; Spring 5 引入了很多核心功能&#xff0c;这其中重要的就是全面拥抱了响应式编程的设计思想和实…

C++ Primer 算术运算符

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

中位数定理:小试牛刀> _ <2025牛客寒假1

给定数轴上的n个点&#xff0c;找出一个到它们的距离之和尽量小的点&#xff08;即使我们可以选择不是这些点里的点&#xff0c;我们还是选择中位数的那个点最优&#xff09; 结论:这些点的中位数就是目标点。可以自己枚举推导&#xff08;很好想&#xff09; (对于 点的数量为…

安全实验作业

一 拓扑图 二 要求 1、R4为ISP&#xff0c;其上只能配置IP地址&#xff1b;R4与其他所有直连设备间均使用共有IP 2、R3-R5-R6-R7为MGRE环境&#xff0c;R3为中心站点&#xff1b; 3、整个OSPF环境IP基于172.16.0.0/16划分&#xff1b; 4、所有设备均可访问R4的环回&#x…

《Python预训练视觉和大语言模型》:从DeepSeek到大模型实战的全栈指南

就是当代AI工程师的日常&#xff1a;* - 砸钱买算力&#xff0c;却卡在分布式训练的“隐形坑”里&#xff1b; - 跟着论文复现模型&#xff0c;结果连1/10的性能都达不到&#xff1b; - 好不容易上线应用&#xff0c;却因伦理问题被用户投诉…… 当所有人都在教你怎么调用…

血压计OCR文字检测数据集VOC+YOLO格式2147张11类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2147 标注数量(xml文件个数)&#xff1a;2147 标注数量(txt文件个数)&#xff1a;2147 …