数据结构:堆

news/2024/10/18 6:00:58/

这张网络上的图片很形象的展示了一棵具有多个分支的独特树木,其分支模式类似于(甚至于是完美)二叉树的结构。我们可以将这棵树的形态作为引入二叉树概念的一个隐喻。在二叉树中,每个节点最多有两个子节点,这与树木的分支方式相似,其中每个分支都可以进一步分为两个更小的分支。

二叉树是计算机科学中常用的数据结构,它的每个节点最多有两个子节点,通常被称为左子节点和右子节点。二叉树的这种结构使得它在执行搜索和排序操作时非常高效,也广泛应用于各种算法中,如二叉搜索树、堆和平衡二叉树等。

二叉树

  • 定义:二叉树是一种数据结构,其中每个节点最多有两个子节点。
  • 类型
    • 满二叉树:所有非叶节点都有两个子节点。
    • 完全二叉树:除最后一层外,每层都被完全填满,且所有叶节点集中在左侧。
    • 满二叉树节点个数与树高度的关系:
    • 满二叉树的节点个数与树高度之间的计算关系可以通过以下公式表示:

    • 节点个数: 对于高度为 ( h ) 的满二叉树,节点个数 ( n ) 为 ( 2^h - 1 )。

    • 树高度: 如果已知节点个数 ( n ),树的高度 ( h ) 可以通过等比数列公式计算 来计算。这些公式说明,对于满二叉树,节点个数是树高度的指数函数,而树高度是节点个数的对数函数。这种关系在数据结构中非常重要,因为它影响树的平衡和搜索操作的效率。

  • 术语
    • 根节点:没有父节点的顶部节点。
    • 叶节点:没有子节点的节点。
    • 高度:从根节点到任意节点的最长路径。
    • 父节点: 一个节点,它有向下连接到子节点的链接。
    • 左子节点: 父节点的左侧子节点。
    • 右子节点: 父节点的右侧子节点。

数据结构的世界里,二叉树无疑是一个神奇而强大的存在。它以其独特的方式组织和存储数据,使得数据的处理变得高效而直观。而在二叉树的众多变种中,堆(Heap)以其特殊的性质和广泛的应用,成为了一个不可或缺的主角。

堆是一种特殊的完全二叉树,它满足一个基本条件:任何一个父节点的值都大于或等于(在最大堆中)或小于或等于(在最小堆中)其子节点的值。这个简单的规则赋予了堆一种强大的能力——快速找到最大或最小元素的能力。

堆的应用范围非常广泛,从操作系统的任务调度到数据库的查询优化,再到各类算法竞赛中的问题求解,堆的身影无处不在。堆排序(Heap Sort)和Top-K问题就是堆应用的经典案例。堆排序是一种高效的排序方法,它利用堆的性质,能够在O(nlogn)的时间复杂度内完成排序。而Top-K问题,即从一组数据中找出最大或最小的K个数,也可以通过维护一个大小为K的堆来高效解决。

那么,堆是如何实现的呢?

在计算机中,堆通常使用数组来表示。对于一个索引为i的节点,其左子节点的索引为2i+1,右子节点的索引为2i+2,父节点的索引为(i-1)/2(我们使用整形来计算,多余浮点数将会被计算机自动去除)。这样的存储方式使得堆的操作变得简单而高效。用数组的下标分别代替根节点与其子节点,用完全二叉树根节点与子节点的关系计算其节点下标

#pragma once
#include<iostream>
#include<cassert>
#include<cstdbool>
using namespace std;typedef int Datatype;class Heap
{friend inline ostream& operator<<(ostream& out, const Heap& h);
public:c++Heap(int capacity=4);~Heap();void Push(Datatype a);Datatype TPop();bool Empty();private:void adjustup(int child);void adjustdown();inline void swap(Datatype& q1, Datatype& q2){Datatype temp = q1;q1 = q2;q2 = temp;}void New_capacity();Datatype* heap;int _size;int _capacity;
};inline ostream& operator<<(ostream& out, Heap& h)
{while (!h.Empty()){out << h.TPop()<<" ";}out << endl;return out;
}

