首页 > 其他分享 >数据结构 - 堆

数据结构 - 堆

时间:2024-10-25 09:12:08浏览次数:7  
标签:int 元素 索引 数组 array 数据结构 节点

今天我们将学习新的数据结构-堆。

01、定义

堆是一种特殊的二叉树,并且满足以下两个特性:

(1)是一棵完全二叉树

(2)中任意一个节点元素值都小于等于(或大于等于)左右子树中所有节点元素值;

小根堆,根节点元素永远是最小值,即堆中每个节点元素值都小于等于左右子树中所有节点元素值;

大根堆,根节点元素永远是最大值,即堆中每个节点元素值都大于等于左右子树中所有节点元素值;

根据堆的定义我们不难发现,堆特别适合用来求集合最值,以及求最值引申的问题比如:排序、优先队列、动态排名等等

02、结构

我们指定堆是一种特殊完全二叉树,因此堆的逻辑结构是树。

我们指定树的存储结构有两种,顺序存储(数组)和链式存储(链表),那么堆的存储结构是什么呢?

既然堆是完全二叉树,那么树的存储结构当然也适用于堆。但是堆一般都选用顺序存储(数组)实现。其原因有三:

(1)位置计算简单:数组实现堆可以使用完全二叉树特性用简单的数学公式即可表示父子节点的索引关系,从而避免了链表实现使用额外的指针,即减少内存开销和实现复杂度;

(2)性能好:数组的连续内存特性使得其有高效的访问速度,而链表因为节点不一定连续访问速度相对较差;

(3)操作简单:数组实现在逻辑实现上更加简单高效,通过交换数组中的元素即可快速实现堆的性质,链表实现在插入和删除操作中需要遍历链表效率远不如数组实现;

总结一句话数组简单、内存连续、性能更好,所以一般选用数组实现堆,当然不一般的情况也可以使用链表实现。

03、实现(最小堆)

下面我们就用数组来实现一个最小堆结构,最大堆只是比较大小不同这里就不做过多赘述。

1、初始化 Init

首先我们需要定义两个私有变量,一个变量用来存放堆元素的数组,一个变量用来存放数组尾元素索引,主要用来跟踪当前堆元素个数情况。

而初始化方法就是初始化两个变量,创建指定容量数组,以及标记数组尾元素索引为-1,表示堆中还没有元素。

//存放堆元素
private int[] _array;
//数组尾元素索引
private int _tailIndex;
//初始化堆为指定容量
public MyselfMinHeap Init(int capacity)
{
    //初始化指定长度数组用于存放堆元素
    _array = new int[capacity];
    //初始化数组尾元素索引为-1,表示空数组
    _tailIndex = -1;
    //返回堆
    return this;
}

2、获取堆容量 Capacity

堆容量指的是数组的固定空间,即数组最多能容纳多少个元素,直接返回数组长度即可。

//堆容量
public int Capacity
{
    get
    {
        return _array.Length;
    }
}

3、获取堆元素数量 Count

堆元素数量指当前堆中一共有多少个元素,我们可以通过私有字段数组尾元素索引值加1获得。

//堆元素数量
public int Count
{
    get
    {
        //数组尾元素索引加1即为堆元素数量
        return _tailIndex + 1;
    }
}

4、获取堆顶元素 Top

堆顶元素指树节点中的根节点,也就是数组中的第一个元素。同时要注意判断数组是否为空,为空报异常。

//获取堆顶元素,即最小元素
public int Top
{
    get
    {
        if (IsEmpty())
        {
            //空堆,不可以进行获取最小堆元素操作
            throw new InvalidOperationException("空堆");
        }
        return _array[0];
    }
}

5、是否为空堆 IsEmpty

空堆即堆中还没有任何元素,当数组尾元素索引为-1时表示空堆。

//检查堆是否为空
public bool IsEmpty()
{
    return _tailIndex == -1;
}

6、是否为满堆 IsFull

满堆即堆空间已满不能再添加新元素,当数组尾元素索引等于数组容量减1时表示满堆。

