1. 优先级队列
1.1 概念
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话.
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
2. 优先级队列的模拟实现
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.2 堆的性质
- 堆是一颗完全二叉树
- 堆中的某个结点的值总是不大于或者不小于父结点的值
2.3 堆的存储方式
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储.
实例如上图所示.
将元素存储到数组中后,可以根据二叉树章节的性质对树进行还原。假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数(前提),则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数(前提),则节点i的右孩子下标为2 * i + 2,否则没有右孩子
2.4 堆的创建
2.4.1 向下调整创建堆
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
首先将数组中的元素按照二叉树层序遍历的方法进行存放.
接下来,我们要做的就是对上面这颗二叉树进行向下调整.那么如何向下调整呢:我们以大根堆为例来说明.
- 从堆的最后一棵子树开始比较父结点和子结点的大小关系.
- 如果父结点的值小于两个子结点其中一个的值,则交换两个值,如果不是则停止向下调整.
- 之后把父结点向上移动,再次向下调整,直到遇到父结点大于子结点或者调整到了根结点.
下面画图来举例,我们给定一个任意完全二叉树:
之后以此类推…
为什么要从子树开始调整呢?因为首先要保证子树是大根堆,调整根层数更小的树的时候才会好调整.就像我们盖房子一样,先要把地基打牢,才可以盖得起高楼大厦.
接下来我们通过代码来创建一个大根堆:
public class MyPriorityQueue {
public int usedSize;
public int[] elem;//堆通过数组来实现
public MyPriorityQueue(int[] elem) {
this.elem = elem;
usedSize += elem.length;
}
public void createBigHeap(){
for (int parent = (usedSize-2)/2; parent >= 0 ; parent--) {//从最后一棵子树开始调整
//之后根向上父结点向上走
shiftDown(parent,usedSize-1);//向下调整
//子结点永远是这棵树的最后一个结点
}
}
/**
* 向下调整
* @param parent 父节点
* @param end 结束位置
*/
private void shiftDown(int parent,int end){
int child = 2*parent+1;//此时child是左孩子
while (child <= end){
if (child+1 < usedSize && elem[child] < elem[child+1]){//若右孩子存在,并且大于左孩子
child++;//让child到右孩子这里
}
if (elem[parent] < elem[child]){
swap(parent,child);
parent = child;
child = 2*child+1;
}else {
break;//如果父结点不大于子结点,说明已经调整完成,因为是从下往上调的
}
}
}
private void swap(int s1,int s2){
int tmp = elem[s1];
elem[s1] = elem[s2];
elem[s2] = tmp;
}
}
2.4.1 创建堆的时间复杂度推导
因此:建堆的时间复杂度为O(N).
2.5 堆的插入与删除
2.5.1 堆的插入
堆的插入分下面两个步骤:
- 把结点插入树的最后一个位置
- 把这个结点进行向上调整
那么,向上调整又如何调整呢?我们以大根堆为例
- 把插入元素所在的结点与父节点进行比较.
- 如果父结点小于插入结点,则交换两个结点,否则停止向上调整.
- 如果成功交换,重复上述步骤,知道父结点大于子结点或者比较到根结点.
下面我们通过代码来展示:
/**
* 插入元素
* @param val 要插入的值
*/
public void offer(int val){
if (isFull()){
this.elem = Arrays.copyOf(elem,elem.length*2);
}
elem[usedSize] = val;
usedSize++;
shiftUp(usedSize-1);//这里注意是usedSize-1,因为usedSize++过,现在需要
//向上调整的元素是usedSize-1位置上的元素
}
/**
* 向上调整
* @param child 因为向上调整的终点都是根结点,所以传入child
*/
private void shiftUp(int child){
int parent = (child - 1)/2;
while (child > 0){
if (elem[parent] < elem[child]){
swap(elem[parent],elem[child]);
child = parent;
parent = (parent-1)/2;
}else {
break;
}
}
}
/**
* 判断堆元素是否为满
* @return
*/
private boolean isFull(){
if (elem.length == usedSize){
return true;
}else {
return false;
}
}
2.5.2 堆的删除
注意:在删除元素的时候,一定删除的是堆顶元素.
- 将堆顶元素和最后一个元素进行交换
- 删除最后一个元素
- 将堆顶结点进行向下调整
下面通过代码来展示:
/**
* 删除堆顶元素
*/
public void poll(){
if (usedSize == 0){
return;
}
swap(elem[0],elem[usedSize-1]);
usedSize --;
shiftDown(0,usedSize-1);
}
3. PriorityQueue
3.1 PriorityQueue的性质
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue.
关于PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为O(log2N)
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素.要想创建大根堆,必须传入比较器对象.
3.2 PriorityQueue的使用
- 构造方法
构造器 | 功能介绍 |
---|---|
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛IllegalArgumentException异常 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
public PriorityQueue(Comparator<? super E> comparator) | 传入比较器----改变元素之间的比较规则 |
源码如下:
private static final int DEFAULT_INITIAL_CAPACITY = 11;//默认容量
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityQueue(int initialCapacity) {//用户传入自定义容量
this(initialCapacity, null);
}
public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);//通过比较器改变优先级队列中元素的比较规则
}
public PriorityQueue(Collection<? extends E> c) {
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
initElementsFromCollection(ss);
}
else if (c instanceof PriorityQueue<?>) {
PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
initFromPriorityQueue(pq);
}
else {
this.comparator = null;
initFromCollection(c);
}
}
使用实例:
static void TestPriorityQueue(){
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
通过传入比较器来创建大根堆:
public class Compare implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class Main {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>(new Compare());
priorityQueue1.offer(1);
priorityQueue1.offer(2);
priorityQueue1.offer(3);
priorityQueue1.offer(4);
priorityQueue1.offer(5);
System.out.println(priorityQueue1);//通过传入比较器对象,可以构建大根堆
}
}
也可以通过传入比较器使得不可比较的对象变为可比较的对象:
public class Student{
public String name;
public int age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
import java.util.Comparator;
/**
* 年龄比较规则
*/
public class Compare1 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.age - o1.age;
}
}
public class Main {
public static void main(String[] args) {
PriorityQueue<Student> priorityQueue2 = new PriorityQueue<>(new Compare1());
//由于Student没有默认创建比较方法,所以必须传入比较器对象,否者异常
priorityQueue2.offer(new Student("zhangsan",12));
priorityQueue2.offer(new Student("lisi",17));
priorityQueue2.offer(new Student("wangwu",19));
System.out.println(priorityQueue2);//按照年龄进行大根堆构建
}
}
也可以不传入比较器,在类中重写Comparable接口的compareTo方法:
public class Student implements Comparable<Student>{
public String name;
public int age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
return this.name.compareTo(o.name);
}
}
public class Main {
public static void main(String[] args) {
PriorityQueue<Student> priorityQueue3 = new PriorityQueue<>();
priorityQueue3.offer(new Student("zhang",12));
priorityQueue3.offer(new Student("li",15));
priorityQueue3.offer(new Student("wang",17));
System.out.println(priorityQueue3);
}
}
- 插入,删除,获取优先级队列中的方法
函数名 | 功能介绍 |
---|---|
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
- 优先级队列的扩容
jdk17 的源码如下:
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);//大于队列大小的时候,进行扩容
siftUp(i, e);
size = i + 1;
return true;
}
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1
/* preferred growth */);
queue = Arrays.copyOf(queue, newCapacity);
}
优先级队列的扩容说明:
- 容量小于64的时候,按照2倍扩容.
- 容量大于64的时候,按照1.5倍扩容.
4. top-k问题
这种算法一般适用于数据比较大的情况下,比如要在1亿,甚至10亿数据中找出前k的数据.
OJ链接
class BigComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;//通过比较器来创建大根堆
}
}
public class Top_k {
public int[] smallestK(int[] arr, int k) {
if (k <= 0){;
return new int[0];//如果k==0,返回空数组
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new BigComparator());
//先取前k个元素放入
for (int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
//如果堆顶元素大于遍历到的元素,删除堆顶元素,让遍历到的元素进来
for (int i = k; i < arr.length; i++) {
if (priorityQueue.peek() > arr[i]){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
int[] array = new int[k];
//使用数组取出前k个元素
for (int i = 0; i < k; i++) {
array[i] = priorityQueue.poll();
}
return array;
}
}