最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。 ----百度百科。
我用俗话说吧。
- 数组来实现二叉树,所以满足二叉树的特性。
- 根元素是最小的元素,父节点小于它的两个子节点。
- 树中的元素是相对有序的。
如何实现堆的相对有序是关键。
插入元素时,插入到数组中的最后一个元素的后面,然后与该节点的父节点比较大小。如果插入的元素小于父节点元素,那么与父节点交换位置。然后插入元素交换到父节点位置时,又与该节点的父节点比较,直到大于父节点元素或者到达堆顶。该过程叫做上浮,即插入时上浮。
移除元素时,只能从堆顶移除元素,再取最后一个元素放到堆顶中。然后堆顶节点与子节点比较时,先取子节点中的较小者,如果堆顶节点大于较小子节点,那么交换位置。此时堆顶节点元素交换到较小子节点上。然后再与其较小子节点比较,直到小于较小子节点或者到达叶子节点为止。该过程叫做下沉,即移除元素时下沉。
先看数组元素对应的父子关系。先感受感受灵魂画法。
上面两个在数据结构上没什么区别,但是在实现上有所差别。
父子节点的下标关系
- 未使用index = 0元素的数组的父子节点的下标关系,即图一:
k=父节点的index -> 左子节点的index = 2k, 右子节点的index = 2k + 1
j = 子节点的index -> 父节点的index = j / 2
2. 使用index = 0元素的数组的父子节点的下标关系,即图二:
k=父节点的index -> 左子节点的index = 2k + 1, 右子节点的index = (k + 1) x 2
j = 子节点的index -> 父节点的index = (j -1) / 2
父子节点的下标关系是实现上浮和下沉的关键,选取节点的判断逻辑就是依靠上面的下标关系的。
下面来分析上浮和下沉的过程。
最小堆使用的数组,但是表现出来的数据结构是二叉树,所以上浮和下沉用树出来。 各位,原谅我画图太烂, 网上找了一下,没找到适合的画图工具,麻烦各位推荐一下。所以下面又要启动我的终极技能, 灵魂画术。
先分析上浮过程:
- 上面这个最小堆上插入一个元素,值= 2。放入最后一个元素中。
2. 与父节点的值比较,此处父节点的值=4,即插入的值小于父节点的值(2 < 4),那么交换。交换后如下图所示。
3. 交换之后,又与该节点的父节点进行比较,此处父节点的值=3, 即插入的值还是小于父节点的值(2 < 3),那么交换,交换后如下图所示。
4. 交换之后,还是与该节点的父节点进行比较,此处父节点的值=1, 即插入的值大于父节点的值(2 >1),那么上浮结束。插入元素已找到适合它的位置。
接着分析下沉过程。还是拿这份数据来分析。
下沉发生在移除元素时,先移除 值=1的堆顶元素。如图11所示。
将最后一个 值= 4的元素提到堆顶。如图12所示。
再与子节点中的较小者比较。
子节点的较小者是 值=2的左子节点。此处值 = 4的堆顶元素(父节点) > 值=2的左子节点,则发生交换。此时 值=4的元素到达左子节点中。
然后再与其子节点中的较小者比较,此处的较小者子节点恰好又是左子节点。
此处 值 = 4的父节点 > 值 = 3的左子节点, 则发生交换。此时 值 = 4的元素又到达左子节点中。
然后 值=4的节点,再次与子节点中的较小者比较,此处因为只有一个子节点了,且父节点的值 < 子节点。所以不发生交换。则此时值=4的元素已经到达适合它的节点。
至此上浮和下沉已经分析完了。接下来就动手写一个最小堆。
/**
* 最小堆的超简化版实现
* Created by wang007 on 2018/6/12.
*/
public class MinHeap {
private int[] values = new int[16] ;
private int size;
/**
* 移除并获取一个堆顶元素
* @return 堆顶元素
*/
public int poll() {
if(size <= 0) throw new IllegalStateException("不存在元素");
int value = values[0];
values[0] = values[--size] ; //将最后一个元素提到堆顶
values[size] = 0 ; //清空最后一个的数据
fixDown(); //下沉操作
return value;
}
/**
* 下沉
* @return 下沉到适合位置的index
*/
private int fixDown() {
int f = 0 ; //父节点的index
int k ; //较小者子节点的index
while((k = (f << 1) + 1) < size) { //至少存在左子节点
if(k < size - 1) { //存在右子节点
if (values[k] > values[k + 1]) k++; //左右子节点进行比较。
}
if(values[f] <= values[k]) break; //父节点小于较小者子节点,则找到合适的位置,退出循环
int temp = values[f] ; values[f] = values[k]; values[k] = temp ;
f = k ;
}
return f;
}
/**
* 上浮
* @return
*/
private int fixUp() {
int j = size -1 ; //最后一个元素的下标
int f ; //父节点的下标
while((f = ((j -1) >>1)) >= 0) { //通过父节点的下标
if(values[f] <= values[j]) break; //父节点的值小于子节点的值,则打适合的位置。
int temp = values[f] ; values[f] = values[j]; values[j] = temp ;
j = f ;
}
return f;
}
/**
* 添加一个元素在最小堆中上
* @return
*/
public int push(int item) {
if(size >= values.length) Arrays.copyOf(values, size << 1) ;
values[size++] = item ;
return fixUp();
}
public static void main(String[] args) {
MinHeap heap = new MinHeap();
heap.push(4);
heap.push(2);
heap.push(7);
heap.push(9);
heap.push(1);
heap.push(5);
heap.push(10);
heap.push(3);
heap.push(2);
for (int i = 0 ;i< 9; i++) {
System.out.println(heap.poll());
}
}
}
为什么要讲这个最小堆呢? 因为定时任务的延迟队列就是使用最小堆实现的。 例如Timer,
ScheduleExecutorService
各位看官,看到这些可爱的图,难道就不想点个赞再走嘛? ~~