目录
一、堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
父亲节点:parent(i) = (i -1) / 2
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
代码实现最大堆:
①首先构造出堆的基本表示
构建堆的底层采用动态数组ArrayList
public class MaxHeap<E extends Comparable<E>>{
private ArrayList<E> data;
public MaxHeap(){
data = new ArrayList<>();
}
public MaxHeap(int capacity){
data = new ArrayList(capacity);
}
//返回堆中的元素个数
public int size(){
return data.size();
}
public boolean isEmpty(){
return data.isEmpty();
}
//返回index索引元素的父亲节点索引
public int parent(int index){
if(index == 0){
throw new IllegalArgumentException("root节点无父亲节点!");
}
return (index-1)/2;
}
public int leftChild(int index){
return index*2+1;
}
public int rightChild(int index){
return index*2+2;
}
}
②构建堆的添加方法
首先将该元素添加到堆的最后一个节点(直接添加到数组末尾即可),然后通过与父节点相比较进行调整成大顶堆
//向堆中添加元素
public void add(E num){
data.add(num);
//将刚刚添加进来的元素进行调整
adjustHeap(data.size()-1);
}
private void adjustHeap(int i) {
//当i不为根节点且i对应的值大于父亲节点对应的值
while (i>0 && data.get(i).compareTo(data.get(parent(i)))>0){
//与父亲节点交换值
swap(i,parent(i));
//索引变为父亲节点的索引,向上升
i = parent(i);
}
}
//交换i,j索引处的值
public void swap(int i,int j){
E temp = data.get(i);
data.set(i,data.get(j));
data.set(j,temp);
}
③查看堆顶元素但不删除
//查看堆顶元素但不删除
public E findMax(){
if(data.size() == 0)
throw new IllegalArgumentException("Can not findMax when heap is empty.");
return data.get(0);
}
④取出堆中最大元素
步骤一:将堆顶元素与最后一个元素交换,也就是下图中的50和15交换位置,
步骤二:然后将堆中最后一个元素取出,在进行堆调整
//取出堆顶元素并调整堆结构
public E extractMax(){
E max = findMax();
swap(0,size()-1);
data.remove(size()-1);
siftDown(0);
return max;
}
private void siftDown(int k) {
//当左孩子存在时
while(leftChild(k) < size()){
int index = leftChild(k);
//如果右孩子存在,且右孩子大于左孩子则用右孩子比较
if(index+1 < size() && data.get(index+1).compareTo(data.get(index))>0){
index++;
}
//如果k节点大于子孩子,则结束
if(data.get(k).compareTo(data.get(index))>0)
break;
swap(k,index);
k=index;
}
}
到了这里,堆结构基本已经构建结束了,接下来进行测试以下
二、优先队列
普通队列:先进先出、后进后出
优先队列:出队顺序和入队顺序无关,和优先级相关
下面基于堆结构来构建一个优先队列
优先队列也是队列,首先定义一个接口,该接口有队列的常用方法
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(int e);
E dequeue();
E getFront();
}
实现上面的接口
下面的代码就是通过大顶堆实现的优先队列
根据加入到优先队列中的值不同,则在优先队列中的顺序就不一样,出队顺序就会是优先级最大的。
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();
}
}
注意:Java内置了优先队列,但是它的底层是最小堆实现的,使用时需注意!
三、利用堆求TOP K
如果需要在一个包含 n 个数据的数组中,查找前 K 个数据,该如何解决呢?
首先,我们创建一个容量是为K的小顶堆,然后遍历数组向小顶堆内加入数据,在加入时只需要比较是否该数据大于堆顶数据,如果大于则插入,小于则不插入。因为小顶堆的堆顶元素是最小的,此时新加入的值比最小的大那就加入,比最小的还小了就不用加入了,最终遍历数组结束堆内K个元素就是该数组的TOP K。
同理那么如果此时是在包含 n 个数据的数组中,查找后 K 个数据,该如何解决呢?
对于查找最小的元素,我们就应该使用大顶堆了,首先创建一个容量为K的大顶堆,然后遍历数组向其中插入数据,如果待插入数据小于堆顶则插入,大于则不插入。
对于上面的两种情况,插入时需要将插入的元素和堆顶元素交换,即将堆顶元素删除。
总结就是一句话,求最大的用小顶堆,求最小的用大顶堆