数据结构——二叉树的顺序存储(堆)(C++实现)

news2024/11/19 1:39:56

数据结构——二叉树的顺序存储(堆)(C++实现)

  • 二叉树可以顺序存储的前提
  • 堆的定义
  • 堆的分类
    • 大根堆
    • 小根堆
  • 整体结构把握
  • 两种调整算法
    • 向上调整算法
      • 递归版本
    • 非递归版本
    • 向下调整算法
    • 非递归版本
  • 向上调整算法和向下调整算法的比较

我们接着来看二叉树:

二叉树可以顺序存储的前提

完全二叉树完全符合顺序存储的前提:

完全二叉树:顺序存储二叉树最适合应用于完全二叉树。完全二叉树是一种特殊的二叉树,除最后一层外,每一层都被完全填满,并且所有结点尽可能集中在左边。由于其结构特性,完全二叉树的结点与数组下标之间存在着直接的数学映射关系,**使得每个结点可以按照固定规则(如左孩子为2i,右孩子为2i+1)**在数组中找到其相应的位置。这种映射保证了数组的存储空间得以充分利用,没有浪费。

顺序存储的二叉树我们称为

堆的定义

堆是一种特殊的树形数据结构,通常以数组的形式进行顺序存储。堆具有以下关键性质:

  1. 完全二叉树结构
    堆是一个完全二叉树或近乎完全二叉树。这意味着除了可能的最后一层外,其他各层都是完全填充的,并且最后一层的所有结点都尽可能靠左排列。这种结构非常适合用数组来表示,因为完全二叉树的结点与数组下标之间存在直接的数学映射关系,使得每个结点可以高效地通过下标访问。

  2. 堆序性质
    堆分为两种主要类型:最大堆和最小堆。无论哪种类型,堆都遵循特定的堆序性质:

  • 大根堆:每个结点的值都大于或等于其子结点的值。即对于任意结点 i,其值 A[i] 大于等于其左孩子 A[2*i+1] 和右孩子 A[2*i+2] 的值。
  • 小根堆:每个结点的值都小于或等于其子结点的值。即对于任意结点 i,其值 A[i] 小于等于其左孩子 A[2*i+1] 和右孩子 A[2*i+2] 的值。

由于堆的完全二叉树特性和堆序性质,它非常适合使用数组进行顺序存储。具体来说:

  • 数组下标与结点关系

假设数组 A 存储了一个堆,根结点位于下标 0。那么对于任一结点 i,其左孩子、右孩子的下标分别为 2*i + 12*i + 2,而其父结点的下标为 (i - 1) // 2(向下取整)。这种固定的下标关系使得在数组中进行堆的操作(如插入、删除、调整等)变得非常直观和高效。

  • 空间利用率

由于堆是完全或近乎完全二叉树,其存储在数组中时空间利用率较高。即使不是严格的完全二叉树,只要整体结构相对平衡,数组中的空闲位置也相对较少,不会造成过多的存储浪费。

  • 操作复杂度

堆的常见操作(如插入、删除堆顶元素、调整堆等)的时间复杂度通常为 O(log n),这是因为堆的高度与结点数成对数关系。数组的随机访问特性使得这些操作能够在常数时间内定位到相关结点,然后通过递归或迭代方式进行堆结构调整。

因此,堆作为一类满足特定条件的二叉树,其完全二叉树特性、堆序性质以及高效的操作性能,使其非常适合采用数组进行顺序存储。堆常用于实现优先队列、求解Top-K问题、堆排序算法等场景。

堆的分类

大根堆

大根堆是一种特殊的二叉堆,其中每个节点的值都大于或等于其子节点的值。具体地说,对于大根堆中的任意节点 i,其值 A[i] 大于等于其左孩子 A[2*i+1] 和右孩子A[2*i+2]的值。根节点(数组下标为1或0,取决于实现)总是包含堆中的最大值。大根堆常用于实现优先队列,其中队首元素始终为当前最大的元素。
在这里插入图片描述

