【java数据结构】优先级队列(堆)
博客最后附有整篇博客的全部代码!!!
一、优先级队列
1.1 概念
队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列是,可能需要优先级高的元素先出队列。比如:双十一,购物车中有一堆自己想要的商品,但是你的资金有限,所以购买的时候肯定买当前需求较高的商品。
在这种情况下,数据结构提供两个最基本操作,一个是返回最高优先级对象。一个是添加新对象。这种数据结构就是优先级队列。
二、优先级队列的模拟实现
JDK1.8 中的 PriorityQueue底层使用了堆这种数据结构 ,而堆实际就是在完全二叉树的基础上进行了一些调整。
2.1 堆的概念
如果有一个 关键码的集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中 并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且Ki >= K2i+2) i = 0 , 1 , 2… ,则 称为小堆 ( 或大堆) 。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2.1.1堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
2.2 堆的存储方式
从堆的概念可知, 堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。
注意:对于 非完全二叉树,则不适合使用顺序方式进行存储 ,因为为了能够还原二叉树, 空间中必须要存储空节点,就会导致空间利用率比较低。
将元素存储到数组中后,可以根据二叉树性质 对树进行还原。假设 i 为节点在数组中的下标,则有:
- 如果 i 为 0 ,则 i 表示的节点为根节点,否则 i 节点的双亲节点为 (i - 1)/2
- 如果 2 * i + 1 小于节点个数,则节点 i 的左孩子下标为 2 * i + 1 ,否则没有左孩子
- 如果 2 * i + 2 小于节点个数,则节点 i 的右孩子下标为 2 * i + 2 ,否则没有右孩子
2.3 堆的创建
2.3.1 向下调整(以小堆为例):
向下调整算法:选出其左右孩子中值较小值元素(建大堆就选较大元素,建小堆就选较小元素,这里以建小堆为例),将这个元素和根节点进行比较,若比根节点还小,就和根节点交换,交换后可能导致子树不满足堆的性质,因此需要继续向下调整。
代码思路:
- 让 parent 标记需要调整的节点, child 标记 parent 的左孩子 (注意:parent如果有孩子一定先是有左孩子)
- 如果 parent 的左孩子存在,即 :child < size , 进行以下操作,直到 parent 的左孩子不存在
(1)parent右孩子是否存在,存在找到左右孩子中最小的孩子,让 child 进行标
(2)将parent 与较小的孩子 child 比较,
如果:
(2.1) parent 小于较小的孩子 child ,调整结束
(2.2) 否则:交换 parent 与较小的孩子 child ,交换完成之后, parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child ; child =parent*2+1; 然后继续 。
public void shiftDown(int[] array,int parent){
int child =parent*2+1;
int size=array.length;
while(child<size){
//如果孩子存在,找到左右孩子中较小的孩子,用child标记
if (child + 1 < size && array[child] < array[child+1]) {
child=child+1;
}
if(array[parent]<=array[child]){
break;
}else{
int tmp=array[parent];
array[parent]=array[child];
array[child]=tmp;
parent=child;
child=parent*2+1;
}
}
}
注意:向下调整算法的使用,必须要求其左右子树必须为大根堆或者小根堆!!!
向下调整算法的时间复杂度为:O(logN),因为最坏情况是从根一路比较到叶子,比较的次数即为完全二叉树的高度次。
2.3.2 堆的创建
那对于普通的序列 { 1,5,3,8,7,6 } ,即根节点的左右子树不满足堆的特性,又该如何调整呢?
public void createHeap(){
for(int parent=(this.usedSize-1-1)/2;parent>=0;parent--){
shiftDown(parent,this.usedSize);
}
}
2.3.3 创建堆的时间复杂度
向下建堆的时间复杂度O(N):
向上建堆的时间复杂度O(n*log n ):
2.4 堆的插入与删除
2.4.1 堆的插入
思路:
1.先判断堆是否已经满了,满了则进行扩容。
2.将插入的元素放到堆的最后,向上调整,调整完还是大根堆。
代码:
public void push(int val) {
if(isFull()){
elem = Arrays.copyOf(elem, elem.length*2);
}
elem[usedSize] = val;
shiftUp(usedSize);
usedSize++;
}
private boolean isFull() {
return usedSize == elem.length;
}
private void shiftUp(int child) {
int parent = (child - 1) / 2;
while(parent >= 0) {
if (elem[parent] < elem[child]) {
int tmp=elem[child];
elem[child]=elem[parent];
elem[parent]=tmp;
child = parent;
parent = (child - 1) / 2;
}else {
break;
}
}
}
2.4.2 堆的删除
思路:
1.先判断堆里面有没有元素。
2.将堆顶元素和最后一个元素交换,改变usedSize,再进行向下调整。
private boolean isEmpty(){
return usedSize==0;
}
public void removeHeap(){
if(isEmpty()){
throw new RuntimeException("Heap is empty");
}else{
int child=usedSize-1;
int tmp=elem[child];
elem[child]=elem[0];
elem[0]=tmp;
usedSize--;
shiftDown(0,usedSize);
}
}
三、PriorityQueue
3.1 实现接口
- 使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为:
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆------即每次获取到的元素都是最小的元素
3.2 构造方法
3.3 常用方法
3.4 OJ练习
最小K个数
思路:
1.整体排序得到前K个最小值
2.将给的数组建立成一个小根堆,得到前K个最小值
3.建立K个元素的大根堆,随后用剩下的元素与堆顶元素进行比较,小于则删除堆顶元素,将元素插入,最后的得到的就是前K个最小值(注意:这里建堆的时候使用Intcmp类建大根堆(堆的建立默认是小根堆))。
代码:
class Intcmp implements Comparator<Integer> {
@Override
public int compare(Integer i1, Integer i2) {
return i2-i1;
}
}
public class Solution {
public int[] smallestK(int[] arr, int k) {
if(arr==null || k<=0){
return new int[0];
}
PriorityQueue<Integer> q = new PriorityQueue<>(new Intcmp());
for (int i = 0; i < k; i++) {
q.offer(arr[i]);
}
for (int i = k; i < arr.length; i++) {
if (arr[i] < q.peek()) {
q.poll(); // 移除堆顶元素
q.add(arr[i]); // 添加新元素
}
}
int[] res = new int[k];
// 输出堆中的元素
while (!q.isEmpty()) {
for(int i=0;i<k;i++){
res[i] = q.poll();
}
}
return res;
}
}