堆的插入操作

堆的插入操作基本上遵循以下步骤:

1. 将新元素添加到堆的末尾。
2. 将新元素与其父节点比较。
3. 如果新元素的值大于(或小于,取决于是最大堆还是最小堆)其父节点的值,则将它们交换。
4. 重复步骤2和3,直到新元素被放置到正确的位置,或者已经到达堆的顶部。

,这样的交换,不会影响其他地方,由图可知,我们通过对插入元素的调正,将其放到合适的位置,来构成堆(大或者小)

void Heap::adjustup(int child)
{int parent = (child - 1) / 2;while (child > 0){if (heap[parent] < heap[child]){swap(heap[parent] , heap[child]);}child = parent;parent = (child - 1) / 2;}
}void Heap::Push(Datatype a)
{if (_size == _capacity){New_capacity();}heap[_size++] = a;adjustup(_size-1);}

堆的插入操作的时间复杂度为O(log n),其中n是堆中元素的数量。这使得堆成为处理大量动态数据集时的理想选择。

堆的删除操作

在最大堆中,删除操作通常涉及移除根节点,即堆中的最大元素。删除根节点后,为了维持堆的性质,需要执行以下步骤:

1. 将堆的最后一个元素移动到根节点位置。
2. 删除最后一个元素,减小堆的大小。
3. 对新的根节点执行向下调正,确保恢复堆的性质。

以下是一个简单的C++代码示例,展示了如何在最大堆中执行删除操作:

void Heap::adjustdown()
{int parent = 0;int child = parent * 2 + 1;while (child < _size)//直到遍历完整个堆{if (child + 1 < _size && heap[child] < heap[child + 1]){child += 1;}//先进性越界判断,在进行比较,防止越界,程序崩溃if (heap[parent] < heap[child]){swap(heap[parent], heap[child]);}parent = child;child = parent * 2 + 1;//更换孩子,父亲的节点下标}
}Datatype Heap::TPop()//输出堆顶数据,也就是最大或者最小数据
{assert(!Empty());Datatype temp = heap[0];swap(heap[0], heap[--_size]);adjustdown();/*cout << temp;return temp;*///调式用
}

简易排序

我们可以通过每次将最大或者最小元素交换到最后,或者直接输出(因为每次输出的都是当前数据中最大或者最小的数据),然后整个数据就变的有序了,但是这并不是堆排序,堆排序将时间复杂度运用到极致。

根据上面代码我们可以得到

while (!h.Empty())
{cout << h.TPop()<<" ";
}cout << endl;

TOP_K 问题

当我们拥有百万级别的数据时,我们需要找到最大的那个值,或者前几个最大的值,如果我们使用排序,这个时间消耗和空间消耗时难以想象的但是我们可以使用堆,我们利用前10个数据建堆,堆顶就是最小的数据,如果后续数据纯在比他大的,那么就将堆顶数据改变,比他大的那个数据进堆,那么到最后,我们就得到了,10个最大的数据!!

我们先用文件操作写出10000个数据,然后再其中改动10个数据,将其大于100000,用于查看我们的程序是否正确,然后屏蔽掉文件写入数据的代码,防止重复写入

