注意本文说的堆是数据结构中的堆,而不是java内存模型中的堆。java
n个元素的序列{k1, k2, …, kn}当且仅当知足如下关系时,称之为堆。若堆顶元素最小,则称之为小顶堆或小根堆。若堆顶元素最大,则称之为大顶堆或大根堆。以下图所示。算法
若以一维数组做为堆的存储结构,并将该一维数组当作是一个彻底二叉树,则彻底二叉树中全部非终端结点的值均不大于(或不小于)其左、右孩子结点的值。数组
堆顶元素(或彻底二叉树的根)是堆中最小值(或最大值)。数据结构
最后一个非终端结点是第个元素。ui
向上移动又有人称其为上浮,是将一个元素与其父结点比较大小,不符合堆的条件就交换位置,交换后继续与新的父结点比较,如此循环,直到符合堆的条件为止。如上图所示,若是是小根堆,而该元素却小于父结点,那么就须要将其向上移动,移动后再与新的父结点比较,以此类推,直到找到某个位置,它再也不小于父结点,则该知足堆的条件,移动结束。spa
参考代码:code
private void shiftUp(int k) {
while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
swap(k, parent(k));
k = parent(k);
}
}
复制代码
向下移动又有人称其为下沉,是将一个元素与其孩子结点进行比较与调整。如上图所示,若是是小根堆,用该结点与其左右孩子中较小的一个结点比较,若是大于那个孩子,则与其交换,交换后继续与新的孩子结点比较,以此类堆,直到找到其合适位置为止。cdn
参考代码:blog
private void shiftDown(int k) {
while(left(k) < _heapSize) {
int j = left(k);
if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
if(_heap[k] < _heap[j]) break;
swap(k,j);
k = j;
}
}
复制代码
插入也就是向堆中加入新成员的操做。那么新成员放在哪里呢?放在最后。那放在最后是否是可能破坏堆的结构啊?没错。怎么办?将其向上移动。排序
参考代码:
public void insert(int v) {
if(_heapSize >= _maxSize) return; //如超出容量,这里只简单地返回,实际中请根据需求进行处理
_heap[_heapSize] = v;
shiftUp(_heapSize);
_heapSize++;
}
复制代码
删除也就是堆顶元素被拿走了。群龙无首这下怎么办?别急咱们要选出新的堆顶。下面我来告诉你怎么办,首先把堆中最后一个元素搬到堆顶,而后将其向下移动。对,就这么简单。
参考代码:
public int delMin() throws Exception {
if(_heapSize == 0) {
throw new Exception("The heap is already empty!");
}
int max = _heap[0];
_heapSize--;
swap(0, _heapSize);
shiftDown(0);
return max;
}
复制代码
我的理解堆的建立其实就是将一个序例经过某些操做,使其知足堆的条件从而转化为堆的过程。也就是你给我一个序列,我还你一个堆!
那么如何去搞呢? 有两种方式:
1)逐个插入。插入操做会自觉保证插入后该序列仍然是堆。
参考代码
public MinHeap(int[] initialNums, int maxSize) {
_maxSize = maxSize;
_heap = new int[maxSize];
//请看关键代码,逐个插入
for(int i = 0; i < initialNums.length; i++) {
insert(initialNums[i]);
}
}
复制代码
2)逐个调整。从最后一个非终端结点开始,向前,逐个调整以各个非终端结点为根的子树,使每棵子树都变成堆,等最后一个非终端结点调整完毕,整个序列就变成了堆。
参考代码
public MinHeap(int[] initialNums, int maxSize) {
_maxSize = maxSize;
_heap = Arrays.copyOf(initialNums, initialNums.length);
_heapSize = initialNums.length;
//从最后一个非终端结点开始逐棵子树调整
for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
shiftDown(i);
}
}
复制代码
该代码简单实现了小顶堆的建立、插入、删除等操做。但愿可以辅助读者理解。为简单起见,这里只接收int类型数据。
package just.doit;
import java.lang.Exception;
public class MinHeap {
private int _heapSize = 0;
private int _maxSize;
private int[] _heap = null;
// public MinHeap(int[] initialNums, int maxSize) {
// _maxSize = maxSize;
// _heap = Arrays.copyOf(initialNums, initialNums.length);
// _heapSize = initialNums.length;
//
// //从最后一个非终端结点开始逐棵子树调整
// for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
// shiftDown(i);
// }
// }
public MinHeap(int[] initialNums, int maxSize) {
_maxSize = maxSize;
_heap = new int[maxSize];
//逐个插入
for(int i = 0; i < initialNums.length; i++) {
insert(initialNums[i]);
}
}
public void insert(int v) {
if(_heapSize >= _maxSize) return; //如超出容量,这里只简单地返回,实际中请根据需求进行处理
_heap[_heapSize] = v;
shiftUp(_heapSize);
_heapSize++;
}
public int delMin() throws Exception {
if(_heapSize == 0) {
throw new Exception("The heap is already empty!");
}
int max = _heap[0];
_heapSize--;
swap(0, _heapSize);
shiftDown(0);
return max;
}
public void printMinHeap() {
for(int i = 0; i < _heapSize; i++) {
System.out.print(_heap[i]+" ");
}
System.out.println();
}
private void shiftUp(int k) {
while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
swap(k, parent(k));
k = parent(k);
}
}
private void shiftDown(int k) {
while(left(k) < _heapSize) {
int j = left(k);
if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
if(_heap[k] < _heap[j]) break;
swap(k,j);
k = j;
}
}
private void swap(int i, int j) {
int temp = _heap[i];
_heap[i] = _heap[j];
_heap[j] = temp;
}
//本代码从0开始存储,因此left为2 * k + 1,若从1开始存储则left为2 * k
private int left(int k) {
return 2 * k + 1;
}
//本代码从0开始存储,因此right为2 * k + 2,若从1开始存储则right为2 * k + 1
private int right(int k) {
return 2 * k + 2;
}
//本代码从0开始存储,因此parent为(k - 1) / 2,若从1开始存储则parent为k / 2
private int parent(int k) {
return (k - 1) / 2;
}
public static void main(String[] args) throws Exception {
int[] a = {10,33,1,4,3,29,5,8};
MinHeap maxHeap = new MinHeap(a, 20); //使用逐个插入的方式构建堆
maxHeap.printMinHeap(); // 1 3 5 8 4 29 10 33
System.out.println(maxHeap.delMin()); // 取出堆顶元素 1
maxHeap.printMinHeap(); //取出堆顶元素后的新堆 3 4 5 8 33 29 10
maxHeap.insert(6); // 插入 6
maxHeap.printMinHeap(); // 插入后的新堆 3 4 5 6 33 29 10 8
}
}
复制代码
堆的使用场景包括但不限于一下三种。
有了上面的基础,堆排序的思路很简单,给一个序列,先将其构建成堆,堆顶元素确定是最大(或最小值),将堆顶元素放到序列末尾,并把末尾元素补充到堆顶,并对其进行向下调整,调整到n-1位置为止,这样前n-1个元素又是一个堆,又能够取到第二大(或第二小)的值,以此类推,直到堆只剩下一个元素,将获得一个有序序列。
以下代码是经过构建小根堆,将int数组从大到小排序:
public static void heapSort(int[] initialNums) {
int[] heap = buildMinHeap(initialNums);
for(int i = heap.length - 1; i > 0; i--) {
swap(heap, 0, i);
shiftDown(heap, 0, i);
}
}
复制代码
package just.doit;
import java.util.Arrays;
public class Sort {
public static void heapSort(int[] initialNums) {
int[] heap = buildMinHeap(initialNums);
for(int i = heap.length - 1; i > 0; i--) {
swap(heap, 0, i);
shiftDown(heap, 0, i);
}
}
private static int[] buildMinHeap(int[] initialNums) {
for(int i = ((initialNums.length - 1) / 2); i >= 0; i--) {
shiftDown(initialNums, i, initialNums.length);
}
return initialNums;
}
private static void shiftDown(int[] heap, int k, int heapSize) {
while(left(k) < heapSize) {
int j = left(k);
if(right(k) < heapSize && heap[right(k)] < heap[left(k)]) j++;
if(heap[k] < heap[j]) break;
swap(heap,k,j);
k = j;
}
}
private static void swap(int[] heap, int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
private static int left(int k) {
return 2 * k + 1;
}
private static int right(int k) {
return 2 * k + 2;
}
public static void main(String[] args) {
int[] a = {5,23,7,33,2,1,16,9};
System.out.println("排序前:" + Arrays.toString(a));
Sort.heapSort(a);
System.out.println("堆排序后:" + Arrays.toString(a));
}
}
输出:
排序前:[5, 23, 7, 33, 2, 1, 16, 9]
堆排序后:[33, 23, 16, 9, 7, 5, 2, 1]
复制代码
堆能够用来实现优先队列(Priority Queue)。说到队列,你们马上会想到先进先出。根据名字来看,优先队列彷佛不同。没错,它根据元素的优先级来决定取出顺序。关于优先队列这里不过多讲述。
例如给了一百万个数据,我想找到最大的100个数据。那么我能够先拿100个元素建一个小根堆,而后一个一个取剩下的元素与堆顶比较,若是大于堆顶,则把堆顶删除,再把这个元素放入堆中。若是小于堆顶,则不作处理。最后堆中100个元素则为最大的100元素。
以上则为做者对堆的一些认识与总结,但愿能给读者一些启发。若有不妥之处,但愿能获得批评指正!
结尾与君共同赏古诗一首,愿君更上一层楼!
登鹳雀楼
[唐] 王之涣
白日依山尽,黄河入海流。
欲穷千里目,更上一层楼。
复制代码
《数据结构》 严蔚敏 吴伟民 编著 《算法导论》 殷建平 徐云 等译 《算法》 谢路云 译