内容简介:堆(heap)又被为优先队列(priority queue)。尽管名为优先队列,但堆并不是队列。因为队列中允许的操作是先进先出(FIFO),在队尾插入元素,在队头取出元素。而堆虽然在堆底插入元素,在堆顶取出元素,但是堆中元素的排列不是按照到来的先后顺序,而是按照一定的优先顺序排列的。
堆(heap)又被为优先队列(priority queue)。尽管名为优先队列,但堆并不是队列。
因为队列中允许的操作是先进先出(FIFO),在队尾插入元素,在队头取出元素。
而堆虽然在堆底插入元素,在堆顶取出元素,但是堆中元素的排列不是按照到来的先后顺序,而是按照一定的优先顺序排列的。
本文通过堆的实现、最小堆(最大堆)、堆的时间复杂度、优先队列的实现、堆 排序 来介绍「 堆 」。
堆的实现
堆的一个经典的实现是完全二叉树(complete binary tree),这样实现的堆称为二叉堆(binary heap)。
这里来说明一下满二叉树的概念与完全二叉树的概念。
满二叉树:除了叶子节点,所有的节点的左右孩子都不为空,就是一棵满二叉树,如下图。
可以看出:满二叉树所有的节点都拥有左孩子,又拥有右孩子。
完全二叉树:不一定是一个满二叉树,但它不满的那部分一定在右下侧,如下图
堆的特性:
- 必须是完全二叉树
- 任一结点的值是其子树所有结点的最大值或最小值
- 最大值时,称为“最大堆”,也称大顶堆;
- 最小值时,称为“最小堆”,也称小顶堆。
堆的基础实现
只要谨记堆的定义特性,实现起来其实是很容易的。
- 特性1. 维持完全二叉树
- 特性2. 子类数字总是大于父类数字
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;
}
}
复制代码
最小堆的插入(ADD)
假设现有元素 5 需要插入,为了维持 完全二叉树 的特性,新插入的元素一定是放在结点 6 的右子树;同时为了 满足任一结点的值要小于左右子树的值 这一特性,新插入的元素要和其父结点作比较,如果比父结点小,就要把父结点拉下来顶替当前结点的位置,自己则依次不断向上寻找,找到比自己大的父结点就拉下来,直到没有符合条件的值为止。
动画讲解:
- 在这里先将元素 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来到新的位置,使得循环可以查看新的位置是否还要大。
}
}
复制代码
最小堆的删除(DELETE)
核心点:将最后一个元素填充到堆顶,然后不断的下沉这个元素。
假设要从节点 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, 考虑一个元素在堆中向下移动的距离。
- 大约一半的结点深度为 d-1 ,不移动(叶)。
- 四分之一的结点深度为 d-2 ,而它们至多能向下移动一层。
- 树中每向上一层,结点的数目为前一层的一半,而子树高度加一
堆有 logn 层深,所以插入删除的平均时间和最差时间都是 O(logN)
优先队列(priority_queue)
普通队列是一种先进先出的数据结构,先放进队列的元素取值时优先被取出来。而优先队列是一种具有最高优先级元素先出的数据结构,比如每次取值都取最大的元素。
优先队列支持下面的操作:
- a. 找出优先级最高的元素(最大或最小元素);
- b. 删除一个具有最高优先级的元素;
- c. 添加一个元素到集合中。
代码实现
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();
}
}
复制代码
堆排序
理解了优先队列,堆排序的逻辑十分简单。
第一步:让数组形成堆有序状态;
第二步:把堆顶的元素放到数组最末尾,末尾的放到堆顶,在剩下的元素中下沉到正确位置,重复操作即可。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 《视觉开发专题》之 OpenGL 3D动画绘制&图形学概念的理解
- 【图解数据结构】 一组动画彻底理解二叉树三种遍历
- 【Android 动画】动画详解之属性动画(三)
- 【Android 动画】动画详解之属性动画(五)
- Flutter 动画全解析(动画四要素、动画组件、隐式动画组件原理等)
- 【Android 动画】动画详解之补间动画(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Algorithms Unlocked
Thomas H. Cormen / The MIT Press / 2013-3-1 / USD 25.00
Have you ever wondered how your GPS can find the fastest way to your destination, selecting one route from seemingly countless possibilities in mere seconds? How your credit card account number is pro......一起来看看 《Algorithms Unlocked》 这本书的介绍吧!