1. 优先级队列
队列是一种先进先出的数据结构,而如果我们操作的数据带有优先级,我们出队的时候就会先出队优先级最高的元素.比如打游戏的时候有人给你打电话,操作系统看来有俩个进程,优先会处理打电话.
主要功能
1> 返回最高优先级对象
2> 添加新的对象
2. 堆的概念
2.1 认识堆
优先级队列是堆的一种.我们先来看看底层的集合,我们可以从图中看出,PriorityQueue实现了Queue接口,优先级队列就是一种完全二叉树.PriorityQueue底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。
2.2 堆的分类
堆分为大根堆和小根堆
1> 小根堆
根结点一定小于孩子结点->小根堆
2> 大根堆
根结点一定大于孩子结点->大根堆
2.3 堆的存储方式
其实从开始我们强调堆是一种完全二叉树,并且从上面来看,我们的堆是用数组来存储的(数组里面的存储顺序是层序遍历),那么为什么要这样呢?为什么完全二叉树要用数组来存储呢?数组可以存储非完全二叉树吗?
如图可以看出,完全二叉树存到数组里面是不会浪费空间,而非完全二叉树会有空间的浪费,
因此有以下结论:
非完全二叉树->用链式结构
完全二叉树->用数组存储
2.4 重要的性质
已知双亲求孩子: 左孩子: 2 * i +1,右孩子:2 * i +2
已知孩子求双亲: 双亲: ( i - 1 ) / 2
3. 模拟实现大根堆
3.1 前置变量介绍
我们使用数组来实现堆的创建,数组的顺序就是我们二叉树层序遍历的顺序,首先我们定义一个数组elem,然后我们用usedSize来记录当前堆中有效数据的个数.我们再提供一个构造方法,初始化数组容量为10,然后我们提供一个initElem方法,用来接受用户给定的数组,把用户给定的数组逐个赋值到elem里面去.
3.2 数组调整成大根堆
主要的调整过程:
1. 从最后一棵子树开始调整.
2. 找到左右孩子的最大值和根结点进行比较,如果比根结点大,就进行交换
3. 主方法: 父节点下标 = (usedSize - 1 - 1) / 2; siftDown方法: 左孩子下标 = parent*2+1
4. 一直调整到0下标的这棵树为止.
就差不多和我下面第一张图的过程差不多,反正每次主方法调用siftDown都要从当前根结点一直向下进行调整.
时间复杂度: O(n)
具体的推导方法:
如果我们采用向上调整的方式则会复杂一些,因为我们还要调整最后一层: O(n*logn)
3.3 堆的插入
在堆里面我们通过向上调整进行元素的插入,先从主方法把我们的元素放在我们二叉树的最后部分上,此时还需要进行堆是否是满的判断,如果是满的就扩容,然后我们进入siftUp方法,我们把usedSize传进去,然后依次让插入元素和每个父节点进行比较,如果大的话就进行交换,然后进入下一次循环,如果小于就直接退出循环了,因为本身就是一个大根堆,如果比最后的父节点还小,那么前面的父节点就更不可能比它大了.
3.4 堆的删除
主要步骤:
1. 我们把数组0下标的值和最后一个下标的值交换
2. 对0下标进行向下查找.
3.5 小总结
数组调整成大根堆 : 对每个父节点进行向下调整.
堆的元素插入: 插入到最后,然后和每个父节点进行比较,进行向上调整
堆的元素删除: 0和最后下标进行交换,然后我们对0下标进行向下调整.
3.6 具体代码
package 优先级队列_堆;
import ArrayList和顺序表.mylist.MyArrayListEmpty;
import java.util.Arrays;
public class MyHeap {
private int[] elem;
public int usedSize;//记录当前堆中有效数据的个数
public MyHeap() {
this.elem = new int[10];
}
//初始化数组
public void initElem(int[] array) {
for (int i = 0; i < array.length ; i++) {
elem[i] = array[i];
usedSize++;
}
}
//在这里把我们的数组变成一个大根堆
public void createHeap() {
//usedSzie - 1 表示的是我们孩子结点的下标,已知孩子求父亲,那么就再-1,然后/2
for (int parent = (usedSize - 1 - 1 )/2; parent >= 0 ; parent--) {//确定每棵子树parent的下标
//每棵子树向下调整
siftDown(parent,usedSize);//parent确定每颗子树的根结点,usedSize确定每颗子树的结束位置
}
}
//向下调整
//时间复杂度O(n)
private void siftDown(int parent, int len) {
//首先我们知道了父节点的下标,我们根据公式求出它左孩子的下标
int child = parent * 2 + 1;
//我们来找最大值
while (child < len) {
//先找到左右孩子的最大值下标
if(child + 1 < len && elem[child] < elem[child + 1]) {
child = child + 1;//child记录的是最大值的下标
}
//再比较和根结点的大小
if(elem[child] > elem[parent]) {
//交换值
int temp = elem[parent];
elem[parent] = elem[child];
elem[child] = temp;
//跟新child的下标
parent = child;
child = parent * 2 + 1;
}else {
//本身是最大值,则不调整了,直接退出循环
break;
}
}
}
//TODO 堆的插入和删除
//向上调整
//直接插入到最后,然后和根进行大小的比较
public void push(int val) {
//判断是否是满的
if(isFull()){
//是满的就扩容
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
//向上调整
siftUp(usedSize);
usedSize++;
}
public boolean isFull() {
return usedSize == elem.length;
}
public void siftUp(int child) {
//已知孩子下标求父结点下标
int parent = (child - 1) / 2;
//比较父亲和插入结点的大小
while (child > 0)
if(elem[parent] < elem[child]) {
//如果小于就交换值
swap(parent,child);
//更新
child = parent;
parent = (child - 1)/2;
}else {
break;//因为本身就是一个大根堆,如果比上最小的根都是比它还小,那么就没有比较的必要了,直接break即可
}
}
private void swap(int i,int j) {
int tmp = elem[i];
elem[i] = elem[j];
elem[j] =tmp;
}
//向上调整建堆的时间复杂度会很大,因为底层元素也得调整,而向下调整是不用调整底层元素的:O(n) = n*log2^n
//TODO 堆的删除
//1. 交换0下标和最后一个下标的值
//2. 向下调整0下标这棵树就行
public int pop() {
//判空
if(empty()) {
//抛出异常
throw new MyArrayListEmpty("堆为空!");
}
//记录原先的值
int oldVal = elem[0];
//交换第一个和最后一个元素
swap(0,usedSize - 1);
usedSize -- ;
//向下调整0下标这棵树
siftDown(0,usedSize);
return oldVal;
}
public boolean empty() {
return usedSize == 0;
}
}
package 优先级队列_堆;
public class MyHeapIsEmpty extends RuntimeException{
public MyHeapIsEmpty(String s) {
super(s);
}
}
4. java底层PriorityQueue常用接口介绍
4.1 宏观了解PriorityQueue
Java中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,我们主要介绍
根据上面的图,我们可以知道PriorityQueue的几个特性:
1. PriorityQueue中放置的类型必须是可以比较大小的
比如我们把Student自定义类放进入,因为没有实现Compareable接口,是无法进行比较的,因此无法构造堆
2. 不能插入null对象
3. 有自动扩容机制,可以任意插入多个元素(后续会说明扩容多少倍)
4. 插入删除元素的的时间复杂度都是O(log2N) (二叉树的高度)
5. PriorityQueue默认是小根堆,如果要转换为大根堆,我们需要自己写一个比较器(后面会说明)
4.2 层源码的解释
4.2.1 构造方法
我们这边介绍四种构造方法
我们的PriorityQueue默认是小根堆
构造器 | 功能介绍 |
PriorityQueue() | 创建一个空的优先级队列,默认容量为11 |
PriorityQueue(int initialCapacity) | 用户自定义容量 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
PriorityQueue(比较器) | 用户自己来设置大根堆 |
我们写比较器就先写一个类,然后我们需要实现Comparator接口,然后根据compareTo的返回值堆o1,o2进行排序.
具体代码:
class Imp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
//TODO 主要是这个逻辑
// return o1.compareTo(o2);//小根堆写法
return o2.compareTo(o1);//大根堆写法
}
}
//TODO 自己实现一个大根堆
public static void main(String[] args) {
Imp imp = new Imp();
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(imp);//传入比较器
priorityQueue.offer(10);
priorityQueue.offer(5);
priorityQueue.offer(6);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.poll());
}
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 static void main(String[] args) {
Test.TestPriorityQueue();
}
}
4.3 增加元素的源码
4.4 扩容机制的解释
优先级队列的扩容说明:
如果容量小于64时,是按照oldCapacity的2倍方式扩容的
如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
4.5 常用方法
函数名 | 功能介绍 |
boolean offer(E e) | 插入元素e,插入成功返回true,对象为空抛出空指针异常,时间复杂度为O(log2N) |
E peek() | 获得优先级最高的元素,队列为空返回null(但是元素本身还在优先级队列里面) |
E poll() | 移除优先级 最高的元素并返回,如果为空返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
具体实例
public static void main(String[] args) {
int[] arr = {4,1,9,2,8,0,7,3,6,5};
//默认创建的是小根堆
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
for (int e: arr) {
q.offer(e);
}
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
// 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
q.poll();
q.poll();
System.out.println(q.size()); // 打印优先级队列中有效元素个数
System.out.println(q.peek()); // 获取优先级最高的元素
q.offer(0);
System.out.println(q.peek()); // 获取优先级最高的元素
// 将优先级队列中的有效元素删除掉,检测其是否为空
q.clear();
if(q.isEmpty()){
System.out.println("优先级队列已经为空!!!");
}else{
System.out.println("优先级队列不为空");
}
}
//运行结果
10
0
8
2
0
优先级队列已经为空!!!
5. OJ题练习
题目: 得到前k个最小的数:面试题 17.14. 最小K个数 - 力扣(LeetCode)
法1: 构造小根堆,出k次
步骤:
1.我们根据给定数组构造一个小根堆
2. 出队k次,每出一次元素我们就对0下标进行向下调整
具体代码:
public static void main4(String[] args) {
//建立小根堆
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
int[] array = {10,3,15,7,19,9};
//把数组元素放进堆里面,向上调整建立小根堆O(n*logn)
for (int i = 0; i < array.length; i++) {
priorityQueue.offer(array[i]);
}
//出k次O(n) = klogn
int k = 3;
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = priorityQueue.poll();
}
System.out.println(Arrays.toString(ret));
}
法2:构造大根堆,对顶和k后面的元素比
步骤:
1. 建立一个大根堆,把前k个元素放入大根堆
2. 让k后面的元素和堆顶元素进行比较,如果比根小,就把堆顶元素删除,加入该元素
具体代码:
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
class Imp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
//TODO 主要是这个逻辑
// return o1.compareTo(o2);//小根堆写法
return o2.compareTo(o1);//大根堆写法
}
}
class Solution {
public int[] smallestK(int[] arr, int k) {
//创建一个大根堆,并且把前k个元素入栈
//把比较器放进去,形成大根堆
Imp imp = new Imp();
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(imp);
int[] temp = new int[k];
for (int i = 0; i < k; i++) {
priorityQueue.offer(arr[i]);
}
if(priorityQueue.peek() == null) {
return temp;
}
//后面k+1后面下标的元素和大根堆堆顶进行比较
for (int i = k; i < arr.length ; i++) {
int com = priorityQueue.peek();
if(com > arr[i]) {
//如果比根小,出堆顶元素,并且把com加入到堆里面
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
//最后把大根堆元素取出来
for (int i = 0; i < k; i++) {
temp[i] = priorityQueue.poll();
}
return temp;
}
}
6. java对象的比较
6.1 基于==比较
基本数据类型直接比较的是值.
引用数据类型像String,我们比较的是地址.有点特殊,下面的代码会解释
自定义类型,我们比较的也是地址,但是,我们的地址是因为栈中的自定义类型指向的是堆中对象的地址,它们new出来时不同的.
6.2 基于equals比较
注意我们如果重写equals方法,我们要设定自己比较的值,我们就按照以下步骤
1. 如果指向的是同一个对象就返回True
2. 如果传入的为null就返回false3. 如果传入的不是比较类或者比较类的子类就返回false
4. 如果我们比较的值是引用类型的值,就要调用它自己的equals方法
注意: equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。
快速查找方法:ctr+fn+f 在搜索里面找
6.3 基于Comparble比较
我们自己自定义的类型,如果想比较出个大小,而不是判断等不等于,我们就在定义类的时候,实现Comparble接口,重写comparTo方法来进行设定比较的标准.
6.4 基于比较器比较Comparator
Comparble一旦写死就不能改了,不灵活,而我们的比较器Conparator就更加灵活,我们直接可以根据比较器里面的标准来对我们对象的值进行比较,并且对类的侵入性不强.我们先写一个比较类实现Comparator接口,重写compare方法.
总结:
equals的返回值是boolean类型,其他俩个是int类型
Comparable和Comparator的区别:前一种相当于写死了对类的侵入性强,后者就比较灵活对类侵入性弱
一般写一个类,我们都要重写equals,实现Comparable,还要hashCode(后面会解释)