优先级队列
按照优先级的大小动态出队(动态指的使元素个数动态变化,而非固定)
普通队列:FIFO按照元素的入队顺序出队,先入先出
现实生活中的优先级队列PriorityQueue
-
医生根据病情排手术
病情情况相同先来先排,病情较重,优先安排
和排序有什么关系?能否按照病情排序,然后按排序后的病人数组安排手术?
排序是数组元素个数确定的情况,而排手术时,病人的人数都在动态变化
-
操作系统中的任务调度
系统的任务优先级一般都比普通的应用要高
CPU、内存等资源有限,当资源不够时,优先让优先级较高的应用获取资源
时间复杂度比较
入队 | 出队(出当前队列 的最大值) | |
---|---|---|
普通链式队列 | O(1) | O(n) |
优先级队列(堆) | O(log n) | O(log n) |
在计算机领域,若见到logN时间复杂度,近乎一定和“树”结构相关(并非一定要构造一棵树结构,算法过程逻辑上一定是一颗树)
归并,快速递归过程也是一个“递归”树 nlogn
堆
基于二叉树的堆称为二叉堆,应用最广的堆
本节讲的二叉堆的特点:
-
是一颗完全二叉树,基于数组存储(元素都是靠左排列,数组中存储时不会浪费空间),普通二叉树要存储空节点,浪费大量空间,因此只有完全二叉树适合用数组这种结构存储,其他的二叉树都要用链式结构
-
关于节点值:
堆中根节点值>=子树中节点值(最大堆,大根堆)
堆中根节点值<=子树中节点值(最小堆,小根堆,JDK中的PriorityQueue默认是基于最小堆的实现)
即满足 完全二叉树
根>=子树 / 根<=子树(从上至下每个树)
注意:节点的层次和节点的大小没有关系,即并不是高层的节点一定大于低层的。因此只能保证在当前树中,树根是最大值,其他节点层次大小关系不确定
-
因为堆是基于数组的层序遍历来存储的,节点之间的关系通过数组下标来表示 ,即从0开始编号
假设此时节点编号为i,且存在父子节点:
-
父节点编号parent = (i-1)/ 2;
-
左子树的编号 left = 2*i+1;
-
右子树的编号 right = 2*i+2;
节点之间通过索引下标来找到父子节点
-
基于动态数组ArrayList实现的最大堆
核心方法(三个)
1. 向堆中添加元素——核心 siftUP上浮操作:
堆是数组实现的,因此添加元素时先将元素放在数组的最后一位即size处
此时这个堆仍然满足完全二叉树的性质,但此时这个完全二叉树就不满足堆的条件了,需要进行上浮操作
不断将此时的索引k和故节点的索引i对应的元素进行大小关系比较,若大于父节点的值则交换彼此的节点值,直到当前节点值<=父节点值 或 走到树根为止
上浮终止条件:
当前已经上浮到树根 =》这个元素一定是最大值(k>0表示还有父节点,且当前元素值>父节点值)
当前元素<=父节点对应
siftUp(int k){
while(k>0 && data[k]>data[parent(k)]){
swap(k,parent(k));
k = parent(k);
}
}
2. 在最大堆中取出最大值——siftDown下沉操作
最大堆的最大值一定处在树根结点,直接取出树根即可
需要融合左右两个子树,使得取出树根后这颗树仍是最大堆(此时融合操作比较复杂,因为左右子树大小关系不确定,且结点的大小和层次没有必然联系)
siftDown思路:
1)直接取根节点作为当前堆的最大值
2)将堆中最后一个元素顶到堆顶,然后进行元素的下沉操作,使其仍满足最大堆性质
3. heapify——堆化操作
任意一个整型数组,将其调整成最大堆,即任意整型数组都可以看作一个 完全二叉树,距离最大堆只差元素调整操作
方法步骤:
1)将这n个元素依次调用add方法添加到一个新的最大堆中,遍历原数组,创建一个新的最大堆,调用最大堆的add方法即可
时间复杂度O(nlogn) 空间复杂度O(n)
2)原地heapify
从最后一个非叶子节点开始进行元素siftDown操作(从当前二叉树中最后一个小子树开始调整,不断将子树调整为最大堆,因此走到树根时,左右子树已经全都是最大堆)
最后一个叶子结点是size-1,故最后一个非叶子结点就是最后一个叶子结点的父亲结点
/**
* 基于动态数组实现的最大堆
**/
public class MaxHeap {
// 实际存储元素的数组
private List<Integer> elementData;
// 当前堆中元素个数
private int size;
public MaxHeap() {
this(10);
}
public MaxHeap(int size) {
elementData = new ArrayList<>(size);
}
/**
* 将任意的整型数组arr调整为堆
* @param arr
*/
public MaxHeap(int[] arr) {
elementData = new ArrayList<>(arr.length);
// 1.先将所有元素复制到data数组中
for (int i : arr) {
elementData.add(i);
size ++;
}
// 2.从最后一个非叶子结点开始进行siftDown操作
// 最后一个叶子结点是size-1,故最后一个非叶子结点就是最后一个叶子结点的父亲结点
for (int i = parent(size - 1); i >= 0; i--) {
siftDown(i);
}
}
/**
* 向堆中添加值为val的元素
* @param val
*/
public void add(int val) {
// 1.直接向数组末尾添加元素
elementData.add(val);
size ++;
// 2.进行元素的上浮操作
siftUp(size - 1);
}
/**
* 获取堆的最大值
* @return
*/
public int extractMax() {
if (size == 0) {
throw new NoSuchElementException("heap is empty!cannot extract!");
}
// 树根就是最大值结点
int max = elementData.get(0);
// 将数组的末尾元素顶到堆顶
elementData.set(0,elementData.get(size - 1));//将索引0处的元素值设为索引size-1处的值
// 将数组的最后一个元素从堆中删除
elementData.remove(size - 1);
size --;
// 进行元素的下沉操作,从索引为0开始
siftDown(0);
return max;
}
/**
* 返回堆中最大值
* @return
*/
public int peekMax() {
if (isEmpty()) {
throw new NoSuchElementException("heap is empty!cannot peek!");
}
return elementData.get(0);
}
/**
* 元素下沉
* @param k
*/
private void siftDown(int k) {
// 存在子树
while (leftChild(k) < size) {
int j = leftChild(k);
// 存在右子树且右子树的值还比左子树大
if (j + 1 < size && elementData.get(j + 1) > elementData.get(j)) {
j ++;
}
// j为左右子树的最大值索引
if(elementData .get(k) >= elementData.get(j)) {
// 当前元素 >= 左右子树最大值,下沉结束,元素k落在了最终位置
break;
}else {
swap(k,j);
k = j;
}
}
}
/**
* 元素上浮
* @param k
*/
private void siftUp(int k) {
while (k > 0 && elementData.get(k) > elementData.get(parent(k))) {
swap(k,parent(k));
k = parent(k);
}
}
/**
* 交换
* @param k
* @param parent
*/
private void swap(int k, int parent) {
int child = elementData.get(k);
int parentVal = elementData.get(parent);
elementData.set(k,parentVal);
elementData.set(parent,child);
}
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
/**
* 找到结点k对应的父节点的索引
* @param k
* @return
*/
private int parent(int k) {
return (k - 1) >> 1;
}
/**
* 找到结点k对应的左子树的索引
* @param k
* @return
*/
private int leftChild(int k) {
return (k << 1) + 1;
}
/**
* 找到结点k对应的右子树的索引
* @param k
* @return
*/
private int rightChild(int k) {
return (k << 1) + 2;
}
@Override
public String toString() {
return elementData.toString();
}
}
拓展:元素的比较——策略模式
策略模式:把Student类的大小关系比较从Student类中“解耦”,此时的比较策略非常灵活,需要哪种方式只需要创新一个类的实现Comparator接口即可,根据不同的大小关系的需求调用不同的比较器对象。(遵循开闭原则)
如Arrays.sort方法,就是根据比较器c的规则给不具备比较能力的某个对象数组a排序
基于最大堆的优先级队列
实现
/**
* 基于最大堆的优先级队列
*/
public class PriorityQueue implements Queue<Integer> {
private MaxHeap heap;
public PriorityQueue(){
heap = new MaxHeap();
}
// 向优先级队列中添加元素,就是最大堆中的添加并上浮操作
@Override
public void offer(Integer val) {
heap.add(val);
}
// 在优先级队列中,每次的出队元素就是堆中最大值
@Override
public Integer poll() {
return heap.extractMax();
}
@Override
public Integer peek() {
return heap.peekMax();
}
@Override
public boolean isEmpty() {
return heap.isEmpty();
}
public static void main(String[] args) {
int[] data = new int[]{3,5,7,6,2,1,9,4};
Queue<Integer> queue = new PriorityQueue();
for (int i:
data) {
queue.offer(i);
}
System.out.println(queue.poll());
}
}
注意:JDK中默认优先级队列是一个最小堆,需要改造成一个最大堆
PriorityQueue<Integer> queue = new PriorityQueue<>();
// 1. 使用比较器
class IntegerReverse implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
// 传入比较器对象
PriorityQueue<Integer> queue1 = new PriorityQueue<>(new IntegerReverse());
// 2. 使用匿名内部类改造成最大堆
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
// 3. 函数式编程:Lambda表达式——优化匿名内部类
PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2)->o2-o1);
TopK问题
与一组数据中的前/后/最大的/最小的k个数有关的问题
TopK问题都可以使用优先级队列,取大用小,取小用大:
若需要取出前k个最大元素,构造最小堆
若需要取出前k个最小元素,构造最大堆
leetcode面试题17.14
设计一个算法找出数组中最小的k个数,以任意顺序返回这k个数均可
思路:
- 排序,依次取出前k个数 时间复杂度O(nlogn)
- 若要时间复杂度优于时间复杂度O(nlogn),则使用优先级队列,构造最大堆,此时时间复杂度为O(nlogk) k<<n
随着堆顶元素不断交换,会把堆顶元素不断变小,最终队列扫描结束就存放了最小的k个数
public int[] smallestK(int[] arr, int k){
int[] ret = new int[k];
// 注意边界条件
if(arr.length == 0 || k == 0){
return ret;
}
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
for (int i = 0; i < arr.length; i++) {
if(queue.size()<k){
queue.offer(arr[i]);
}else {
if(arr[i]<queue.peek()){
queue.poll();
queue.offer(arr[i]);
}
}
}
int i = 0;
while (!queue.isEmpty()){
ret[i] = queue.poll();
i++;
}
return ret;
}
匿名内部类
所谓内部类就是一个类嵌套到另一个类的内部的操作
匿名内部类
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
Comparator是一个接口,接口无法直接实例化对象,必须由其子类进行向上转型进而对接口实例化,因此new Comparator接口实际上就是
class 没名字 implements Compatator{}