数据结构之堆(优先级队列)
“愿独立的你,在随心而行的途中,学会释怀而止,而非一时放纵之后而任性非为”
这好像是我第一次写关于数据结构的文章吧,关于数据结构,那真的是太奥秘了,想领略其中的奥秘,必须得付出相应的努力呀!!!
这篇文章呢,将会给大家介绍一种“堆”的数据结构,话不多说,直接开整!
堆:
堆是一种特殊的树形数据结构,通常用于实现优先级队列。堆的特点是满足堆的性质,即每个节点的值都大于或等于(大根堆)或者小于或等于(小根堆)其子节点的值。堆通常以数组的形式实现!
堆的特性:
1.完全二叉树:堆是一种完全二叉树,除最后一层外,其他层的节点都被填满,最后一层的节点从左到右填充。
2.堆性质:
- 大根堆:每个节点的值都大于或等于其节点的值,根节点是最大值
- 小根堆:每个节点的值都小于或等于其子节点的值,根节点是最小值
堆的应用:
- 优先级队列:堆通常用于实现优先级队列,支持高效的插入和删除操作
- 排序算法:堆排序是一种基于堆的数据结构的排序算法,时间复杂度为O(n*log n)
小根堆示例:
大根堆示例:
堆化:
将一个无序数据转换为堆结构。
假设我们有一个无序数组{29,56,34,19,52,16,74,46,92,39},我们怎样将它转换成是一个堆结构呢?
这里我是以大根堆为例,进行展开下面的内容,我们可以以最后一棵子树进行逐个“回退”地向下调整。定义一个parent记录其父节点,定义一个child记录其子节点。那么我们根据其数组的长度就可以很好的知道其最后一棵子树的父节点和子节点。
父节点:对于节点的索引 i
,其父节点的索引可以通过以下公式计算:
父节点索引=⌊i−12⌋
这里,i
是当前节点的索引,floor
表示向下取整。
左子节点:对于节点的索引 i
,其左子节点的索引可以通过以下公式计算:
左子节点索引=2i+1
右子节点:对于节点的索引 i
,其右子节点的索引可以通过以下公式计算:
右子节点索引=2i+2
parent依次减减,这样我们就可以遍历到每一棵子树到整棵树。那么在第四次调整的时候,是否也是和前三次调整一样如此轻松呢?
那么在进行调整的时候,我们要注意调整的时候,会不会影响到其他其中一颗子树的结构,所以我们应该继续向下进行调整,那么要继续向下调整到什么时候呢?是不是当我们的child下标大于我们的数组长度的时候,是不是就可以不用继续向下调整了呢?那么基于上述的讲述,我们就可以实现一些代码了! 我们先看看最终的画图结果,然后后面再看看代码运行出来的结果是否和画图上展示的一样。
public class myheap {private int[] elem;//记录数组中元素的个数private int usedSize;public myheap() {elem = new int[10];}//初始化public void init(int[] array) {for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}public void createHeap() {for(int parent = (usedSize-1-1)/2; parent >= 0;parent--) {siftDown(parent,usedSize);}}private void siftDown(int parent, int end) {//计算孩子节点的下标int child = parent*2+1;while(child < end) {//先找到孩子节点中较大值的孩子节点下标(注意此处右孩子是否存在)if(child+1 < end && elem[child] < elem[child+1]) {child++;}//比较孩子节点和父节点的值,如果父节点小于子节点的值,就进行交换if(elem[parent] < elem[child]) {swap(child,parent);//交换之后,继续进行向下调整parent = child;child = parent*2+1;}else{break;}}}//交换private void swap(int child, int parent) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;}
}
测试代码:
public class testMyHeap {public static void main(String[] args) {int[] array = new int[]{29,56,34,19,52,16,74,46,92,39};myheap heap = new myheap();heap.init(array);heap.createHeap();}
}
接下来进行打断点来看看是否和上面的图中结果一样:
结果是一样的,所以说没有问题。
堆插入:
将新元素添加到堆中,通常将其放在数组的末尾。
在进行插入的时候,我们首先要考虑的问题应该是数组是否还有空间进行插入,如果不够的话,便进行扩容!!!
插入的时候,我们应该怎么进行调整呢?如果我们新插入的节点比在其父节点小呢?因为此时我们是一个大根堆的形式,所以这种情况下,不需要进行任何处理。那如果是插入的值很大呢,我们是不是要和在其父节点进行交换之后,然后再往上面继续进行调整呢?那么也是要考虑何时能够停止,是不是应该是parent小于0时就结束此次向上调整呢?所以我们的堆插入也是很简单的!
假设我们依次插入数据{29,56,34,19,52},我们先来画画图,以便验证后续的代码。
public void offer(int key) {if(isFull()) {elem = Arrays.copyOf(elem,2* elem.length);}elem[usedSize] = key;usedSize++;siftUp(usedSize-1);}private void siftUp(int child) {int parent = (child-1)/2;while(parent >= 0) {if(elem[parent] < elem[child]) {swap(child,parent);//交换之后,继续向上调整child = parent;parent = (child-1)/2;}else{break;}}}private boolean isFull() {return usedSize == elem.length;}
堆删除:
删除的时候,一般是删除“堆顶”元素。
在删除堆顶元素时,我们一般的操作是先将堆尾元素与堆顶元素进行交换后,那么交换之后呢,我们就从0位置开始向下进行调整了,没有影响到的其他子树,我们不需要管。基于上面的知识后,对于这个删除来说就不用多谈了,直接上代码。
public int poll() {if(isEmpty()) {return -1;}int ret = elem[0];swap(0,usedSize-1);usedSize--;siftDown(0,usedSize);return ret;}private boolean isEmpty() {return usedSize == 0;}
堆排序:
堆排序是一种基于比较的排序算法,利用堆这种数据结构来实现排序。
假设我们是进行从小到大进行排序,因为堆排序是基于堆来实现的,所以当我们弄好一个堆结构时,我们再进行排序的操作(大根堆为例)。此时是一个大根堆,那么堆顶元素就是最大的数了,此时我们就交换堆顶元素和堆尾元素,那么最大的数就到了最后面了,这个位置就是它该到的位置。我们调整了0位置,所以要从0位置开始向下进行调整,使后面的最大的数到堆顶后,我们重复上述的步骤即可!
public void sortHeap() {int endIndex = usedSize-1;while(endIndex > 0) {swap(0,endIndex);siftDown(0,endIndex);endIndex--;}}
认识堆之后,我们来简单认识一下优先级队列!
优先级队列:
数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数
据结构就是优先级队列(Priority Queue),JDK1.8中的PriorityQueue底层使用了堆这种数据结构。
关于PriorityQueue的使用的注意事项:
1. 使用时必须导入PriorityQueue所在的包
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
从输出的结果中可以看出,默认是以小根堆的形式创建的。那么如果我们想要建立一个大根堆呢,其实也是可以的。
- 希望通过本篇博客,读者能够对堆有一个更好的理解。
本期节目到此结束,拜拜!!!