8、优先队列和树
优先队列
- 普通队列:先进先出,后进后出
- 优先队列:出队顺序和入队顺序无关,和优先级有关
- 为什么使用优先队列
- 动态选择优先级最高的执行任务
- 关键词:动态
- 优先队列实现接口
- 优先队列的实现方式:
- 普通顺序结构:数组
- 顺序线性结构:已经排序好的
- 堆:最大堆或者最小堆
优先队列的实现(基于堆)
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 void enqueue(E e) {
maxHeap.add(e);
}
@Override
public E dequeue() {
return maxHeap.extractMax();
}
@Override
public E getFront() {
return maxHeap.findMax();
}
}
优先队列实现方式的时间复杂度对比
堆
- 堆的基本结构:二叉堆
- 满二叉树:每一层均是满的
- 完全二叉树:当满二叉树在最后一层级容纳不下的话,继续在下面一层从左到右继续添加节点。 简而言之,完全二叉树:把元素顺序排列成树的形状
- 二叉堆的性质:
- 堆中任意个节点的值总是不大于其父节点的值(所有父节点的值大于其孩子节点的值),所以根节点的值最大,所以这样的堆可以定义成最大堆。
- 上面可以得出最大堆,相反,可以得出最小堆,最小堆就是堆中的任意节点的值总是小于其父节点的值,所以此时最小堆中的根节点值最小。
- 用数组存储二叉堆(当数组索引从1开始计数)
- left child(i): 2*i
- right child(i):2*i+1
- parent(i):i/2
- 用数组存储二叉堆(当数组索引从0开始计数)
- left child(i): 2*i+1
- right child(i):2*i+2
- parent(i):(i-1)/ 2
最大堆的实现
-
添加元素:堆中添加元素均是在堆的最后索引位置添加元素
下面是最大堆中添加元素的详细图解:
-
取出元素:只能取出最大元素,所以是索引为0的元素
- 取出最大元素
- 互换索引为0和索引为最大的元素
- 将最后一个元素删除
- 此时索引为0的元素是最小的,所以需要往下调
下面是最大堆取出最大元素的详细图解:
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap() {
data = new Array<>();
}
public MaxHeap(int capacity) {
data = new Array<>(capacity);
}
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 not have parent");
return (index-1)/2;
}
private int leftChild(int index) {
return index*2 +1;
}
private int rightChild(int index) {
return index*2 +2;
}
//向堆中添加元素
public void add(E e) {
data.addLast(e);
siftUp(data.getSize()-1);
}
//将节点往上调,在添加元素时运用
private void siftUp(int index) {
//当节点的父节点比自身的节点值大时,说明当前节点也比父节点的另一个子节点值大,
//所以互换当前节点与父节点之后,父节点大于两个子节点。
while(index>0 && data.get(parent(index)).compareTo(data.get(index))<0) {
data.swap(index,parent(index));
index = parent(index);
}
}
//找出最大元素的节点
public E findMax() {
if(data.getSize() == 0) {
throw new IllegalArgumentException("heap is empty");
}
return data.get(0);
}
//取出堆中最大元素
public E extractMax() {
E ret = findMax();
data.swap(0,data.getSize()-1);
data.removeLast();
siftDown(0);
return ret;
}
//将节点往下调,在删除最大元素节点时运用
public void siftDown(int index) {
while(leftChild(index) < data.getSize()) {
int j = leftChild(index);
//当右子节点存在并且在比较左右子节点大小时,如果右子节点大于左子节点时
if(j+1 < data.getSize() && data.get(j+1).compareTo(data.get(j))>0) {
j = j +1;
}
//此时data[j]是leftChild与rightChild中的最大值,
// 所以使用data[j]与data[index]进行比较
if(data.get(index).compareTo(data.get(j)) >= 0)
break;
data.swap(index,j);
index = j;
}
}
}
最大堆的测试
//测试MaxHeap
int n = 1000000;
MaxHeap<Integer> maxHeap = new MaxHeap<>(n);
Random random = new Random();
for(int i = 0; i<n; i++) {
maxHeap.add(random.nextInt(Integer.MAX_VALUE));
}
int[] data = new int[n];
for(int i=0; i<n; i++) {
data[i] = maxHeap.extractMax();
}
for(int i =1; i<n; i++) {
//最大堆中如果存在前面索引的比后面索引的小,则表明最大堆实现有问题。
if(data[i-1]<data[i])
throw new IllegalArgumentException("maxHeap is illegal");
}
System.out.print("Test MaxHeap is Completed");
replace
将堆顶的最大元素返回,并替换成某个元素
//取出最大元素,并替换成元素e
public E repalce(E e) {
E ret = findMax();
data.set(0,e);
siftDown(0);
return ret;
}
heapify
将任意数组整理成堆的形状
实现
-
定义一个构造方法,传入数组后,得到的数组就是最大堆了
-
实现要点,对最后一个叶子节点的父节点开始往前每一个节点进行siftDown操作
-
依次对22,13,19,17,15进行siftDown操作
-
最后一个节点的叶子节点是(n-1)/2,其中n-1就是最后一个节点的索引
-
下图中蓝色的均为叶子节点
public MaxHeap(E[] arr) {
data = new Array<>(arr);
//从最后一个叶子节点的父节点开始进行往下调节,执行siftDown操作
for(int i = parent(data.getSize()-1);i>=0;i--) {
siftDown(i);
}
}
heapify的算法复杂度
优先队列的经典问题
在N个元素中选出前M个元素
private class Freq implements Comparable<Freq>{
int e,freq;
public Freq(int e,int freq) {
this.e = e;
this.freq = freq;
}
//定义优先级,当前元素小于另外一个元素的频率时,返回1
@Override
public int compareTo(Freq o) {
if(this.e < o.freq)
return 1;
else if(this.e > o.freq)
return -1;
else
return 0;
}
}
//在N个元素中选出前M个元素问题,在数组nums中取出前k个频率最大的元素
public List<Integer> topFrequent(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> priorityQueue = new PriorityQueue<>();
for(int key : map.keySet()) {
//当优先队列的元素个数小于k时
if(priorityQueue.getSize() < k) {
priorityQueue.enqueue(new Freq(key,map.get(key)));
//当优先队列中的最小频率元素priorityQueue.getFront().freq的频率
//小于当前遍历到映射中的元素频率时
//优先队列取出一个元素,然后将当前遍历到映射中的元素放入
} else if(priorityQueue.getFront().freq < map.get(key)) {
priorityQueue.dequeue();
priorityQueue.enqueue(new Freq(key,map.get(key)));
}
}
LinkedList<Integer> linkedList = new LinkedList<>();
while(!priorityQueue.isEmpty()) {
linkedList.add(priorityQueue.dequeue().e);
}
return linkedList;
}
java标准库中的PriorityQueue
- java的PriorityQueue内部是基于最小堆实现
- peek():队首元素
- remove():去除队首元素
- add():队尾添加元素
- size():队内元素个数