小根堆

小根堆也是一种特殊的二叉堆,其中每个节点的值都小于或等于其子节点的值。对于小根堆中的任意节点 i,其值 A[i] 小于等于其左孩子 A[2*i+1] 和右孩子 A[2*i+2] 的值。根节点(同样为数组下标为1或0)始终包含堆中的最小值。小根堆同样适用于优先队列的场景,但此时队首元素为当前最小的元素。
在这里插入图片描述

整体结构把握

我们这里用vector作为底层容器来存储数据,这些数据按照顺序排放:

#pragma once
#include<iostream>
#include<vector>


template<class T>
//堆的定义
class Heap
{
public:
    Heap()
        :_size(0)
    {
        _data.resize(10);
    }


    Heap(const size_t& size)
        :_size(0)
    {
        _data.resize(size + 1);
    }

    //插入
    void insert(const T& data)
    {
        if(_size > _data.capacity())
        {
            _data.resize(2 * _data.size());
        }

        _data[++_size] = data;
    }

    //是否为空
    bool empty()
    {
        return _size == 0;
    }

    //打印堆
    void printHeap()
    {
        for(int i = 1; i < _size + 1; i++)
        {
            std::cout<< _data[i] << " ";
        }

        std::cout << std::endl;
    }

private:
    std::vector<T> _data; //存放数据
    size_t _size; //当前数据个数
};

这里注意一下,我的一个数据并没有放在0号位置,而是放在了1号位置,这样方便我们寻找父节点:
在这里插入图片描述
我们可以先测试一下:

#include"heap.h"


int main()
{
    Heap<int> heap;

    heap.insert(12);
    heap.insert(23);
    heap.insert(1);
    heap.insert(0);
    heap.insert(24);
    heap.insert(4);
    heap.insert(188);
    heap.insert(9);
    heap.insert(58);


    heap.printHeap();

    return 0;
}

在这里插入图片描述

两种调整算法

向上调整算法

向上调整算法的核心是把每一个结点都当做孩子,去跟自己的父亲比较,如果比自己的父亲大(或者小)交交换数据
在这里插入图片描述

递归版本

递归版本比较好想,我只管我自己和父亲的比较,比较完之后,继续向上比较:

// 向上调整函数(以小根堆为例)
// 输入参数:index - 需要进行调整的子节点索引
void sifUpHeap(const size_t& index)
{
    // 如果索引小于 1,说明已经到达根节点或无效索引,无需继续调整,直接返回
    if (index < 1)
    {
        return;
    }

    // 获取当前子节点的父节点索引
    size_t parentIndex = Parent(index);

    // 如果子节点索引大于 1(即不是根节点),并且子节点的值小于其父节点的值
    // 则交换两者,确保父节点的值小于其子节点的值(小根堆性质)
    if (index > 1 && _data[parentIndex] > _data[index])
    {
        std::swap(_data[parentIndex], _data[index]);

        // 对交换后的新父节点(原子节点)继续进行向上调整,确保整棵子树满足小根堆性质
        sifUpHeap(parentIndex);
    }

    // 返回,完成当前节点的向上调整过程
    return;
}

但是我们这样只是完成了一个数据的调整,我们要所有的数据进行调整:

    //向上调整算法(以小根堆为例)
   void sifUpHeap(const size_t& index)
   {
	   if (index < 1)
	    {
	        return;
	    }
	    
        if(index > 1 && _data[Parent(index)] > _data[index])
        {
            std::swap(_data[Parent(index)],_data[index]);
            //接着向上
            sifUpHeap(Parent(index));
        }
        return;
    }
    
    //调整为小根堆
    void ToMinHeap()
    {
        for(int i = 1; i < _size + 1; i++)
        {
            sifUpHeap(i);
        }
    }

在这里插入图片描述

非递归版本

我们也可以不用递归,使用迭代来完成:

// 向上调整函数(非递归版本,以小根堆为例)
// 输入参数:child - 需要进行调整的子节点索引
void sifUpHeap_non(size_t child)
{
    // 计算当前子节点的父节点索引
    size_t parent = child / 2;

    // 循环迭代,直到子节点成为根节点或已满足小根堆性质
    while (child > 1)
    {
        // 如果子节点的值小于其父节点的值
        // 则交换两者,确保父节点的值小于其子节点的值(小根堆性质)
        if (_data[Parent(child)] > _data[child])
        {
            std::swap(_data[Parent(child)], _data[child]);

            // 更新子节点索引为交换后的父节点索引,准备对新的子节点进行下一轮比较
            child = parent;

            // 重新计算父节点索引
            parent = child / 2;
        }
        else
        {
            // 子节点已满足小根堆性质,跳出循环,结束调整
            break;
        }
    }
}

//调整为小根堆
 void ToMinHeap()
 {
     for(int i = 1; i < _size + 1; i++)
     {
         sifUpHeap_non(i);
     }
 }

向下调整算法

向下调整算法是把所有结点当做父亲结点,去和自己的孩子结点比较,看哪个孩子结点比自己大或小,就交换
在这里插入图片描述向下调整算法有个条件:左右子树必须为堆,因为这个特性,我们向下调整算法得从最后一个有孩子的双亲结点开始:

// 下降调整函数(以小根堆为例)
// 输入参数:index - 需要进行调整的父节点索引
void sifDownHeap(const size_t& index)
{
    // 计算当前父节点的左孩子索引
    size_t leftchild = LeftChild(index);

    // 如果左孩子索引超出了堆的有效范围(即不存在左孩子),说明无需调整,直接返回
    if (leftchild > _size)
    {
        return; // 超出范围,无需调整
    }

    // 初始化 "miner" 为当前父节点的左孩子索引
    // "miner" 用于记录待调整子节点中值最小的那个的索引
    int miner = leftchild;

    // 比较左孩子与右孩子(如果存在)的值,确定哪个子节点的值更小
    // 如果右孩子存在且其值小于左孩子,更新 "miner" 为右孩子索引
    if (index < _size + 1 && _data[leftchild] > _data[leftchild + 1])
    {
        miner++;
    }

    // 如果当前父节点的值大于其最小子节点(即 "miner" 所指向的子节点)的值
    // 则交换两者,确保父节点的值小于其子节点的值(小根堆性质)
    if (_data[miner] < _data[index])
    {
        std::swap(_data[miner], _data[index]);

        // 对交换后的新父节点(原子节点)继续进行向下调整,确保整棵子树满足小根堆性质
        sifDownHeap(miner);
    }

    // 返回,完成当前节点的向下调整过程
    return;
}

    //调整为小根堆
    void ToMinHeap()
    {
        // for(int i = 1; i < _size + 1; i++)
        // {
        //     sifUpHeap_non(i);
        // }

        for(int i = _size / 2 ; i >=1 ; i--) //从最后一个父节点结点开始调整
        {
            sifDownHeap(i);
        }
    }

在这里插入图片描述

非递归版本

我们也可以用非递归的方式实现:

// 下降调整函数(以小根堆为例)
// 输入参数:parent - 需要进行调整的父节点索引
void sifDownHeap_non(size_t parent)
{
    // 计算当前父节点的左孩子索引,假定左孩子为待调整子节点中值最小的一个
    int child = LeftChild(parent);

    // 循环迭代,直到越界
    while (child < _size + 1)
    {
        // 如果右孩子存在且其值小于左孩子,更新 "child" 为右孩子索引
        // 保持 "child" 指向待调整子节点中值最小的那个
        if (parent + 1 < _size + 1 && _data[child] > _data[child + 1])
        {
            child++;
        }

        // 如果当前父节点的值大于其最小子节点(即 "child" 所指向的子节点)的值
        // 则交换两者,确保父节点的值小于其子节点的值(小根堆性质)
        if (_data[parent] > _data[child])
        {
            std::swap(_data[parent], _data[child]);

            // 更新父节点索引为交换后的子节点索引,准备对新的子节点进行下一轮比较
            parent = child;

            // 重新计算子节点索引,从新的父节点开始
            child = LeftChild(parent);
        }
        else
        {
            // 子节点已满足小根堆性质,跳出循环,结束调整
            break;
        }
    }
}

    //调整为小根堆
    void ToMinHeap()
    {
        // for(int i = 1; i < _size + 1; i++)
        // {
        //     sifUpHeap(i);
        // }

        for(int i = _size / 2 ; i >=1 ; i--) //从最后一个结点开始调整
        {
            sifDownHeap_non(i);
        }
    }

