一. 什么是优先队列
普通队列: 先进先出;后进后出
优先队列: 出队顺序和入队顺序无关; 和优先级相关
- 最典型应用: 操作系统会动态的选择优先级最高的任务去执行
二. 堆的基础表示
堆
- 使用二叉树实现堆 称为 二叉堆
- 二叉堆是一棵完全二叉树
二叉堆的性质:
- 最大堆: 堆中某个节点的值总是不大于其父节点
- 最小堆: 堆中某个节点的值总是不小于其父节点
用数组存储二叉堆
特性:(数组索引为0 的位置空出来)
假设一个节点对应在数组中的下标为 i, 则
父节点: i/2
左孩子: 2*i
右孩子: 2*i+1
如果数组索引为0 的位置不空出来,则
父节点: (i-1)/2
左孩子: 2*i+1
右孩子: 2*i+2
下面都默认 都是 索引为0的位置 不空出来
代码架构实现
新建项目MaxHeap:
.
├── MaxHeap.iml
└── src
├── Array.java
├── Main.java
└── MaxHeap.java
Array.java即为以前实现过的 动态数组
MaxHeap.java
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public MaxHeap(){
data = new Array<>();
}
// 返回堆中的元素
public int size(){
return data.getSize();
}
// 返回一个布尔值 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回完全二叉树的数组表示中, 一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0){
throw new IllegalArgumentException("index-0 does'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;
}
}
三. 向堆中添加元素和Sift Up
流程:
步骤一: 在数组末尾添加入元素 a
步骤二:Sift Up
while(a > a的父亲节点):
a与a的父亲节点互换位置
代码实现
MaxHeap.java
public class MaxHeap<E extends Comparable<E>> {
...
// 向堆中添加元素
public void add(E e){
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k){
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
data.swap(k, parent(k));
k = parent(k);
}
}
}
为动态数组添加 数据交换方法 swap
Array.java
public class Array<E> {
...
public void swap(int i, int j){
if(i < 0 || i>= size || j < 0 || j >= size){
throw new IllegalArgumentException("Index is illegal");
}
E t = data[i];
data[i] = data[j];
data[j] = data[i];
}
...
}
四. 从堆中取出元素和Sift Down
流程:
假设取出的元素为a
步骤一: 取出a, 将数组中最后一个元素b 放到原先a的位置
步骤二: Sift Down
while(存在b的子元素 > b):
b与较大的那个子元素互换位置
代码实现
MaxHeap.java
public class MaxHeap<E extends Comparable<E>> {
...
// 看堆中最大的元素
public E findMax(){
if(data.getSize() == 0){
throw new IllegalArgumentException("Can not finMax when heap is empty");
}
return data.get(0);
}
// 取出堆中最大元素
public E extracMax(){
E ret = findMax();
data.swap(0, data.getSize()-1);
data.removeLast();
siftDown(0);
return ret;
}
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++;// j = rightChild(k); 此时data[j]是 两孩子中的最大值
}
if(data.get(k).compareTo(data.get(j)) >= 0)
break;
data.swap(k, j);
k = j;
}
}
}
测试:
Main.java
import java.util.Random;
public class Main {
public static void main(String[] args) {
// write your code here
int n = 1000000;
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.extracMax();
}
for(int i = 1; i < n; i++ ){
if(arr[i-1] < arr[i]){
throw new IllegalArgumentException("Error");
}
}
System.out.println("Test MaxHeap completed");
}
}
结果:Test MaxHeap completed
五. Heapify 和 Replace
replace
- 取出最大元素后, 放入一个新的元素
- 实现: 可以直接将堆顶的元素替换, 然后进行Sift Down, 一次O(logn)的操作
MaxHeap.java
...
// 取出堆中最大值, 并且替换成元素e
public E replace(E e){
E ret = findMax();
data.set(0, e);
siftDown(0);
return ret;
}
}
heapify
- 将任意的数组整理成堆的形状
- 实现流程:
设 数组最后一个元素的父节点索引为 k (索引的计算 (最后一个节点的索引-1)/2)
while(k >= 0):
siftdown k
k--
- 算法复杂度
- 方法一: 将n个元素逐个插入到一个空堆中, 算法复杂度是O(nlogn)
- 方法二: heapify的过程, 算法复杂度为O(n)
MaxHeap.java中加入一种新的构造方法
...
public MaxHeap(E[] arr){
data = new Array<>(arr);
for(int i = parent(arr.length-1); i >=0; i--){
siftDown(i);
}
}
...
Array.java
...
public Array(E[] arr){
data = (E[])new Object[arr.length];
for(int i = 0; i < arr.length; i++){
data[i] = arr[i];
}
size = arr.length;
}
...
测试:
Main.java
import java.util.Random;
public class Main {
private static double testHeap(Integer[] testData, boolean isHeapify){
long startTime = System.nanoTime();
MaxHeap<Integer> maxHeap;
// 两种创建方法
if(isHeapify){
maxHeap = new MaxHeap<>(testData);
}
else{
maxHeap = new MaxHeap<>();
for(int num: testData){
maxHeap.add(num);
}
}
int[] arr = new int[testData.length];
for(int i = 0; i < testData.length; i++){
arr[i] = maxHeap.extracMax();
}
for(int i = 1; i < testData.length; i++){
if(arr[i-1] < arr[i]){
throw new IllegalArgumentException("Error");
}
}
System.out.println("Test MaxHeap completed");
long endTime = System.nanoTime();
return (endTime-startTime)/1000000000.0;
}
public static void main(String[] args) {
int n = 1000000;
Random random = new Random();
Integer[] testData = new Integer[n];
for(int i = 0; i < n; i++){
testData[i] = random.nextInt(Integer.MAX_VALUE);
}
double time1 = testHeap(testData, false);
System.out.println("without heapify: "+time1+" s");
double time2 = testHeap(testData, true);
System.out.println("with heapify: "+time2+" s");
}
}
结果:
Test MaxHeap completed
without heapify: 0.319265444 s
Test MaxHeap completed
with heapify: 0.20685298 s
由此可见Heapify能有效提升效率
六. 基于堆的优先队列
之前的代码,已经实现了最大堆, 我们在这些代码的基础上实现优先队列。
接口Queue.java
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue(E e);
E dequeue();
E getFront();
}
优先队列的实现PriorityQueue.java
public class PriorityQueue<E extends Comparable> 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.extracMax();
}
}
七.Leetcode上优先队列相关问题
在1000000个元素中选出前100名?
即在N个元素中选出前M个元素
解题思路:
1.使用优先队列, 维护当前看到的前M个元素。遇到更小的就放入队列,并踢掉最大的。
给定一个非空的整数数组,返回其中出现频率前 k 高的元素。
例如,
给定数组 [1,1,1,2,2,3] , 和 k = 2,返回 [1,2]。
注意:
你可以假设给定的 k 总是合理的,1 ≤ k ≤ 数组中不相同的元素的个数。
你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。
用我们自己设计的PriorityQueue来解答
思路:
1. 使用映射map, 存放给定数组nums中的元素及其出现频次
2. 一个长度为k的优先队列, 放入map中的元素(key,value),频次低的元素优先出队
3. 我们自定义的PriorityQueue,是越'大'优先级越高, 而我们可以自定义Comparable来定义'大'(频次越小越大)
Solution.java
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
class Solution {
private class Freq implements Comparable<Freq>{
int e, freq;
public Freq(int e, int freq){
this.e = e;
this.freq = freq;
}
@Override
public int compareTo(Freq another){ // 元素越小,优先级越高 这样在优先队列中,频率最低的先出, 最后留下的都是频率最高的几个
if(this.freq < another.freq){
return 1;
}
else if (this.freq > another.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<Freq>();
for(int key: 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, map.get(key));
}
}
LinkedList<Integer> res = new LinkedList<>();
while(!pq.isEmpty()){
res.add(pq.dequeue().e);
}
return res;
}
}
八. Java中的PriorityQueue
1.Java中的PriorityQueue是 最小优先队列
用Java自带的PriorityQueue, 改写347号问题:
Solution.java
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.PriorityQueue;
class Solution {
private class Freq implements Comparable<Freq>{
int e, freq;
public Freq(int e, int freq){
this.e = e;
this.freq = freq;
}
@Override
public int compareTo(Freq another){ //PriorityQueue是最小优先队列, 所以反过来, 元素越大优先级越高。优先级小的会留在队中
if(this.freq < another.freq){
return -1;
}
else if (this.freq > another.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<Freq>();
for(int key: map.keySet()){
if(pq.size() < k){ // getsize 改为size
pq.add(new Freq(key, map.get(key))); // enqueue 改为 add
}
else if(map.get(key) > pq.peek().freq){ // getFront 改为peek
pq.remove(); // dequeue改为rempve
pq.add(new Freq(key, map.get(key)));
}
}
LinkedList<Integer> res = new LinkedList<>();
while(!pq.isEmpty()){
res.add(pq.remove().e);
}
return res;
}
}
2. PriorityQueue传入比较器
之前我们通过改写compareTo
来 自定义比较方法。 我们还有更好的方法:
Solution.java
import ..
import java.util.Comparator;
// 之前的自定义比较的方法
// private class Freq implements Comparable<Freq>{
// int e, freq;
//
// public Freq(int e, int freq){
// this.e = e;
// this.freq = freq;
// }
//
// @Override
// public int compareTo(Freq another){ //PriorityQueue是最小优先队列, 所以反过来, 元素越大优先级越高。优先级小的会留在队中
// if(this.freq < another.freq){
// return -1;
// }
// else if (this.freq > another.freq){
// return 1;
// }
// else
// return 0;
// }
// }
private class Freq {
int e, freq;
public Freq(int e, int freq) {
this.e = e;
this.freq = freq;
}
}
private class FreqComparator implements Comparator<Freq>{
@Override
public int compare(Freq a, Freq b){
return a.freq - b.freq;
}
}
...
// 新的自定义比较器方法, 需要出入PriorityQueue中
// Java的PriorityQueue可以传入比较器
PriorityQueue<Freq> pq = new PriorityQueue<Freq>(new FreqComparator());
...
3. 代码进一步优化
Solution.java
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.PriorityQueue;
import java.util.Comparator;
class Solution {
private class Freq {
int e, freq;
public Freq(int e, int freq) {
this.e = e;
this.freq = freq;
}
}
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<Integer>(new Comparator<Integer>() { // 比较器在初始化优先队列的定义
// @Override
// public int compare(Integer a, Integer b) {
// return map.get(a) - map.get(b);
// }
// });
PriorityQueue<Integer> pa = new PriorityQueue<>( // 进一步简化
(a, b) -> map.get(a) - map.get(b) // 使用lamda表达式
);
for (int key : map.keySet()) {
if (pq.size() < k) {
pq.add(key); //pq.add(new Freq(key, map.get(key)));
} else if (map.get(key) > map.get(pq.peek())) { //map.get(key) > pq.peek().freq
pq.remove();
pq.add(key); // pq.add(new Freq(key, map.get(key)));
}
}
LinkedList<Integer> res = new LinkedList<>();
while (!pq.isEmpty()) {
res.add(pq.remove()); //res.add(pq.remove().e);
}
return res;
}
}