堆(heap)又被为优先队列(priority queue)。尽管名为优先队列,但堆并非队列。数组
由于队列中容许的操做是先进先出(FIFO),在队尾插入元素,在队头取出元素。bash
而堆虽然在堆底插入元素,在堆顶取出元素,可是堆中元素的排列不是按照到来的前后顺序,而是按照必定的优先顺序排列的。数据结构
本文经过堆的实现、最小堆(最大堆)、堆的时间复杂度、优先队列的实现、堆排序来介绍「 堆 」。ide
堆的一个经典的实现是彻底二叉树(complete binary tree),这样实现的堆称为二叉堆(binary heap)。函数
这里来讲明一下满二叉树的概念与彻底二叉树的概念。动画
堆的特性:ui
- 最大值时,称为“最大堆”,也称大顶堆;
- 最小值时,称为“最小堆”,也称小顶堆。
只要谨记堆的定义特性,实现起来实际上是很容易的。spa
public class MinHeap <E extends Comparable<E>> {
private Array<E> data;
public MinHeap(int capacity){
data = new Array<>(capacity);
}
public MinHeap(){
data = new Array<>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回彻底二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
return (index - 1) / 2;
}
// 返回彻底二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
// 返回彻底二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
}
复制代码
假设现有元素 5 须要插入,为了维持彻底二叉树的特性,新插入的元素必定是放在结点 6 的右子树;同时为了知足任一结点的值要小于左右子树的值这一特性,新插入的元素要和其父结点做比较,若是比父结点小,就要把父结点拉下来顶替当前结点的位置,本身则依次不断向上寻找,找到比本身大的父结点就拉下来,直到没有符合条件的值为止。code
动画讲解:cdn
- 在这里先将元素 5 插入到末尾,即放在结点 6 的右子树。
而后与父类比较, 6 > 5 ,父类数字大于子类数字,子类与父类交换。
重复此操做,直到不发生替换。
Show me the code:
添加一个辅助函数,用来交换传入的索引两个位置的元素值
/**
* 交换传入的索引两个位置的元素值
*
* @param i
* @param j
*/
public void swap(int i, int j) {
if (i < 0 || i >= size || j < 0 || j >= size)
throw new IllegalArgumentException("Index is illegal.");
E temp = data[i];
data[i] = data[j];
data[j] = temp;
}
复制代码
数组中添加交换两元素位置的方法,注意下面代码中注释的描述特性位置。
/**
* 堆中添加元素方法。
*
* @param e
*/
public void add(E e) {
//特性1:新插入的元素首先放在数组最后,保持彻底二叉树的特性
data.addLast(e);
siftUp(data.getSize() - 1);
}
/**
* index 为i位置元素上浮。
*
* @param i
*/
private void siftUp(int i) {
//特性2:比较插入值和其父结点的大小关系,小于父结点则用父结点替换当前值,index位置上升为父结点
// 当上浮元素大于父亲,继续上浮。而且不能上浮到0之上
// 直到i 等于 0 或 比 父亲节点小了。
while (i > 0 && data.get(i).compareTo(data.get(parent(i))) > 0) {
// 数组Array中添加方法swap
data.swap(i, parent(i));
i = parent(i); // 这句话让i来到新的位置,使得循环能够查看新的位置是否还要大。
}
}
复制代码
核心点:将最后一个元素填充到堆顶,而后不断的下沉这个元素。
假设要从节点 1 ,也能够称为取出节点 1 ,为了维持彻底二叉树的特性 ,咱们将最后一个元素 6 去替代这个 1 ;而后比较 1 和其子树的大小关系,若是比左右子树大(若是存在的话),就要从左右子树中找一个较小的值替换它,而它能本身就要跑到对应子树的位置,再次循环这种操做,直到没有子树比它小。
经过这样的操做,堆依然是堆,总结一下:
Show me the code:
public E findMin() {
return data.get(0);
}
public E extractMin() {
E ret = findMin();
data.swap(0, data.getSize() - 1); // 0位置元素和最后一个元素互换。
data.removeLast(); // 删除此时的最后一个元素(最小值)
siftDown(0); // 对于0处进行siftDown操做
return ret;
}
/**
* k位置元素下移
*
* @param k
*/
private void siftDown(int k) {
while(leftChild(k) < data.getSize()){
int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
if( j + 1 < data.getSize() &&
data.get(j + 1).compareTo(data.get(j)) < 0 )
j ++;
// data[j] 是 leftChild 和 rightChild 中的最小值
if(data.get(k).compareTo(data.get(j)) >= 0 )
break;
data.swap(k, j);
k = j;
}
}
复制代码
对于有 n 个节点的堆来讲,其高度 d = log2n + 1
。 根为第 0 层,则第 i 层结点个数为 2i, 考虑一个元素在堆中向下移动的距离。
堆有logn
层深,因此插入删除的平均时间和最差时间都是O(logN)
普通队列是一种先进先出的数据结构,先放进队列的元素取值时优先被取出来。而优先队列是一种具备最高优先级元素先出的数据结构,好比每次取值都取最大的元素。
优先队列支持下面的操做:
public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
private MaxHeap<E> maxHeap;
public PriorityQueue(){
maxHeap = new MaxHeap<>();
}
@Override
public int getSize(){
return maxHeap.size();
}
@Override
public boolean isEmpty(){
return maxHeap.isEmpty();
}
@Override
public E getFront(){
return maxHeap.findMax();
}
@Override
public void enqueue(E e){
maxHeap.add(e);
}
@Override
public E dequeue(){
return maxHeap.extractMax();
}
}
复制代码
理解了优先队列,堆排序的逻辑十分简单。
第一步:让数组造成堆有序状态;
第二步:把堆顶的元素放到数组最末尾,末尾的放到堆顶,在剩下的元素中下沉到正确位置,重复操做便可。