在这里插入图片描述

向上调整算法和向下调整算法的比较

向上调整算法(Sift Up)和向下调整算法(Sift Down)是堆数据结构中常用的两种调整方法,它们各有特点和适用场景,无法简单地说哪个更优秀。选择使用哪种调整方法取决于具体的堆操作需求和上下文。下面分别介绍两者的特性及适用场景:

向上调整算法(Sift Up)

  • 用途:通常用于将新插入的元素或被修改的元素调整到堆中的正确位置,使其满足堆性质(大根堆或小根堆)。例如,在插入新元素后,将其放在堆末尾,然后从该位置开始向上调整,确保新元素及其路径上的所有节点满足堆性质。
  • 特点:从堆底部(新元素所在位置或被修改元素所在位置)开始,逐层向上比较父节点与子节点的值,若子节点值更适合堆顶(对于大根堆,子节点值更大;对于小根堆,子节点值更小),则交换二者,直至子节点成为堆顶或已满足堆性质。
  • 优点
  • 适用于插入操作和单元素修改后的调整,因为新元素或被修改元素的初始位置已知,可以直接从该位置开始调整。
  • 调整过程中涉及的节点数量相对较少,时间复杂度为 O(log n),效率较高。
  • 缺点
  • 不适用于堆顶元素被删除后的调整,因为此时需要重新确定堆顶元素,且可能需要对多个子节点进行比较和调整。

向下调整算法(Sift Down)

  • 用途:通常用于删除堆顶元素后重新调整堆,或在构建堆的过程中对整个堆进行调整。例如,在删除堆顶元素后,将堆末尾元素移至堆顶,然后从堆顶开始向下调整,确保所有节点满足堆性质。
  • 特点:从堆顶开始,逐层向下比较父节点与子节点的值,若父节点值更适合堆底(对于大根堆,父节点值更小;对于小根堆,父节点值更大),则交换二者,直至父节点成为堆底或已满足堆性质。
  • 优点
  • 适用于堆顶元素被删除后的调整,因为此时堆顶元素已知,可以直接从该位置开始调整。
  • 在构建堆的过程中,可以从最后一个非叶子节点开始逐个进行向下调整,确保整个堆满足堆性质。
  • 缺点
  • 对于插入操作或单元素修改后的调整,可能需要遍历到堆底才能找到新元素或被修改元素的最终位置,调整过程中涉及的节点数量可能较多。

综上所述,向上调整算法和向下调整算法各有优势,适用于不同的堆操作场景。在实际应用中,根据具体需求选择合适的调整方法,或者结合使用这两种方法,可以有效维护堆数据结构的性质,确保堆操作的高效性。因此,不能简单地说哪个更优秀,而应视具体情况灵活选用。

如果大家阅读完之后还是比较迷糊的话,可以点击这里,这里是我之前写的堆的博客,介绍的更为详细:

https://blog.csdn.net/qq_67693066/article/details/131544172

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

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

相关文章

1. 房屋租赁管理系统(Java项目 springboot/vue)

1.此系统的受众 1.1 在校学习的学生&#xff0c;可用于日常学习使用或是毕业设计使用 1.2 毕业一到两年的开发人员&#xff0c;用于锻炼自己的独立功能模块设计能力&#xff0c;增强代码编写能力。 1.3 亦可以部署为商化项目使用。 2. 技术栈 jdk8springbootvue2mysq5.7&8…

