目录
前言:
🎉优先级队列可以很快找出一组数据中最小或者最大的数据,只需改正它建小堆或者大堆。它底层是用数组存储的(顺序存储),所以它是一颗完全二叉树,这样才不会浪费数组空间。这种数据结构对于一些数据的操作带来了很大的便利。
大小堆概念
😉这是一个大堆,父亲节点值是要大于它左右孩子值,对于每一个子树都要符合此条件。小堆就是父亲节点值是要小于它左右孩子值,同样的对于每一棵子树也要符合此条件。注意它左右孩子大小值是随机的。
建堆(大堆)
🎈首先我们认为一个数据是有序的,所以从18开始往前依次进行建大堆,由于数组中是顺序存储这些值的,当我们从18开始往前遍历完数组时,那么每一个子树都是大堆的结构。
🎈在左右孩子中找最大的数据,然后和父亲节点值进行比较。如果父亲节点值比左右孩子中最大的值都小,那么就交换这两个节点的值,这样最大的值就在堆顶。这样的方法叫做向下调整。从18开始往前遍历完数组,全部采取向下调整,每棵子树都是大堆的结构,那么整棵树的大堆就建成了。
🎈采取向下调整时,调整完成后,它孩子这颗子树就不一定是大堆的结构,那么就需要一直往下调,直到求出的孩子越界,说明这棵树调整完成了。
🎈已知父亲下标求左右孩子下标:childLeft = parent * 2 + 1 childRight = parent * 2 + 2
🎈已知孩子下标求父亲下标:parent = (child - 1) / 2
🎈注意:如果要建小堆,只需改变比较的判定即可。
代码实现
注意:这里是成员方法,操作的是elem数组,usedSize是有效数据个数。
/**
* 向下调整,建大堆
* 时间复杂度:log(n)
* @param parent
* @param len
*/
private void shiftDown(int parent, int len) {
int child = parent * 2 + 1;
while(child < len) {
//确保有右孩子的存在,防止越界
if(child + 1 < len && elem[child] < elem[child + 1]) {
child++;
}
if(elem[child] > elem[parent]) {
int tmp = elem[child];
elem[child] = elem[parent];
elem[parent] = tmp;
//确保整棵树都是符合大堆要求
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
/**
* 建堆,将每个元素向下调整
* 时间复杂度 o(n)
*/
public void createHeap() {
//由下往上调整
for(int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(parent, usedSize);
}
}
堆中插入数据
🪖往最后面插入数据,然后和父亲节点值比较,如果比它大,就交换两个节点的值,这样大的数据就在堆顶。但是第一次交换完成后,只是这颗子树是大堆的结构,这样的方法叫做向上调整,所以依次向上调整,直到堆顶元素比它大,当80遍历完成后这颗树就是大堆的结构。
代码实现
/**
* 向上调整,调孩子
* @param child
*/
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while(child > 0) {
if(elem[parent] < elem[child]) {
int tmp = elem[parent];
elem[parent] = elem[child];
elem[child] = tmp;
child = parent;
parent = (child - 1) / 2;
}else {
break;
}
}
}
/**
* 堆中插入,插入到最后面,向上调整,保持大堆
* @param val
*/
public void offer(int val) {
//判断空间,不够则扩容
if(isFull()) {
this.elem = Arrays.copyOf(elem, elem.length * 2);
}
this.elem[usedSize++] = val;
shiftUp(usedSize - 1);
}
堆中删除数据
🎄堆中删除数据只能删除堆顶元素,将堆顶元素和最后一个元素交换,然后控制usedSize将最后一个元素删除。由于原来就是大堆的结构,交换完成后,只有堆顶元素这颗树不符合大堆结构,只需要将堆顶数据进行向下调整,整棵树就保持大堆结构。
代码实现
public int pop() {
if(isEmpty()) {
throw new NullArrayExpection("空数组");
}
int tmp = this.elem[0];
this.elem[0] = this.elem[usedSize - 1];
this.elem[usedSize - 1] = tmp;
this.usedSize--;
shiftDown(0, usedSize);
return tmp;
}
获取堆顶元素
😊如果堆不为空,直接访问第一个数据即可。注意只获取不删除。
代码实现
public int peek() {
if(isEmpty()) {
throw new NullArrayExpection("空数组");
}
return this.elem[0];
}
TopK问题
题目
设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]
解析
🧢首先将前k个数据建大堆,即堆顶元素就是这k个数据中最大的。然后其他数据依次和堆顶元素比较,如果比它小就弹出堆顶数据,插入这个数据。当数组遍历完成后,这个堆中存储的就是前k个最小的数据。
代码实现
class Solution {
class IntCmp implements Comparator<Integer> {
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
public int[] smallestK(int[] arr, int k) {
if(k == 0) {
return new int[0];
}
int[] arr2 = new int[k];
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());
for(int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
for(int i = k; i < arr.length; i++) {
int tmp = priorityQueue.peek();
if(arr[i] < tmp) {
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for(int i = 0; i < k; i++) {
arr2[i] = priorityQueue.poll();
}
return arr2;
}
}
注意:
🤨PriorityQueue中如果传入比较器,会优先调用比较器。如果没有传比较器,那么这个数据必须具有可比较性,意味需要实现Comparable接口,重写CompareTo方法。源码中会向上转型为Comparable调用CompareTo方法。
🤨由于int的包装类型是Integer,如果不传递比较器,会调用Integer中的CompareTo方法,默认是小堆。那么要建大堆就需要传入比较器,改变比较的判定。
小结:
🐵在学习集合类型时,我们可以去看一些源码,明白底层的原理。这样当我们使用它时就会得心应手。