集合及数据结构第十节(下)————常用接口介绍、堆的应用和java对象的比较
系列文章目录
集合及数据结构第十节(下)————常用接口介绍和堆的应用
常用接口介绍和堆的应用
- 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) {// 创建一个空的优先级队列,底层默认容量是11PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacityPriorityQueue<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) {// 创建一个空的优先级队列,底层默认容量是11PriorityQueue<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>{@Overridepublic 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 codeif (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow 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*log2nfor(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>{@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);//大根堆}}public int[] smallestK(int[] arr, int k) {//整体的时间复杂度是K*logK + N*logK - K*logK = N * logKif (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>() {@Overridepublic 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) *logKfor (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)/2for (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 + 1while (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 * logNpublic void heapSort(){int end = usedSize - 1;//记录最后一个元素为endwhile (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;}@Overridepublic 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 是最小的@Overridepublic 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 是最小的@Overridepublic 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置为nullpublic 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 compatibilityif (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);elsesiftUpComparable(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;}}