论文阅读之MMSD2.0: Towards a Reliable Multi-modal Sarcasm Detection System

文章目录 论文地址主要内容主要贡献模型图技术细节数据集改进多视图CLIP框架文本视图图像视图图像-文本交互视图 实验结果 论文地址 https://arxiv.org/pdf/2307.07135 主要内容 这篇文章介绍了一个名为MMSD2.0的多模态讽刺检测系统的构建&#xff0c;旨在提高现有讽刺检测系…

Amazon云计算AWS之[5]关系数据库服务RDS

文章目录 RDS的基本原理主从备份和下读写分离 RDS的使用 RDS的基本原理 Amazon RDS(Amazon Relational Database Service) 将MySQL数据库移植到集群中&#xff0c;在一定的范围内解决了关系数据库的可扩展性问题。 MySQL集群方式采用Share-Nothing架构。每台数据库服务器都是…

JavaEE——介绍 HTTPServlet 三部分使用与 cookie 和 session 的阐述

文章目录 一、HTTPServlet介绍其中的关键 三个方法 二、HTTPServletRequest(处理请求)1.分块介绍方法作用get 为前缀的方法字段中 含有 getParameter 字段 的方法(前后端交互)&#xff1a;字段中 含有 getHeader 字段 的方法&#xff1a; 2.解释前后端的交互过程3.使用 json 格…

【小迪安全2023】第59天:服务攻防-中间件安全CVE复现lSApacheTomcatNginx

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java、PHP】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收…

RocketMQ快速入门:namesrv、broker、dashboard的作用及消息发送、消费流程(三)

0. 引言 接触rocketmq之后&#xff0c;大家首当其冲的就会发现需要安装3个组件&#xff1a;namesrv, broker, dashboard&#xff0c;其中dashboard也叫console&#xff0c;为选装。而这几个组件之前的关系是什么呢&#xff0c;消息发送和接收的过程是如何传递的呢&#xff0c;…

如何在 Visual Studio 中通过 NuGet 添加包

在安装之前要先确定Nuget的包源是否有问题。 Visual Studio中怎样更改Nuget程序包源-CSDN博客 1.图形界面安装 打开您的项目&#xff0c;并在解决方案资源管理器中选择您的项目。单击“项目”菜单&#xff0c;然后选择“管理 NuGet 程序包”选项。在“NuGet 包管理器”窗口中…

Swift 中的 Range 运算符

在 Swift 中&#xff0c;Range 运算符是一种强大的工具&#xff0c;用于表示一系列连续的数值或字符。Range 可以用于循环、数组切片、条件语句等场景&#xff0c;为我们提供了方便的方法来处理数据集合。 闭区间运算符 a...b 闭区间运算符 a...b 用于创建一个从起始值到结束…

在虚拟环境中找到Qt Designer

Pyqt5中找到Qt Designer 安装Pyqt5和Qt Designer: pip install pyqt5-tools 假设Python的虚拟环境名为:d2l &#xff0c;虚拟环境在d2l文件夹中 D:\Software\d2l\Lib\site-packages\qt5_applications\Qt\bin 双击Qt designer启动 Pyside2中找到Qt Designer d2l是虚拟环境…

NDK 基础(五)—— C++ 高级特性2

1、左值右值 在 C 中&#xff0c;左值&#xff08;lvalue&#xff09;和右值&#xff08;rvalue&#xff09;是用于描述表达式的术语&#xff0c;它们与赋值操作和内存中对象的生命周期有关。 **左值&#xff08;lvalue&#xff09;**是指可以出现在赋值操作符左侧的表达式&a…

【Vue3+Tres 三维开发】02-Debug

