数据结构与算法——堆

简介: 堆(Heap),其实是一种特殊的二叉树,主要满足了二叉树的两个条件:1. 堆是一种完全二叉树,还记得完全二叉树的定义吗?叶节点都在最底下两层,最后一层的节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种树叫做完全二叉树。2. 堆中的每个节点的值都必须大于等于(或者小于等于)其左右子节点的值。对于堆中的每个节点都大于等于其左右子节点的值,叫做大顶堆,反之,则叫做小顶堆。看看下面的图就能懂了。

1. 什么是堆


堆(Heap),其实是一种特殊的二叉树,主要满足了二叉树的两个条件:

  1. 堆是一种完全二叉树,还记得完全二叉树的定义吗?叶节点都在最底下两层,最后一层的节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种树叫做完全二叉树。
  2. 堆中的每个节点的值都必须大于等于(或者小于等于)其左右子节点的值。

对于堆中的每个节点都大于等于其左右子节点的值,叫做大顶堆,反之,则叫做小顶堆。看看下面的图就能懂了。

其中,1 是大顶堆,2 是小顶堆,3 不是堆。


2. 堆是如何存储的?


其实,堆可以按照完全二叉树的存储方式来储存,因为完全二叉树是比较省空间的,所以我们可以直接用数组来存储,然后按照数组下标来取出堆中数据。参照下图,来看看堆的存储:

其中,对于任意位置上的节点 i ,其左子节点是 2 * i + 1,右子节点是 2 * i + 2,父节点是 (i - 1) / 2


3. 堆的几种操作


明白了堆是怎样储存的,我们在来看看堆最常见的两个操作:往堆中插入元素和删除堆顶元素。

首先,如果要往堆中插入一个元素,我们先将其插入到数组中最后一个位置,然后与其父节点的值进行比较,如果大于父节点,则交换位置,继续比较。看看下面的图你就明白了:

交换操作的代码,我也放到这里:

public class Heap {
    private int[] data;//存储堆数据的数组
    private int n;//堆中可存储的元素容量
    private int size;//堆中存储的元素个数
    public Heap(int capacity) {
        this.data = new int[capacity];
        this.n = capacity;
        this.size = 0;
    }
    //往堆中插入数据
    public void insert(int value){
        if (size >= n) return;//堆满了
        data[size] = value;
        int i = size;
        while ((i - 1) / 2 >= 0 && data[i] > data[(i - 1) / 2]){
            //交换data[i] 极其父节点 data[(i - 1) / 2] 的值
            swap(data, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
        size ++;
    }
    //交换数组两个位置的元素
    private void swap(int[] data, int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
}

接下来看看第二种操作:删除堆顶元素。

根据堆的定义,堆顶元素其实就是堆的最大或最小元素。所以删除堆顶元素,我们只需要移除数组中的第 0 个元素,然后再进行堆化,让堆继续保持顺序。那该怎么进行堆化呢?

首先我们直接将堆中的最后一个元素移到堆顶,然后与其左右子节点的值进行比较,找到较大的那么子节点,交换位置,然后继续比较,你可以结合代码来理解一下:

//删除数据,如果是大顶堆,则删除的是堆中的最大元素
    //如果是小顶堆,则删除的堆中的最小元素
    public int removeMax(){
        if (size == 0) return -1;//堆为空
        //将数组中的最后一个元素,放到第一个位置
        int result = data[0];
        data[0] = data[size - 1];
        data[-- this.size] = 0;
        //进行堆化
        heapify(data, size, 0);
        return result;
    }
    //堆化函数
    private void heapify(int[] data, int size, int i){
        while (true){
            int max = i;
            if ((2 * i + 1) < size && data[i] < data[2 * i + 1]) max = 2 * i + 1;
            if ((2 * i + 2) < size && data[max] < data[2 * i + 2]) max = 2 * i + 2;
            if (max == i) break;
            swap(data, i, max);
            i = max;
        }
    }

4. 堆排序


现在来看看里用堆这种数据结构是怎么实现排序功能的。堆排序的时间复杂度非常的稳定,是O(nlogn),并且是原地排序算法,具体是怎么实现的呢?我们一般把堆排序分为两个步骤:建堆和排序。

建堆

对于一个未排序的数组,例如 data[3,5,8,2,1,4,6],其原始的结构是这样的:

可以看到第一个非叶子节点是 8,所以我们从 8 开始从上往下堆化,然后依次是 5 - 3,堆化后的效果就是这样的:

这样,我们就将一个无序的数组堆化成了具有堆的性质的数据,还需要说明以下,如果确定一个堆的第一个非叶子节点是多少呢?实际上,对于长度为 length 的数组,(length - 2) / 2下标对应的数据,就是堆中的第一个非叶子节点。接下来的操作就是排序了。


排序

排序的过程类似于上面说到的删除堆顶元素,因为堆顶元素是堆的最大或最小元素,以大顶堆为例,我们只需要将堆顶元素和数组中最后一个元素交换位置,然后重新构造堆,继续交换堆顶元素和数组中最后一个未排序数据,知道堆中元素剩下最后一个。

示意图如下:

整个建堆和排序的实现的代码也贴在这里:

//堆排序
    public void heapSort(int[] data){
        int length = data.length;
        if (length <= 1) return;
        //建堆
        buildHeap(data);
        while (length > 0){
            swap(data, 0, --length);
            heapify(data, length, 0);
        }
    }
    //建堆
    //从非叶子节点依次堆化
    private void buildHeap(int[] data){
        int length = data.length;
        for (int i = (length - 2) / 2; i >= 0; -- i) {
            heapify(data, length, i);
        }
    }
相关文章
|
11天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
25 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
67 1
|
2月前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
51 5
【数据结构】优先级队列(堆)从实现到应用详解
|
1月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
1月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
29 0
|
1月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现