优先队列和堆
一、优先队列
普通队列:先进先出;后今后出
优先队列:出队顺序和入队顺序无关;和优先级相关
- 优先队列本质也是队列,因此接口是没有变化的。但是在实现接口方法的时候具体的方法是需要做改变的。
- 堆的基本结构
二叉堆:满足一些特殊性质的二叉树
1.二叉堆是一颗完全二叉树
满二叉树:除了叶子节点外,其他节点的左右节点都不为空
完全二叉树:不一定是一个满二叉树,但是它不满的部分(缺失节点的部分)一定是在整棵树的右下侧。
二叉堆的性质:堆中某个节点的值总时小于等于其父节点的值(即根节点的值大于等于其孩子节点的值,最大堆。相应可以定义最小堆)。 - 用数组来表示一个完全二叉树
- 当数据的索引从0开始计算时
- 根据以上分析,最大堆的初始化代码为
package cn.itcast.day7;
public class MaxHeap<E extends Comparable<E>> {
private myArray<E> data;
public MaxHeap(int capacity) {
data = new myArray<>(capacity);
}
public MaxHeap() {
data = new myArray<>();
}
//返回堆中的元素个数
public int size() {
return data.getSize();
}
//返回一个布尔值,表示堆中是否为空
public boolean isEmpty() {
return data.isEmpty();
}
//返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index) {
if(index == 0) {
throw new IllegalArgumentException("错误");
}
return (index-1)/2;
}
//返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index) {
return index*2+1;
}
//返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index) {
return index*2+2;
}
}
- 向堆中添加元素和Sift Up
如:向下面数组添加元素时,首先直接在数组尾部添加数据52。得到[62,41,30,28,16,22,13,19,17,15,52],此时52的索引为10.
将52挂接在原始数组所构成的完全二叉树上,然后与相应的父亲节点进行比较,如果小于相应的父亲节点,则进行位置的交换。
如下:将52挂接在16的右子树,大于16,交换位置,交换位置后,再与其新的父节点(41)比较,还是要打,再与41交换位置。依此类推。(完成了上浮过程)
- 代码实现(利用到之前实现的Array数组):
1.首先在myArrayl类中添加元素交换的方法
public void swap(int i,int j) { //交换索引为i和j的元素的位置
if(i<0 || i>= size || j <0 || j>=size) {
throw new IllegalArgumentException("错误");
}
E t = data[i];
data[i] = data[j];
data[j] = t;
}
2.添加元素的实现
//向堆中添加元素
public void add(E e) {
data.addLast(e);//向尾部添加元素
shiftUp(data.getSize()-1); //添加的元素的索引
}
private void shiftUp(int k) {
//当k没有到达根节点并且索引k对应位置的元素的值小于当前父亲节点的值,则继续循环
while(k > 0 && data.get(parent(k)).compareTo(data.get(k))<0) {
data.swap(k, parent(k)); //交换位置
k = parent(k); //交换位置后,再让当前的k变为之前的父亲节点所在的索引位置
}
}
- 取出堆中的最大元素和Sift Down
1.堆顶的元素是最大的元素,如果直接将此元素取出,则要对剩下的左右子树部分进行融合成一个新的堆。这样的操作较为复杂,因此考虑另外一种方法:先将数组表示的堆底元素(数组的最后一个元素)放到堆顶,然后将堆低元素去掉。反映到数组中,则是将原来的数组[62,52,30,28,41,22,13,19,17,15,16]转变为[16,52,30,28,41,22,13,19,17,15]。元素个数减1且堆顶元素变为之前的堆底元素。
2.在进行上述操作后,不再满足最大堆的性质。此时,需要将新生成的堆的堆顶元素进行“下浮”操作:每次都将元素与其两个孩子节点进行比较,选择它的孩子节点中最大的那个元素比较,如果它的孩子节点中最大的元素比它自己还要大,那么它就和这个孩子节点中最大的元素交换位置。交换位置后,再将16与其新的孩子节点进行比较,判断是否还要继续下沉。如下所示:
- 代码实现
//k看堆中的最大元素
public E findMax() {
if(data.getSize()==0) {
throw new IllegalArgumentException("错误");
}
return data.get(0);
}
//取出堆中最大元素
public E extractMax() {
E ret = findMax();
data.swap(0, data.getSize()-1); //将堆尾元素与堆顶元素交换位置
data.removeLast(); //然后将堆尾元素去掉
siftDown(0); //下沉操作
return ret;
}
private void siftDown(int k) {
//如果k所处的位置已经没有孩子,是叶子节点时,循环终止;
while(leftChild(k) < data.getSize()) { //如果
int j = leftChild(k);
//j+1<data.getSize()说明有右孩子(rightChild(k)=leftChild(k)+1)
if(j+1<data.getSize() && data.get(j+1).compareTo(data.get(j))>0) { //右孩子的值大
j = rightChild(k);
//此时data[j]是leftChild和rightChild中的最大值
}
if(data.get(k).compareTo(data.get(j))>=0) {//如果当前值比它的孩子节点的值都要大,就不用继续下沉了
break;
}
data.swap(k, j);
k = j;
}
}
3.测试用例
package cn.itcast.day7;
import java.util.Random;
public class Main {
public static void main(String[] args) {
int n = 100000;
MaxHeap<Integer> maxHeap = new MaxHeap<>();
Random random = new Random();
for(int i=0;i<n;i++) {
maxHeap.add(random.nextInt(Integer.MAX_VALUE));
}
int[] arr = new int[n];
for(int i=0;i<n;i++) {
arr[i] = maxHeap.extractMax();//将所有取出的最大值存放到数组
}
for(int i=1;i<n;i++) {
if(arr[i-1]<arr[i]) { //不满足此条件说明堆出错
throw new IllegalArgumentException("出错");
}
}
System.out.println("运行结束");
}
}
- replace操作
- heapify:将任意数组整理成堆的形状
思路:对于一个给定的数组,直接看作是一个完全二叉树,然后找到对于当前数组中的最后一个非叶子节点开始计算,如下图,22是最后一个非叶子节点。找到这个节点后,倒着从后向前不断的进行Sift Down操作。
Question:如何定位最后一个非叶子节点的索引?(经典面试题)只需要找到最后一个节点对的索引,然后找到这个节点的父节点即可。
具体的操作如下图演示:
- 将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn)
- heapify的过程,算法复杂度为O(n)
代码实现:
1.先在myArray类中添加构造函数
public myArray(E[] arr) {
data = (E[])new Object[arr.length];
for(int i=0;i<arr.length;i++) {
data[i] = arr[i];
}
size = arr.length;
}
2.创建构造函数
public MaxHeap(E[] arr) {
data = new myArray<>(arr);
for(int i = parent(arr.length-1);i>=0;i--) {
siftDown(i);
}
}
二、基于堆的优先队列
- 代码实现
package cn.itcast.day7;
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(); //findMax()中已经进行了堆为空的处理
}
@Override
public void enqueue(E e) {
maxHeap.add(e);
}
@Override
public E dequeue() {
return maxHeap.extractMax();
}
}
三、优先队列的经典问题
1.在1000000个元素中选出前100名?(在N个元素中选出前M个元素)
- 使用排序算法:算法复杂度为NlogN
- 使用优先队列:算法复杂度为NlogM
优先队列解决:使用优先队列,维护当前看到的前M个元素----> 先将M个元素放进这个优先队列中,之后每次看到一个新的元素,如果这个新的元素比当前的优先队列中的最小的元素还要大的话,那么就把这个优先队列中的最小的元素给扔出去,再换上这个新元素。(需要使用最小堆:要非常快速的取出当前看到的前M个元素中的最小的元素。因为我们是不断的将前M个大的元素中的最小的元素进行替换)
leetCode347题:前K个高频元素
class Solution {
private class Freq implements Comparable<Freq>{
int e,freq;
public Freq(int e,int freq) {
this.e = e;
this.freq = freq;
}
public int compareTo(Freq o) {
if(this.freq<o.freq) { //注意:在这里使用的“优先”规则,定义的是数字较小的数优先出列(优先队列的优先规则是可以自定义的)
return 1;
}else if(this.freq>o.freq) {
return -1;
}else {
return 0;
}
}
}
public List<Integer> topKFrequent(int[] nums, int k) {
TreeMap<Integer,Integer> map = new TreeMap<>();
for(int num:nums) {
if(map.containsKey(num)) {
map.put(num, map.get(num)+1);
}else {
map.put(num, 1);
}
}
PriorityQueue<Freq> pq = new PriorityQueue<>();
for(int key:map.keySet()) { //map.keySet() 得到所有的键
if(pq.getSize()<k) {
pq.enqueue(new Freq(key,map.get(key)));
}else if(map.get(key)>pq.getFront().freq) {
pq.dequeue();
pq.enqueue(new Freq(key,map.get(key)));
}
}
LinkedList<Integer> res = new LinkedList<>();
while(!pq.isEmpty()) {
res.add(pq.dequeue().e);
}
return res;
}
}
- 利用java标准库的优先队列解决
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeMap;
class Solution {
public List<Integer> topKFrequent(int[] nums, int k) {
TreeMap<Integer,Integer> map = new TreeMap<>();
for(int num:nums) {
if(map.containsKey(num)) {
map.put(num, map.get(num)+1);
}else {
map.put(num, 1);
}
}
PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
public int compare(Integer a, Integer b) {
return map.get(a) - map.get(b);
}
}); //java内部维护的是一个最小堆.创建优先队列时可以接收一个比较器,使用的匿名内部类
for(int key:map.keySet()) { //map.keySet() 得到所有的键
if(pq.size()<k) {
pq.add(key);
}else if(map.get(key)>map.get(pq.peek())) {
pq.remove();
pq.add(key);
}
}
LinkedList<Integer> res = new LinkedList<>();
while(!pq.isEmpty()) {
res.add(pq.remove());
}
return res;
}
}