队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列;在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
优先级队列的实现底层使用了堆的数据结构,首先我们来了解一下堆
1️⃣小堆:按完全二叉树的顺序存储方式存储在一个一维数组,满足:Ki <= K2i + 1 且 Ki <= K2i + 2(父亲节点大于孩子节点)根节点最小的堆叫做最小堆或小根堆。
2️⃣大堆:按完全二叉树的顺序存储方式存储在一个一维数组,满足:Ki >= K2i + 1 且 Ki >= K2i + 2(孩子节点大于父亲节点)将根节点最大的堆叫做最大堆或大根堆
3️⃣堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树(采用顺序方式存储)
1️⃣堆向下调整及创建 :对于一个集合的数据,我们需要把它创建成一个堆——大堆或者小堆
1. 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
2. 如果parent的左孩子存在,即:child < size,进行以下操作,直到parent的左孩子不存在
parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标
parent大于较小的孩子child,调整结束否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。
我们以大堆为例:从一棵树的最后一棵子树开始,依次调整成为大根堆——左右孩子节点比较大小,大的与其根点交换,直到成为大堆
public class TestHeap {
public int[] elem;
public int usedSize;
public TestHeap() {
this.elem = new int[10];
}
public void initElem(int[] array) {
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
}
/**
* 时间复杂度:O(N)
*/
//堆的创建
public void createHeap() {
for(int parent = (usedSize-1-1)/2;parent >= 0; parent--) {//从长度-1开始,第一个父亲结点为{(长度-1)-1}/2
shiftDown(parent,usedSize);
}
}
/**
* 父亲下标
* 每棵树的结束下标
* @param parent
* @param len
*/
//向下调整—— 时间复杂度:logN
public void shiftDown(int parent,int len) {
int child = 2*parent + 1;//定义孩子节点
//最起码 要有左孩子
while(child < len) {//一共usedSize个数,孩子节点小于总长度
//一定是有右孩子的情况下
if(child+1 < len && elem[child] < elem[child+1]) {//child+1 < len防止越界
child++;//右孩子节点大的话,child++就不变成了右孩子
}
//此时,child下标一定是左右孩子最大值的下标
if(elem[child] > elem[parent]) {
//当孩子节点大于父亲节点,使用第三方将最大和最小值交换
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
//此时完成了交换,但是有一个问题完成交换之后的孩子节点与孩子的孩子节点此时的大小顺序又被打乱
//所以让父亲节点等于孩子节点,孩子节点等于孩子的孩子节点继续比较大小
parent = child;
child = 2*parent + 1;
}else {
break;
}
}
}
}
2️⃣建堆的时间复杂度——O(N)
1️⃣堆的插入:堆的插入使用向上调整——时间复杂度O(N*logN)
c————孩子节点 p————父亲节点
//堆的插入:堆的插入使用向上调整
public void offer(int val) {
if(isFull()) {
//扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize++] = val;最大元素下标为插入元素,元素个数+1————11
//向上调整
shiftUp(usedSize-1);//10————下标为10
}
public boolean isFull() {
return usedSize == elem.length;
}
//向上调整
private void shiftUp(int child) {
int parent = (child-1)/2;
while (child > 0) {
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
//继续向上调整,孩子节点为原来的父亲节点,父亲的父亲的节点为父亲节点
child = parent;
parent = (child-1)/2;
}else {
break;
}
}
}
2️⃣堆的删除——删除的一定是堆顶元素
第一步:让堆顶元素和堆中最后一个元素交换 第二步:这时候有效数据减少一个,这时候只需要调整0下标这棵树
第三步: 使用向下调整——时间复杂度:logN
//堆的删除——删除的一定是堆顶元素
//第一步:让堆顶元素和堆中最后一个元素交换 第二步:这时候有效数据减少一个,这时候只需要调整0下标这棵树 第三步: 使用向下调整
public void pop() {
if(isEmpty()) {
return;
}
swap(elem,0,usedSize-1);
usedSize--;//交换堆顶元素和堆中最后一个元素,此时数据减少一个
shiftDown(0,usedSize);//向下调整
}
public boolean isEmpty() {
return usedSize == 0;
}
//交换
public void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//向下调整 时间复杂度——logN
private void shiftDown(int parent,int len) {
int child = 2*parent + 1;//定义孩子节点
//最起码 要有左孩子
while (child < len) {//一共usedSize个数,孩子节点小于总长度
//一定是有右孩子的情况下
if(child+1 < len && elem[child] < elem[child+1]) {//child+1 < len防止越界
child++;//右孩子节点大的话,child++就不变成了右孩子
}
//child下标 一定是左右孩子 最大值的下标
if(elem[child] > elem[parent]) {
//当孩子节点大于父亲节点,使用第三方将最大和最小值交换
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
//此时完成了交换,但是有一个问题完成交换之后的孩子节点与孩子的孩子节点此时的大小顺序又被打乱
//所以让父亲节点等于孩子节点,孩子节点等于孩子的孩子节点继续比较大小
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
1️⃣使用时必须导入PriorityQueue所在的包,即:
importjava.util.PriorityQueue;
2️⃣PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
3️⃣不能插入null对象,否则会抛出NullPointerException
4️⃣没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5️⃣插入和删除元素的时间复杂度为 O(logN)
6️⃣PriorityQueue底层使用了堆数据结构
7️⃣PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
任何一个类,都需要从构造方法出发来理解,下面我们来看PriorityQueue的构造方法
//创建一个空的优先级队列,默认容量是11且默认没有比较器
PriorityQueue()
//创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常
PriorityQueue(int initialCapacity)
//用一个集合来创建优先级队列
PriorityQueue(Collection c)
leetcod题目:设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可
题目链接:最小K个数
做题思路:建立小根堆
//最小K个数
//设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
public int[] smallestK(int[] arr, int k) {
int[] ret = new int[k];
if(arr == null || k == 0) {
return ret;
}
//O(N*logN)————向上调整
Queue minHeap = new PriorityQueue<>(arr.length);
for (int x:arr) {
minHeap.offer(x);//遍历小根堆,放入小根堆中
}
//O(k*logN)————弹k次,每次向下调整
for (int i = 0; i < k; i++) {
ret[i] = minHeap.poll();
}
return ret;
}
✨TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
做题思路:1️⃣用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆 前k个最小的元素,则建大堆
2️⃣用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
例求最大的K个值:
/**
* 前K个最大的元素 时间复杂度:N*logK
* @param arr
* @param k
* @return
*/
public int[] maxK2(int[] arr, int k) {
int[] ret = new int[k];
if(arr == null || k == 0) {
return ret;
}
Queue minHeap = new PriorityQueue<>(k);//建立大小为k的小根堆
//1.遍历数组的前K个 放到堆当中
for (int i = 0; i < k; i++) {
minHeap.offer(arr[i]);//前K个小根堆
}
//2.遍历剩下的数据,每次和堆顶元素进行比较
//堆顶元素 小的时候,就出堆
for (int i = k; i < arr.length; i++) {
int val = minHeap.peek();//查看堆顶元素
if(val < arr[i]) {//数组的元素大于堆顶元素
minHeap.poll();//弹入堆当中,放入即为最后一个位置
minHeap.offer(arr[i]);
}
}
//放入数组中
for (int i = 0; i < k; i++) {
ret[i] = minHeap.poll();
}
return ret;
}