一. 优先级队列:
1 概念: 队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。 这种数据结构就是优先级队列(Priority Queue)。
二. 优先级队列的模拟实现:
1. JDK1.8中的PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。 2. 堆的概念: 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i 1 且 Ki<= K2i 2 (Ki >= K2i 1 且 Ki >= K2i 2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。简单来说小堆就是,堆的实现底层->完全二叉树,的每一棵树的父亲节点大于左右孩子节点就是大根堆,相反是小根堆。 堆的性质: (1).堆中某个节点的值总是不大于或不小于其父节点的值 (2).堆总是一棵完全二叉树 3.堆的存储方式: 1. 堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储。 2.堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储 总结: 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2 如果2 * i 1 小于节点个数,则节点i的左孩子下标为2 * i 1,否则没有左孩子 如果2 * i 2 小于节点个数,则节点i的右孩子下标为2 * i 2,否则没有右孩子
4. 堆的创建(以大根堆):
1 堆向下调整:
建堆的时间复杂度: O(N) ; 向下调整复杂度 (logn)
思路:获得左右孩子的最大值,确定最大孩子,让后调整为大小根堆
图解:
代码:这里以建立大根堆为例子:
代码语言:javascript复制//创建大根堆 ,时间复杂度:O(N)
public void createHeap() {
//自下而上比较,创建堆
//parent为根节点序号
for (int parent = (usedSize-1-1) / 2; parent >= 0; parent--) {
siftDown(parent, usedSize);
}
}
/**
*
* @param parent:每颗子树开始调整的位置
* @param usedSize:判断每颗子树,调整结束位置
* 向下调整:每颗子树都从根节点,开始往下调整,效率高, 时间复杂度 O(N)
*/
public void siftDown(int parent, int usedSize) {
//先确定父亲节点并传过来
int child = parent*2 1;
while (child < usedSize) {
//获得左右孩子的最大值,确定最大孩子
if (child 1 < usedSize && elem[child] < elem[child 1]) {
child ;
}
//调整为大根堆
if (elem[child] > elem[parent]) {
swap(elem,child,parent);
//跟新孩子孩子父亲节点
parent = child;
child = parent * 2 1;
} else {
break;
}
}
}
5.堆的插入与删除:
(1).插入:
先将元素放入到底层空间中(注意:空间不够时需要扩容) 将最后新插入的节点向上调整,直到满足堆的性质
代码:
代码语言:javascript复制/**
* 插入,方式:向上调整,效率很慢
* 时间复杂度 O(N*logN)
*/
public void offer(int val) {
//判断是否满了,满了就扩容
if (isFull()) {
elem = Arrays.copyOf(elem, 2*elem.length);
}
//插入usedSize位置
elem[usedSize] = val;
//调整插入的节点
siftUp(usedSize);
this.usedSize ;
}
(2).删除:
注意:堆的删除一定删除的是堆顶元素
步骤1 判空
步骤2.将堆顶元素对堆中最后一个元素交换
步骤3. 对堆顶元素进行向下调整
图解:
代码:
代码语言:javascript复制/**
* 删除,
* 方式:把 0 位置和 usedSize-1位置交换,交换后调整 0位置,及其一下位置
*/
public int poll() {
if (isEmpty()) {
throw new PriorityQueueException("堆是空的!!");
}
int val = elem[0];//记录一下 0 位置好返回
swap(elem,0,usedSize-1);//交换
siftDown(0, usedSize-1);//调整
usedSize--;
return val;
}
代码语言:javascript复制6.用堆模拟实现优先级队列整个模拟代码呈现:
public class MyPriorityQueue {
public int[] elem;
public int usedSize;
public MyPriorityQueue() {
this.elem = new int[10];
}
//初始化
public void initElem(int[] array) {
for (int i = 0; i < array.length; i ) {
this.elem[i] = array[i];
this.usedSize ;
}
}
//创建大根堆 ,时间复杂度:O(N)
public void createHeap() {
//自下而上比较,创建堆
//parent为根节点序号
for (int parent = (usedSize-1-1) / 2; parent >= 0; parent--) {
siftDown(parent, usedSize);
}
}
/**
*
* @param parent:每颗子树开始调整的位置
* @param usedSize:判断每颗子树,调整结束位置
* 向下调整:每颗子树都从根节点,开始往下调整,效率高, 时间复杂度 O(N)
*/
public void siftDown(int parent, int usedSize) {
//先确定父亲节点并传过来
int child = parent*2 1;
while (child < usedSize) {
//获得左右孩子的最大值,确定最大孩子
if (child 1 < usedSize && elem[child] < elem[child 1]) {
child ;
}
//调整为大根堆
if (elem[child] > elem[parent]) {
swap(elem,child,parent);
//跟新孩子孩子父亲节点
parent = child;
child = parent * 2 1;
} else {
break;
}
}
}
private void swap(int[] elem, int i, int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] = tmp;
}
private void siftUp(int child) {
int parent = (child-1) / 2;
while (parent >= 0) {
if (elem[child] > elem[parent]) {
swap(elem, child, parent);
//更新孩子和父亲
child = parent;
parent = (parent-1) /2;
}else {
break;
}
}
}
/**
* 插入,方式:向上调整,效率很慢
* 时间复杂度 O(N*logN)
*/
public void offer(int val) {
//判断是否满了,满了就扩容
if (isFull()) {
elem = Arrays.copyOf(elem, 2*elem.length);
}
//插入usedSize位置
elem[usedSize] = val;
//调整插入的节点
siftUp(usedSize);
this.usedSize ;
}
public boolean isFull() {
return this.usedSize == elem.length;
}
/**
* 删除,
* 方式:把 0 位置和 usedSize-1位置交换,交换后调整 0位置,及其一下位置
*/
public int poll() {
if (isEmpty()) {
throw new PriorityQueueException("堆是空的!!");
}
int val = elem[0];//记录一下 0 位置好返回
swap(elem,0,usedSize-1);//交换
siftDown(0, usedSize-1);//调整
usedSize--;
return val;
}
public int peek() {
if (isEmpty()) {
throw new PriorityQueueException("堆是空的!!");
}
return elem[0];
}
public boolean isEmpty() {
return usedSize == 0;
}
/** 从小到大堆排序:
* 1.创建大根堆
* 2.交换 0 位置和 end位置(end=usedSize-1)
* 3.调整
* 4.end--
* 循环以上步骤即可
*
* 时间复杂度 O(N) 堆排序:0(NlogN)
*/
public void heapSort() {
int end = usedSize - 1;
while (end > 0) {
swap(elem, 0, end);
siftDown(0, end);
end--;
}
}
}
三.常用接口介绍:
1. PriorityQueue的特性: Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本博客要介绍PriorityQueue。
PriorityQueue的使用要注意事项:
1. 使用时必须导入PriorityQueue所在的包,即:
代码语言:javascript复制import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
可以重写相关方法和传比较器:
代码语言:javascript复制public static void main7(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
}
3. 不能插入null对象,否则会抛出NullPointerException 4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容 5. 插入和删除元素的时间复杂度为 6. PriorityQueue底层使用了堆数据结构 7. PriorityQueue默认情况下是小堆
2.优先级队列的构造:
注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
代码语言:javascript复制class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
}
3. 优先级队列的扩容说明: 如果容量小于64时,是按照oldCapacity的2倍方式扩容的 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
3 oj练习:最大或者最小的前k个数据. - 力扣(LeetCode) 解法一:(直接放入小根堆,然后放到返回数组删除)
代码:
代码语言:javascript复制PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
//时间复杂度 0(N*logN)
for (int i = 0; i < arr.length; i ) {
//默认是小根堆
priorityQueue.offer(arr[i]);
}
//时间复杂度 0(K*logN)
int[] ret = new int[k];
for (int i = 0; i < k; i ) {
ret[i] = priorityQueue.poll();
}
return ret;
解法二:把 N个元素的数组,前 K个变成大根堆,再与 N-K个比较, 如果第 N-K个,比堆顶元素小,就把堆顶元素删除,把这个元素放入堆中。
时间复杂度为 0( K*logK (N-K)*logK == N*logK)
这里的K可能很小,所以该算法比较好
代码:
代码语言:javascript复制/**
* 解法二:把 N个元素的数组,前 K个变成大根堆,再与 N-K个比较,
* 如果第 N-K个,比堆顶元素小,就把堆顶元素删除,把这个元素放入堆中。
*
* //时间复杂度 0( K*logK (N-K)*logK == N*logK)这里的K可能很小,所以该算法比较好
*/
//构建大根堆
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
public int[] smallestK2(int[] arr, int k) {
int[] ret = new int[k];
//注意K的范围
if(arr == null || k == 0) {
return ret;
}
//前 K个变成大根堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k,new IntCmp());
//时间复杂度 O(K*logK)
for (int i = 0; i < k; i ) {
//现在是大根堆
priorityQueue.offer(arr[i]);
}
//第 N-K个元素从,K下标开始
//时间复杂度:(N-K)*logK
for (int i = k; i < arr.length; i ) {
//peek一下堆顶元素,与开始第K个一直比较
int peekVal = priorityQueue.peek();
if (arr[i] < peekVal) {
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for (int i = 0; i < k; i ) {
ret[i] = priorityQueue.poll();
}
return ret;
}