//检查堆是否为满堆
public bool IsFull()
{
    //数组末尾元素索引等于数组容量减1表示满堆
    return _tailIndex == _array.Length - 1;
}

7、入堆 Push

入堆即向堆中新添加一个元素。

而入堆也涉及到一个问题,就是如何保存每次添加完新元素后,还保持着堆的特性。很显然我们也没办法做到直接把新元素直接插入到其正确的位置上,因此我们可以梳理新添加一个元素的流程,可能大致有以下几个步骤:

(1)首先把新元素添加到堆的末尾;

(2)然后调整堆使其满足堆的性质;

(3)更新堆尾元素索引;

而难点显然就在第二步,如何调整数组使其满足堆的性质?

我们先直接模拟把7654按顺序推入堆中,看看如何实现。

(1)首先7入堆,此时只有一个元素,无需做任何操作,而7就作为根节点;

(2)然后6入堆,此时已有两个元素,因此需要保持堆的两个特性:根节点永远是最小元素和堆是完全二叉树。由完全二叉树特性可得,根节点左子节点索引为20+1=1,而右子节点索引为20+2=2,而此时6的索引为1,所以6为左子节点;又因6比7小,所以根节点变为6, 7变为根节点的左子节点;

(3)然后5入堆,此时已有3个元素,所以5的索引为2,而根节点右子节点索引为2*0+2=2,所以5添加为根节点的右子节点。5比6小,所以5和6交互位置;

(4)然后4入堆,此时已有4个元素,所以4的索引为3,而7节点左子节点索引为2*1+1=3,所以4添加为7节点的左子节点。4比7小,所以4和7交互位置; 再次比较4和5,4比5小,所以4和5交互位置;

相信到这里大家已经看出一点规律了,调整整个数组元素使其满足堆的性质的整个过程大致分为以下几个步骤:

(1)从新元素开始向上比较其与父节点元素的值大小;

(2)如果新元素值小于其父节点元素值,则交互两者位置,否则结束调整;

(3)重复以上步骤直至处理到根节点。

代码实现如下:

//入堆 向堆中添加一个元素
public void Push(int data)
{
    if (IsFull())
    {
        //满堆,不可以进行入添加元素操作
        throw new InvalidOperationException("满堆");
    }
    //新元素索引
    var index = _tailIndex + 1;
    //在数组末尾添加新元素
    _array[index] = data;
    //向上调整堆,以保持堆的性质
    SiftUp(index);
    //尾元素索引向后前进1位
    _tailIndex++;
}
//向上调整堆,以保持堆的性质
private void SiftUp(int index)
{
    //一直调整到堆顶为止
    while (index > 0)
    {
        //计算父节点元素索引
        var parentIndex = (index - 1) / 2;
        //如果当前元素大于等于其父节点元素,则结束调整
        if (_array[index] >= _array[parentIndex])
        {
            break;
        }
        //否则,交换两个元素
        (_array[parentIndex], _array[index]) = (_array[index], _array[parentIndex]);
        //更新当前索引为父节点元素索引继续调整
        index = parentIndex;
    }
}

8、出堆 Pop

出堆即删除并返回堆顶元素(堆中最小元素)。

而出堆同样也涉及到和入堆同样的问题,就是如何保存每次删除元素后,还保持着堆的特性。

显然删除和添加元素情况还不一样。添加新元素后还是一棵完全二叉树,只不过可能没有满足堆的性质,所以需要调整。而删除就不一样了,想象一下当我们把堆顶元素删除后,回怎样?根节点空了,此时还能较完全二叉树吗?显然不能。

如何处理呢?直观的想法就是根节点空了就用其子节点补充上呗,就这样从上到下一直填补,直至最后的空落在了叶子节点那一层,如果这个空位落到了叶子节点左侧,而右侧还有值,此时就表明堆不满足完全二叉树这一特性,因此还需要把这个空位移到堆的末尾,想象都头大。这显然不是一个好办法。

既然我们最后要把根节点删除后的这个空位移到堆的末尾,何不直接把这个空位和堆的尾元素直接调换个位置呢,然后再参考出堆中调整整个数组使其满足堆的性质。

