系列文章目录
集合及数据结构第十节(下)————常用接口介绍和堆的应用
常用接口介绍和堆的应用
- PriorityQueue的特性
- .PriorityQueue常用接口介绍
- top-k问题
- 堆排序
- PriorityQueue中插入对象
- 元素的比较
- .对象的比较
- .集合框架中PriorityQueue的比较方式
文章目录
- 系列文章目录
- 集合及数据结构第十节(下)————常用接口介绍和堆的应用
- 一、常用接口介绍
- 1.PriorityQueue的特性
- 2.PriorityQueue常用接口介绍
- 1. 优先级队列的构造
- 2. 插入/删除/获取优先级最高的元素
- 二、堆的应用
- 1. top-k问题
- 方法一:
- 方法二(优化)
- 2.堆排序
- 三、java对象的比较
- 1.PriorityQueue中插入对象
- 2.元素的比较
- 1. 基本类型的比较
- 2. 对象比较的问题
- 3.对象的比较( * * )
- 1. 覆写基类的equals
- 注意:
- 2.基于Comparble接口类的比较
- 3. 基于比较器比较
- 4 .三种方式对比
- 4.集合框架中PriorityQueue的比较方式
一、常用接口介绍
1.PriorityQueue的特性
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,所以主要介绍PriorityQueue
关于PriorityQueue的使用要注意:
- 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
-
PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常。
-
不能插入null对象,否则会抛出NullPointerException
-
没有容量限制,可以插入任意多个元素,其内部可以自动扩容
-
插入和删除元素的时间复杂度为
-
PriorityQueue底层使用了堆数据结构
-
PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素
2.PriorityQueue常用接口介绍
1. 优先级队列的构造
此处只是列出了PriorityQueue中常见的几种构造方式
public static void main(String[] args) {
// 创建一个空的优先级队列,底层默认容量是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());
}
注意:默认情况下,PriorityQueue队列是小堆
public static void main(String[] args) {
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();//实例化一个PriorityQueue对象后,默认是一个小根堆
priorityQueue.offer(1);
priorityQueue.offer(2);
priorityQueue.offer(3);
priorityQueue.offer(4);
priorityQueue.offer(5);
System.out.println(priorityQueue.peek());
}
如果需要大堆需要用户提供比较器:
class Imp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);//大根堆
}
}
public static void main(String[] args) {
Imp imp = new Imp();
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(imp);
priorityQueue.offer(6);
priorityQueue.offer(2);
priorityQueue.offer(3);
priorityQueue.offer(4);
priorityQueue.offer(1);
System.out.println(priorityQueue.poll());
System.out.println(priorityQueue.peek());
}
此时创建出来的就是一个大堆
2. 插入/删除/获取优先级最高的元素
函数名 | 功能介绍 |
---|---|
boolean offer(E e) | 插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,时间复杂度 ,注意:空间不够时候会进行扩容 |
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("优先级队列不为空");
}
}
注意:以下是JDK 1.8中,PriorityQueue的扩容方式:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));//(oldCapacity + 2) 是二倍扩容
//(oldCapacity >> 1))是1.5倍扩容
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
优先级队列的扩容说明:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
二、堆的应用
1. top-k问题
top-k问题:最大或者最小的前k个数据
设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
方法一:
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]
思路:
- 整体建立一个小根堆
- 将堆顶元素出堆,出看、次就找到了最小的k个数
public int[] smallestK(int[] arr, int k) {
if (null == arr || k <= 0) {//当数组为空,或者k小于等于0时,不能进行查找
return new int[0];
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
//相当于是以向上调整的方式建立小根堆时间复杂度是:O(n*log2n)
for (int i = 0; i < arr.length; i++) {
priorityQueue.offer(arr[i]);
}
// 将优先级队列的前k个元素放到数组中
int[] ret = new int[k];
//时间复杂度:k*log2n
for(int i = 0; i < k; ++i){
ret[i] = priorityQueue.poll();
}
return ret;
}
该解法只是PriorityQueue的简单使用,并不是topK最好的做法
方法二(优化)
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
- 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
class Imp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);//大根堆
}
}
public int[] smallestK(int[] arr, int k) {//整体的时间复杂度是K*logK + N*logK - K*logK = N * logK
if (null == arr || k <= 0) {//当数组为空,或者k小于等于0时,不能进行查找
return new int[0];
}
/*PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Imp());*/
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);//大根堆
}
});//匿名内部类的写法
//O(k * log k)
for (int i = 0; i < k; i++) {//用数据集合中前K个元素来建堆
priorityQueue.offer(arr[i]);
}
//O(N - k) *logK
for (int i = k; i < arr.length; i++) {//从k + 1个元素开始,将剩下的元素1与堆顶元素进行比较
int top = priorityQueue.peek();//拿到堆顶元素
if (top > arr[i]){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
// 将优先级队列的前k个元素放到数组中
int[] ret = new int[k];
//时间复杂度:k*log2n,这个地方不能算是topK的复杂度,这个地方是这里整理数据
for(int i = 0; i < k; ++i){
ret[i] = priorityQueue.poll();
}
return ret;
}
}
2.堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 升序:建大堆
- 降序:建小堆
- 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
堆排序代码实现:
public class TestHeap {
private int[] elem;//用来存完全二叉树的数组
public int usedSize;//用来记录当前堆中有效的数据个数
public TestHeap(){//构造方法
this.elem = new int[10];//初始化数组大小为10
}
public void initElem(int[] array){//初始化elem数组
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;//拷贝一个有效数据加1
}
}
public void createHeap(){//创建大根堆
//usedSize - 1 -->len //usedSize - 1 - 1 -->拿到最后一个孩子节点(9)下标 //(usedSize - 1 - 1) / 2 -->拿到该孩子节点的父亲节点
//如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
for (int parent = (usedSize - 1 - 1) / 2;parent >= 0;parent--){//确定每棵子树parent的下标
siftDown(parent,usedSize);//每棵子树向下调整,传参为每颗子树的根和结束的位置
}
}
public void siftDown(int parent,int len){//每棵子树向下调整
int child = 2 * parent + 1;//parent节点的左孩子下标为2 * i + 1
while (child < len){//当至少有左孩子时
//在进行比较的时候要保证child + 1 < len,否则就会越界比较
if (child + 1 < len && elem[child] < elem[child + 1]){//左孩子和右孩子进行比较,如果右孩子的值大,那么就记录一下它的下标
child = child + 1;
}
//走完上述if语句,则child下标一定保存的是左右两个孩元素最大值的下标
if (elem[child] > elem[parent]){//child下标的元素大于parent下标的元素,进行交换
int temp = elem[child];
elem[child] = elem[parent];
elem[parent] = temp;
parent = child;//parent指向child的位置
child = 2 * parent + 1;//再接着对child的右孩子进行相同操作,parent节点的左孩子下标为2 * i + 1
}else{
break;//不需要比较了,直接break退出循环
}
}
}
public void swap(int child,int parent){
int temp = elem[child];
elem[child] = elem[parent];
elem[parent] = temp;
}
//堆排序代码实现
//时间复杂度是:N * logN
public void heapSort(){
int end = usedSize - 1;//记录最后一个元素为end
while (end > 0){//当end大于堆顶元素下标0时
swap(0,end);//交换0下标与end下标元素
siftDown(0,end);//0 ~ end下标的元素向下调整,让堆顶元素成为比其他下标数大但小于end下标元素
end--;//end向前移动一位确定下一个数
}
}
}
三、java对象的比较
1.PriorityQueue中插入对象
优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够进行比较,为了简单起见,我们只是插入了Integer类型,那优先级队列中能否插入自定义类型对象呢?
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public class TestPriorityQueue {
public static void TestPriorityQueue()
{
PriorityQueue<Card> p = new PriorityQueue<>();
p.offer(new Card(1, "♠"));
p.offer(new Card(2, "♠"));
}
public static void main(String[] args) {
TestPriorityQueue();
}
}
优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而此时Card是没有办法直接进行比较的,因此抛出异常
2.元素的比较
1. 基本类型的比较
在Java中,基本类型的对象可以直接比较大小
public class TestCompare {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a > b);
System.out.println(a < b);
System.out.println(a == b);
char c1 = 'A';
char c2 = 'B';
System.out.println(c1 > c2);
System.out.println(c1 < c2);
System.out.println(c1 == c2);
boolean b1 = true;
boolean b2 = false;
System.out.println(b1 == b2);
System.out.println(b1 != b2);
}
}
2. 对象比较的问题
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
public void main(String[] args) {
Card c1 = new Card(1, "♠");
Card c2 = new Card(2, "♠");
Card c3 = c1;
//System.out.println(c1 > c2); // 编译报错
System.out.println(c1 == c2); // 编译成功 ----> 打印false,因为c1和c2指向的是不同对象
//System.out.println(c1 < c2); // 编译报错
System.out.println(c1 == c3); // 编译成功 ----> 打印true,因为c1和c3指向的是同一个对象
}
c1、c2和c3分别是Card类型的引用变量,上述代码在比较编译时:
c1 > c2 编译失败
c1== c2 编译成功
c1 < c2 编译失败
从编译结果可以看出,Java中引用类型的变量不能直接按照 > 或者 < 方式进行比较。 那为什么 == 可以比较?
因为:对于用户实现自定义类型,都默认继承自Object类,而Object类中提供了equal方法,而 == 默认情况下调用的就是equal方法,但是该方法的比较规则是:没有比较引用变量引用对象的内容,而是直接比较引用变量的地址,但有些情况下该种比较就不符合题意。
// Object中equal的实现,可以看到:直接比较的是两个引用变量的地址
public boolean equals(Object obj) {
return (this == obj);
}
3.对象的比较( * * )
有些情况下,需要比较的是对象中的内容,比如:向优先级队列中插入某个对象时,需要对按照对象中内容来调整堆,那该如何处理呢
1. 覆写基类的equals
public class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@
Override
public boolean equals(Object o) {
// 自己和自己比较
if (this == o) {
return true;
}
// o如果是null对象,或者o不是Card的子类
if (o == null || !(o instanceof Card)) {
return false;
}
// 注意基本类型可以直接比较,但引用类型最好调用其equal方法
Card c = (Card) o;
return rank == c.rank
&& suit.equals(c.suit);
}
}
注意:
一般覆写 equals 的套路就是上面演示的
- 如果指向同一个对象,返回 true
- 如果传入的为 null,返回 false
- 如果传入的对象类型不是 Card,返回 false
- 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
- 注意下调用其他引用类型的比较也需要 equals,例如这里的 suit 的比较
覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较
2.基于Comparble接口类的比较
Comparble是JDK提供的泛型的比较接口类,源码实现具体如下:
public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象大于 o 指向的对象
int compareTo(E o);
}
对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法
public class Card implements Comparable<Card> {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
// 根据数值比较,不管花色
// 这里我们认为 null 是最小的
@Override
public int compareTo(Card o) {
if (o == null) {
return 1;
}
return rank - o.rank;
}
public static void main(String[] args){
Card p = new Card(1, "♠");
Card q = new Card(2, "♠");
Card o = new Card(1, "♠");
System.out.println(p.compareTo(o)); // == 0,表示牌相等
System.out.println(p.compareTo(q)); // < 0,表示 p 比较小
System.out.println(q.compareTo(p)); // > 0,表示 q 比较大
}
}
Compareble是java.lang中的接口类,可以直接使用
3. 基于比较器比较
按照比较器方式进行比较,具体步骤如下:
- 用户自定义比较器类,实现Comparator接口
public interface Comparator<T> {
// 返回值:
// < 0: 表示 o1 指向的对象小于 o2 指向的对象
// == 0: 表示 o1 指向的对象等于 o2 指向的对象
// > 0: 表示 o1 指向的对象等于 o2 指向的对象
int compare(T o1, T o2);
}
注意:区分Comparable和Comparator
- 覆写Comparator中的compare方法
import java.util.Comparator;
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
}
class CardComparator implements Comparator<Card> {
// 根据数值比较,不管花色
// 这里我们认为 null 是最小的
@Override
public int compare(Card o1, Card o2) {
if (o1 == o2) {
return 0;
} if
(o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
return o1.rank - o2.rank;
}
public static void main(String[] args){
Card p = new Card(1, "♠");
Card q = new Card(2, "♠");
Card o = new Card(1, "♠");
// 定义比较器对象
CardComparator cmptor = new CardComparator();
// 使用比较器对象进行比较
System.out.println(cmptor.compare(p, o)); // == 0,表示牌相等
System.out.println(cmptor.compare(p, q)); // < 0,表示 p 比较小
System.out.println(cmptor.compare(q, p)); // > 0,表示 q 比较大
}
}
注意:Comparator是java.util 包中的泛型接口类,使用时必须导入对应的包
4 .三种方式对比
覆写的方法 | 说明 |
---|---|
Object.equals | 因为所有类都是继承自 Object 的,所以直接覆写即可,不过只能比较相等与否 |
Comparable.compareTo | 需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于内部顺序 |
Comparator.compare | 需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性强 |
4.集合框架中PriorityQueue的比较方式
集合框架中的PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了:
Comparble和Comparator两种方式。
- Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法
- 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现Comparator接口并覆写comp
// JDK中PriorityQueue的实现:
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
// ...
// 默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 内部定义的比较器对象,用来接收用户实例化PriorityQueue对象时提供的比较器对象
private final Comparator<? super E> comparator;
// 用户如果没有提供比较器对象,使用默认的内部比较,将comparator置为null
public PriorityQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
// 如果用户提供了比较器,采用用户提供的比较器进行比较
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
// ...
// 向上调整:
// 如果用户没有提供比较器对象,采用Comparable进行比较
// 否则使用用户提供的比较器对象进行比较
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}
// 使用Comparable
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
// 使用用户提供的比较器对象进行比较
@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}
}