预览 介绍 Debug 这里主要是讲在三维中的调试,同以前threejs中使用的lil-gui类似,TRESJS也提供了一套可视化参数调试的插件。使用方式和之前的组件相似。 使用 通过导入useTweakPane 即可 import { useTweakPane, OrbitControls } from "@tresjs/cientos"const {…

PotatoPie 4.0 实验教程(21) —— FPGA实现摄像头图像二值化(RGB2Gray2Bin)

PotatoPie 4.0开发板教程目录&#xff08;2024/04/21&#xff09; 为什么要进行图像的二值化&#xff1f; 当我们处理图像时&#xff0c;常常需要将其转换为二值图像。这是因为在很多应用中&#xff0c;我们只对图像中的某些特定部分感兴趣&#xff0c;而不需要考虑所有像素的…

机器视觉系统-工业光源什么是同轴光

光路描述&#xff1a;反射光线与镜头平行&#xff0c;称为同轴光。 效果分析&#xff1a;光线经过平面反射后&#xff0c;与光轴平行地进入镜头。此时被测物相当于一面镜子&#xff0c;图像体现的是光源的信息&#xff0c;当“镜子“出现凹凸不平时&#xff0c;将格外地明显。 …

Win32 API 光标隐藏定位和键盘读取等常用函数

Win32 API 光标隐藏定位和键盘读取等常用函数 一、Win32 API二、控制台程序指令modetitlepausecls 三、控制台屏幕上坐标的结构体COORD四、句柄获取函数GetStdHandle五、控制台光标操作1.控制台光标信息结构体CONSOLE_CURSOR_INFO2.得到光标信息函数GetConsoleCursorInfo3. 设置…

会跳舞的网站引导页HTML源码

源码介绍 这套引导页源码非常好看&#xff0c;网址也不会不停的动起来给人一种视觉感很强烈 简单修改一下里面的地址就行看&#xff0c;非常简单&#xff01; 效果预览 源码下载 会跳舞的网站引导页HTML源码

排序FollowUp

FollowUp 插入排序 直接插入排序 时间复杂度:最坏情况下:0(n^2) 最好情况下:0(n)当数据越有序 排序越快 适用于: 待排序序列 已经基本上趋于有序了! 空间复杂度:0(1) 稳定性:稳定的 public static void insertSort(int[] array){for (int i 1; i < array.length; i) {int…

64位整数高低位的数据获取与赋值操作探讨

参考本篇->LOWORD和HIWORD函数_hidword-CSDN博客 一&#xff0c;如何获取一个64位整数的高32位和低32位 原理其实很简单&#xff1a; 解释一些概念 ①十六进制和二进制直接挂钩 一个十六位的十六进制数【0XAABBCCDD12345678】转为二进制的过程是把其中的每个数转为对应的二…

构建中小型企业网络-单臂路由

1.给IP地址配置好对应的IP和网关 2.配置交换机 3.路由配置 在交换机ge0/0/1中配置端口为trunk是可以允许多个vlan通过的&#xff0c;但路由器是不能够配置vlan&#xff0c;而交换机和路由器间连接的只有一根线&#xff0c;一个端口又只能配置一个ip地址&#xff0c;只有一个ip地…

人脸识别概念解析

目录 1. 概述 2. 人脸检测 3. 人脸跟踪 4. 质量评价 5. 活体检测 6. 特征提取 7. 人脸验证 8. 人脸辨识 1. 概述 人脸识别在我们的生活中随处可见&#xff0c;例如在大楼门禁系统中&#xff0c;它取代了传统的门禁卡或密码&#xff0c;提高了进出的便捷性和安全性。在商…

如何通过4G DTU实现现场仪表的分布式采集并发布到MQTT服务器

提供一份资料文档以一个具体的工程案例来讲解&#xff0c;如何通过4G DTU实现现场仪表的分布式采集并发布到MQTT服务器。采用的数据采集模块是有人物联的边缘采集4G DTU&#xff0c;采集多个多功能电表和远传水表的数据&#xff0c;通过MQTT通讯的型式传送给MQTT服务器&#xf…