我们梳理整个流程,可能大致有以下几个步骤:

(1)首先获取堆顶元素并暂存;

(2)将堆尾元素赋值给堆顶元素,并将堆尾元素置空;

(3)然后调整堆使其满足堆性质;

(4)更新数组尾元素索引,并返回暂存的堆顶元素;

而难点同样在第二步,如何调整数组使其满足堆的性质?入堆是新元素在堆尾所以是从下往上进行调整,而出堆是堆顶元素需要调整是否可以从上往下进行调整呢?

我们把87654按顺序推入堆中后把4推出堆中,看看如何实现。

首先删除根节点4,并把堆尾元素8放到根节点;

为了保持堆特性,找出根节点及其子节点中最小元素并与当前根节点8交互位置,因为8>6>5,所以5与8交互位置;

然后8节点继续与其子节点进行比较,因为8>7,所以7与8交互位置。

这一出堆过程我们可以总结为以下步骤:

(1)从当前节点开始,找到其子节点元素值;

(2)比较当前节点元素值与其子节点元素值大小,如果当前节点值最小则结束调整,否则取值最小的与其交互;

(3)重复以上步骤直至处理到叶子节点。

代码实现如下:

//出堆 删除并返回堆中最小元素
public int Pop()
{
    if (IsEmpty())
    {
        //空堆,不可以进行删除并返回堆中最小元素操作
        throw new InvalidOperationException("空堆");
    }
    //取出数组第一个元素即最小元素
    var min = _array[0];
    //将数组末尾元素赋值给第一个元素
    _array[0] = _array[_tailIndex];
    //将数组末尾元素设为默认值
    _array[_tailIndex] = 0;
    //将数组末尾元素索引向前移动1位
    _tailIndex--;
    //向下调整堆,以保持堆的性质
    SiftDown(0);
    //返回最小元素
    return min;
}
//向下调整堆,以保持堆的性质
private void SiftDown(int index)
{
    while (index <= _tailIndex)
    {
        //定义较小值索引变量,用于存放比较当前元素及其左右子节点元素中最小元素
        var minIndex = index;
        //计算右子节点索引
        var rightChildIndex = 2 * index + 2;
        //如果存在右子节点,则比较其与当前元素,保留值较小的索引
        if (rightChildIndex <= _tailIndex && _array[rightChildIndex] < _array[minIndex])
        {
            minIndex = rightChildIndex;
        }
        //计算左子节点索引
        var leftChildIndex = 2 * index + 1;
        //如果存在左子节点,则比较其与较小值元素,保留值较小的索引
        if (leftChildIndex <= _tailIndex && _array[leftChildIndex] < _array[minIndex])
        {
            minIndex = leftChildIndex;
        }
        //如果当前元素就是最小的,则停止调整
        if (minIndex == index)
        {
            break;
        }
        //否则,交换当前元素和较小元素
        (_array[minIndex], _array[index]) = (_array[index], _array[minIndex]);
        //更新索引为较小值索引,继续调整
        index = minIndex;
    }
}

9、堆化 Heapify

堆化即把一个无序数组堆化成小根堆。

可以通过调用出堆是用到的调整方法来完成。大家可以思考一下为什么不是堆尾元素开始调整?为什么是从下往上调用向下调整方法调整?

//堆化,即把一个无序数组堆化成小根堆
public void Heapify(int[] array)
{
    if (array == null || _array.Length < array.Length)
    {
        throw new InvalidOperationException("无效数组");
    }
    //将数组复制到堆中
    Array.Copy(array, _array, array.Length);
    //更新尾元素索引
    _tailIndex = array.Length - 1;
    //从最后一个非叶子节点开始向下调整堆
    for (int i = (array.Length / 2) - 1; i >= 0; i--)
    {
        SiftDown(i);
    }
}

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

标签:int,元素,索引,数组,array,数据结构,节点
From: https://www.cnblogs.com/hugogoos/p/18501737

