堆(英语:heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。接下来我们就探讨下常见的堆,最大堆。
要点
一、 二叉树补充
- 满二叉树:除了孩子节点其他的节点左右孩子都不为空。(如下图)
- 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
完全二叉树简单理解:把元素按照从上而下。从左到右排成二叉树形状。排好后整个树右下角还有空余。(参考下图)
二、 概念分类
1、堆(二叉堆):满足特殊概念的二叉树
条件:
- 二叉堆是一个完全二叉树
- 堆中每个节点的值总是不大于其父亲节点的值(根节点是最大的元素)
满足以上两个条件就是堆,而且是堆中的最大堆的定义。
条件:
- 二叉堆是一个完全二叉树
- 堆中每个节点的值总是小于等于其孩子节点的值(根节点为最小元素)
满足以上两个条件就是堆,而且是堆中的最小堆的定义。
ps:由于最大堆和最小堆差不多我们本文就探讨最大堆。
2、最大堆的设计探讨
思考:如上图为是一个完全二叉树,当然,满足最大堆的性质,也是最大堆。但是在api设计上有个巧妙的设计。使用数组实现。
1 如上图我们从根节点自上而下,自左而由给完全二叉树添加索引,这不正好和数组很像吗?(只不过此图的索引从1开始的,我们从0定义一样)
2 此时我们使用数组的话我们需要解决的问题是任意一个元素左右孩子是谁.
如上图我们观察索引可以得出某个元素与的父节点、左孩子、有孩子之间关系。
例如:41索引为2, 他的父亲索引为2/i = 1,左孩子索引为2i = 4,右孩子为2i+1 = 5
ps:我们进行的int类型的乘除需要留意下。
所以如上图如果索引从0开始,我们求某元素的父亲节点索引,左孩子索引、右孩子索引也可推导出。
ps:本文就以索引为0的开始探讨
3类的设计
本文就以数组为底层,而且是我们以前封装的动态数组作为底层设计,有关动态数组可以参考“深入探究数组”
3.1、首先简单的设计最大堆(MaxHeap)
public class MaxHeap<E extends Comparable<E>> {
private AutoArray<E> data;//以我们的动态数组为底层实现
/**
* 构造 初始化容量
*/
public MaxHeap(int capacity) {
data = new AutoArray<>(capacity);
}
/**
* 构造 使用默认容量(参看数组底层源码)
*/
public MaxHeap() {
data = new AutoArray<>();
}
/**
* 堆存的元素个数
*/
public int size() {
return data.getSize();
}
/**
* 堆 判空
*/
public boolean isEmpty() {
return data.isEmpty();
}
// 下面三个辅助函数。 父亲节点,左孩子 有孩子 与数组索引关系(通过堆的完全二叉树树模型图推导)
/**
* 返回 给定索引所代表元素父节点的索引
*/
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("root node don't have parent");
}
return (index - 1) / 2;// 公式 参考推导图
}
/**
* 返回 给定索引所代表元素左孩子节点的索引
*/
private int leftChild(int index) {
return index * 2 + 1;// 公式 参考推导图
}
/**
* 返回 给定索引所代表元素有孩子节点的索引
*/
private int rightChild(int index) {
return index * 2 + 2;// 公式 参考推导图
}
}
可以看出没啥东西主要是以数组为成员,我们又封装了parent,leftChild,rightChild这三个工具方便我们使用。
3.2 添加元素的siftUp
如图我们以数组为底层添加到末尾十分简单,这时我们只需要考虑最大堆的性质就行了
性质: 堆中每个节点的值总是不大于其父亲节点的值(根节点是最大的元素)
所以我们添加后元素要不断上浮与父节点比较,看位置是否合适。
于是add设计如下
/**
* 向堆中添加元素
*/
public void add(E e) {
// 1 添加到末尾
data.addLast(e);
// 维护堆的性质(根父亲节点比较,看值是否合适,比父亲节点大就对调位置上浮,直到满足条件)
siftUp(data.getSize() - 1);// 左后一个元素的索引
}
/**
* @param k 元素的索引
* 元素上浮
*/
private void siftUp(int k) {
// E parent = data.get(parent(k));
//循环执行: 当前元素与父节点左比较,不符合就交换位置 ,直到符合为止
while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {
data.swap(k, parent(k));
k = parent(k);
}
}
3.3取出元素的siftdown
我们的堆中是只允许取出根节点的元素的 也就是最大堆的最大值。
/**
* 堆中的最大元素
* <p>
* 最大堆中第一个元素就是最大元素
*/
public E maxValue() {
if (data.getSize() == 0) {
throw new IllegalArgumentException("heap is empty");
}
return data.get(0);
}
/**
* 取出堆中的最大元素
*/
public E extractMax() {
// 1 找出最大元素
E maxV = maxValue();
//2 吧最后一个元素替换最大元素
data.swap(0, data.getSize() - 1);
data.removeLast();
// 3 元素下浮处理(与左右节点比较)
siftDown(0);
return maxV;
}
/**
* 元素下沉处理
*/
private void siftDown(int k) {
// 假如有左孩子时
// 从根节点开始 当左孩子的 索引大于元素的最大索引时结束循环
while (leftChild(k) < data.getSize()) {
int j = leftChild(k);// 记录左右孩子中最大孩子的索引,开始时默认左孩子的索引。
// 如果有右孩子时 切有孩子比左大
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {
j = rightChild(k);// 较大孩子的索引为又孩子的
/*
* 如果没有有孩子,或者又孩子的节点存的值比左孩子小 if不成立,则孩子中最大值为左孩子
* j就是用左孩子的索引
* */
}
// 根节点 给 最大孩子比较 满足条件结束循环
if (data.get(k).compareTo(data.get(j)) >= 0) {
break;
}
data.swap(k, j);//更换元素
k = j;//给k重新赋值
}
}
实现思路参考注释详解
3.4 替换堆根节点元素replace
/**
* @param e 用户传来的元素
* <p>
* 思路:替换堆顶元素,执行siftdown 满足堆的性质
* @function 将堆顶的元素替换为 用户传来的元素
* <p>
* 注意:使用的组合也可完成 取出最大元素(maxValue),添加末尾add(), 两次 o(logn) 操作
* 本思路就一次 o(logn)
*/
public E replace(E e) {
E temp = data.get(0);
data.set(0, e);
siftDown(0);
return temp;
}
留意下通过组合方式实现还有本思路实现细节。
3.5 堆的heapify
/**
* Heapify
* 将任意数组整理成堆的形状
* 写成构造就行了
*/
public MaxHeap(E[] arr) {
data = new AutoArray<>(arr);// 元素存入数组,当成完全二叉树
// 在整理成堆
for (int i = parent(arr.length - 1); i >= 0; i--){
siftDown(i);
}
}
这样一个最大堆我们就实现了
4、 基于堆的优先队列(PriorityQueue)设计
我们前面就通过线性表实现了队列,可以使用不同的数据结构,同样优先队列也是,为了高效我们使用堆实现(本文使用最大堆)
package heap;
import queue.Queue;
/**
* Create by SunnyDay on 2019/03/09
* 基于堆(最大堆)的优先队列
*
*ps:java原装的优先队列底层使用的最小堆
*/
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.maxValue();
}
@Override
public E dequeue() {
return maxHeap.extractMax();
}
@Override
public void enqueue(E e) {
maxHeap.add(e);
}
// todo 使用线性结构实现(只是底层实现不同 时间复杂度可能不同)
}
有了前面的封装我们很快就完成了!
五、拓展
1 、本文主要探讨了二叉堆的最大堆,其实还有d叉堆,索引堆等等,这些我们有机会慢慢了解吧!
2java原生的优先队列(PriorityQueue)底层是最小堆实现
六、BAT经典面试应用例题
给定100万个数 求前100大的数(给定N(N是个大数字)个数,求前M个最大数)
1、起初看这道题我们也许会想到排序,把这100万个数据排序,然后取出前100即可。这样实现是没错的。可是注意看我们的要求,输入的数字量级是百万级以上的,如果单纯为了实现结果,排序是没错的,但是对百万级的数据进行排序,不管使用任何排序都会花费很多时间。并且float在java中占4个字节,1000000*4/1024/1024=3.8 G,这样庞大的数据同样需要考虑到空间复杂度的问题,排序算法基本都需要额外的空间,如果对这样大的数据进行排序是不可能的。
2 或许我们会想到快速排序、归并排序。这时时间复杂度为nlogn级别也行,但是还有更效率的使用最小堆(复杂度nlogM)。
分析:由于数据较大打印不好看结果,我们就模拟100个数,取出前10最大的。
我们先把前十个维护在一个最小堆中,然后不断遍历后面90个数据,比我们堆中最小值大我们就替换堆中最小值即可。由于java原装的优先队列使用最小堆实现我们就是用java util包下的优先队列实现。
package heap;
import sun.rmi.runtime.Log;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Random;
/**
* Create by SunnyDay on 2019/03/09
*/
public class Demo {
public static void main(String[] args) {
float arr[] = new float[100];
for (int i = 0; i < 100; i++) {
Random random = new Random();
float v = random.nextInt(1000);
arr[i] = v;
}
Arrays.sort(arr);
for (int i = 0; i < 100; i++) {
System.out.println("第" + (i + 1) + "个数" + arr[i]);
}
// 首先让前10个放入优先队列
PriorityQueue<Float> priorityQueue = new PriorityQueue<>();
for (int i = 0; i < 10; i++) {
priorityQueue.add(arr[i]);
}
for (int i=11;i<100;i++){
if (arr[i]> priorityQueue.peek()){
priorityQueue.remove();
priorityQueue.add(arr[i]);
}
}
for (int i = 0; i < 10; i++) {
System.out.println(priorityQueue.remove());
}
}
}
打印结果:
…
第91个数905.0
第92个数912.0
第93个数912.0
第94个数914.0
第95个数916.0
第96个数932.0
第97个数937.0
第98个数975.0
第99个数982.0
第100个数986.0
队列中最终数字:
905.0
912.0
912.0
914.0
916.0
932.0
937.0
975.0
982.0
986.0
结果正确
友情链接:leetcode347比我们的测试题难点,题型类似。
源码下载:heap
小结
吧堆简单的探讨了一下,本文主要探讨了最大堆的底层实现,有了此基础,相信再看其他堆不会太迷茫。