void data()
{FILE* fp = fopen("data.txt", "w+");if (fp == nullptr){perror("foprn");exit(-1);}for (int i = 0; i < 10000; i++){int d = rand() % 100000;fprintf(fp, "%d\n", d);}fclose(fp);
}int main()
{srand((unsigned int)time(nullptr));data();return 0;
}
void top_k()
{Heap top;FILE* fp= fopen("data.txt", "r+");int ch = 0;for (int i = 0; i < 10; i++){fscanf(fp, "%d ", &ch);top.Push(ch);}int flag = 0;while ((flag=fscanf(fp, "%d ", &ch))==1)//判断是否读完{top.TopPush(ch);}cout << top;fclose(fp);}int main()
{srand((unsigned int)time(nullptr));/*data();*///写完数据后屏蔽top_k();return 0;
}

堆的实现看似简单,但其背后蕴含着深刻的数据结构和算法原理。它不仅仅是一个数据结构,更是一种算法设计的哲学。通过堆,我们可以看到,如何通过简单的规则和高效的操作,来解决复杂的问题。

如果你对堆或者其他数据结构感兴趣,不妨深入学习和探索。在这个数据驱动的时代,数据结构和算法的知识将是你通往成功的重要钥匙。而堆,无疑是这把钥匙中的精华部分。让我们一起探索堆的奥秘,发现数据结构的美妙世界吧!。


http://www.ppmy.cn/news/1425395.html

相关文章

2010年认证杯SPSSPRO杯数学建模B题(第一阶段)交通拥堵问题全过程文档及程序

2010年认证杯SPSSPRO杯数学建模 交通拥堵问题 B题 Braess 悖论 原题再现&#xff1a; Dietrich Braess 在 1968 年的一篇文章中提出了道路交通体系当中的Braess 悖论。它的含义是&#xff1a;有时在一个交通网络上增加一条路段&#xff0c;或者提高某个路段的局部通行能力&a…

C++ 数据结构 linux 【第一天】

1.命名空间 在C/C中&#xff0c;变量、函数和类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存在于全局作 用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c;以避免命名冲突或名字污染&#xff0c;namespace关键字…

FlinkCDC基础篇章2-数据源 SqlServerCDC写入到ES中

接着 上期FlinkCDC基础篇章1-安装使用 下载 Flink 和所需要的依赖包 # 下载 Flink 1.17.0 并将其解压至目录 flink-1.17.0 下载下面列出的依赖包&#xff0c;并将它们放到目录 flink-1.17.0/lib/ 下&#xff1a; 下载链接只对已发布的版本有效, SNAPSHOT 版本需要本地编译 …

OpenCV杂记(2):图像拼接(hconcat, vconcat)

OpenCV杂记&#xff08;1&#xff09;&#xff1a;绘制OSD&#xff08;cv::getTextSize, cv::putText&#xff09;https://blog.csdn.net/tecsai/article/details/137872058 1. 简述 做图像处理或计算机视觉技术的同学都知道&#xff0c;我们在工作中会经常遇到需要将两幅图像拼…

3、MYSQL-一条sql如何在MYSQL中执行的

MySQL的内部组件结构 大体来说&#xff0c;MySQL 可以分为 Server 层和存储引擎层两部分。 Server层 主要包括连接器、查询缓存、分析器、优化器、执行器等&#xff0c;涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有的内置函数&#xff08;如日期、时间、数学和加密函…

【数据结构】图论(图的储存方式,图的遍历算法DFS和BFS、图的遍历算法的应用、图的连通性问题)

目录 图论一、 图的基本概念和术语二、图的存储结构1. 数组(邻接矩阵)存储表示无向图的数组(邻接矩阵)存储表示有向图的数组(邻接矩阵)存储表示 邻接表存储表示有向图的十字链表存储表示无向图的邻接多重表存储表示 三、图的遍历算法图的遍历——深度优先搜索&#xff08;DFS&a…

Unity导出package

C#代码导出后为一个dll&#xff0c;原有的不同平台的库不变。 以下操作均在build PC 平台下操作。 1.在要导出的文件夹下建assembly definition (Any platform) 2.将项目文件夹下的\Library\ScriptAssemblies中的相应assembly definition的dll复制到要导出的文件夹下 3.在uni…

如何在 Linux 和 Mac 终端命令中添加别名

在本文中&#xff0c;我们将探讨一种简单的技巧&#xff0c;可以节省您在终端中输入重复命令的时间。 作为开发者&#xff0c;我们花费大量时间在终端上执行命令。无论是浏览目录、运行脚本、更改 Node.js 版本还是版本控制命令&#xff0c;手动输入每个命令都是一项耗时的任务…