相关文章

  • 数据结构补充
    P1972[SDOI2009]HH的项链求[l,r]区间中颜色的数量#include<cstdio>#include<algorithm>#include<vector>#definefo(i,a,b)for(int(i)=(a);(i)<=(b);(i)++)usingnamespacestd;constintN=1e6+5;intc[N],ans,s;intn,m,x,op,p[N],t[N],a[N],f[N],g[......
  • 新高一暑假第一期集训恢复性训练【数据结构-线段树晚测】(补)
    新高一暑假第一期集训恢复性训练【数据结构-线段树晚测】(补)A.[CF1045G]AIrobots我们先按视野降序排序,这样一个一个考虑,如果后面的能看到前面,那前面的也肯定能看到后面。这样,就是对于每一个机器人,在他前面有几个智商在\([q-k,q+k]\),位置在\([x-r,x+r]\)。那么把这个东......
  • 【数据结构与算法】之栈详解
    栈(Stack)是一种基本的线性数据结构,遵循后进先出、先进后出的原则。本文将更详细地介绍栈的概念、特点、Java实现以及应用场景。1.栈概念概述想象一摞叠放的盘子,你只能从最上面取盘子,放盘子也只能放在最上面。栈就像这样一摞盘子,它只有一个开口,称为栈顶(Top)。另一端封闭,称......
  • 【数据结构与算法】之队列详解
    队列(Queue)是一种重要的线性数据结构,遵循先进先出、后进后出的原则。本文将更详细地介绍队列的概念、特点、Java实现以及应用场景。模运算小复习:a%b的值总是小于b5%4=1  5 %2=11%5=1  4%5=41.队列概念概述想象一下排队买票,先排队的人总是先买......
  • 大二上 数据结构与算法笔记 20241024
    一.inline在C和C++编程语言中,inline关键字是一种函数修饰符,用于建议编译器在编译时将函数的代码直接插入到每个函数调用的地方,而不是进行常规的函数调用。这样做的目的是减少函数调用的开销,尤其是在函数体较小且调用频繁的情况下。作用和优点:减少函数调用开销:通过将函数......
  • 数据结构与算法——双链表的实现
    上次学习了单链表,这次来学习双链表。二者之间的区别是,单链表中的每个结点只存有后继结点的地址,而双链表中则存了两个地址,一个是前驱结点的地址,一个是后继结点的地址。结构体structListNode{ intelement;//数据域 structListNode*next; ......
  • 数据结构懒标记时间戳差异问题
    对于数据结构打lazytag后节点时空不统一问题的解决可以看看之前写的一篇文章线段树初步理解,里头初步介绍了懒标记的作用与使用懒标记所带来的时空不统一的问题。实际上是可以将懒标记拓展到其他数据结构上的。就以经典的毛毛虫链剖分举例子,就是现在我要给树上的与给定......
  • 【C++篇】栈的层叠与队列的流动:在 STL 的韵律中探寻数据结构的优雅之舞
    文章目录C++栈与队列详解:基础与进阶应用前言第一章:栈的介绍与使用1.1栈的介绍1.2栈的使用1.2.1最小栈1.2.2示例与输出1.3栈的模拟实现第二章:队列的介绍与使用2.1队列的介绍2.2队列的使用2.2.1示例与输出2.3队列的模拟实现2.3.1示例与输出第三章:优先队......
  • 第十三章_数据结构与集合源码二
    目录8.Map接口分析8.1哈希表的物理结构8.2HashMap中数据添加过程8.2.1JDK7中过程分析8.2.2JDK8中过程分析8.3HashMap源码剖析8.3.1JDK1.7.0_07中源码1、Entry2、属性3、构造器4、put()方法8.3.2JDK1.8.0_271中源码1、Node2、属性3、构造器4、put()方法......
  • 【数据结构】-数组
    数组特点:数组的地址连续,可以通过下标获取数据。1.数组扩容步骤:$1.创建一个比原来数组更长的新数组$2.让原来数组当中的数据依次复制到新数组当中$3.让arr指向新数组,原数组空间释放  2.数组插入2.1最后位置插入$1.判断数组当中有没有位置,用size记录当......