需求
假如我们现在要设计一种数据结构,用来存放整数,要提供三个接口:
添加元素、获取最大值、删除最大值
我们可以使用动态数组或者双向链表来做。
获取最大值:需要遍历整个数组或者链表,因此,时间复杂度为O(n)
删除最大值:需要遍历整个数组或者链表,因此,时间复杂度为O(n)
添加元素:默认是添加到最后。
动态数组添加在最后,在知道数组size的情况下,添加元素的时间复杂度为O(1)
双向链表添加在最后,由于双向链表有first和last指针,因此,可以直接找到最后的元素,所以,添加元素的时间复杂度为O(1)
当然,我们也可以使用有序的动态数组或者双向链表来做
获取最大值:如果从小到大排序的话,因为是有序的,所以直接取出最后一个元素即可。因此,时间复杂度为O(1)
删除最大值:如果从小到大排序的话,因为是有序的,所以直接取出最后一个元素即可。因此,时间复杂度为O(1)
添加元素:默认是添加到最后。
动态数组添加在最后,在知道数组size的情况下,插入元素的时间复杂度为O(1),但是插入完毕后需要做比较,保证数组还是有序的,这就要做比较,因此,这样其时间复杂度为O(n)
双向链表添加在最后,由于双向链表有first和last指针,因此,可以直接找到最后的元素,并插入。但是插入完毕后需要做比较,保证数组还是有序的,这就要做比较,因此,这样其时间复杂度为O(n)
但是,我们只是要做获取最大值,删除最大值。就用到了全排序,维护全排序有点浪费。
当然,我们还可以使用平衡二叉搜索树BBST来做
由于BBST的最大值一定是位于最右边,因此:
获取最大值:查找最右边的元素,树的高度为logn,因此,时间复杂度为O(logn)
删除最大值:查找最右边的元素,树的高度为logn,因此,时间复杂度为O(logn)
添加元素:AVL树和红黑树的添加元素都是O(logn)
但,维护一个AVL树或者红黑树也是够复杂的。
因此,有没有更好的数据结构可以解决我们的需求呢?
这就是我们要讲的堆。
堆(Heap)
堆的获取最大值时间复杂度为O(1)
删除最大值时间复杂度为O(logn)
添加元素的时间复杂度为O(logn)
并且,堆广泛应用在Top K的问题中
什么是Top K问题?
在海量数据里面,找出前K个数据。
比如:在100万整数中,找出最大的100个数。
当然,堆是解决Top K的一种方法之一,而不是解决Top K的唯一方法。
堆是一种树状的数据结构(注意,不要跟内存存储的堆栈空间混淆)
常见的堆实现有:
二叉堆(完全二叉堆)
多叉堆
索引堆
二项堆
等等
堆的一个重要性质:任意节点的值总是>=(<=)子节点
如果任意节点的值总是>=子节点的堆,称为:最大堆、大根堆、大顶堆
如果任意节点的值总是<=子节点的堆,称为:最小堆、小根堆、小顶堆
既然要有>=或者<=,可见堆需要具备可比较性。
基本上涉及到的接口有:
int size(); // 元素的数量
boolean isEmpty(); // 是否为空
void clear(); // 清空
void add(E element); // 添加元素
E get(); // 获得堆顶元素
E remove(); // 删除堆顶元素
E replace(E element); // 删除堆顶元素的同时插入一个新元素
二叉堆(Binary Heap)
二叉堆的逻辑结构就是一棵完全二叉树,所以也叫完全二叉堆
对于完全二叉树的一些性质,二叉堆的底层(物理结构)一般用数组实现。
最大堆的添加
最大堆的删除
批量建堆
Top K问题
从n个整数中,找出最大的前k个数(k远远小于n)
如果使用排序算法进行全排序(1,2,3…),需要O(nlogn)的时间复杂度
如果使用二叉堆来解决,可以达到O(nlogk)的时间复杂度
过程:
新建一个小顶堆
扫描n个整数
先将遍历到的前k个数放入堆中
从k+1个数开始,如果大于堆顶元素,就使用replace操作(删除堆顶元素,将第k+1个元素添加到堆中)
扫描完毕后,堆中剩下的元素,就是数据里面的最大的k个元素
同样,如果是查找一大堆数据里面的前k个最小元素
就使用大顶堆
如果小于堆顶元素,则使用replace操作
堆的实用—优先级队列
优先级队列(priority queue)
普通队列都是FIFO
优先级队列是都可以进,但每次取优先级最高的元素出队。
这不正好是堆的特性,因此,可以利用最大堆或者最小堆来实现优先级队列。