当前位置: 首页 > news >正文

集合及数据结构第十节(下)————常用接口介绍、堆的应用和java对象的比较

系列文章目录

集合及数据结构第十节(下)————常用接口介绍和堆的应用

常用接口介绍和堆的应用

  1. PriorityQueue的特性
  2. .PriorityQueue常用接口介绍
  3. top-k问题
  4. 堆排序
  5. PriorityQueue中插入对象
  6. 元素的比较
  7. .对象的比较
  8. .集合框架中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集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的PriorityBlockingQueue是线程安全的,所以主要介绍PriorityQueue

关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常

  2. 不能插入null对象,否则会抛出NullPointerException

  3. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容

  4. 插入和删除元素的时间复杂度为
    在这里插入图片描述

  5. PriorityQueue底层使用了堆数据结构

  6. 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]

思路:

  1. 整体建立一个小根堆
  2. 将堆顶元素出堆,出看、次就找到了最小的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问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆
  1. 用剩余的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.堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
  • 升序:建大堆
  • 降序:建小堆
  1. 利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序
在这里插入图片描述
堆排序代码实现:

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 的套路就是上面演示的

  1. 如果指向同一个对象,返回 true
  2. 如果传入的为 null,返回 false
  3. 如果传入的对象类型不是 Card,返回 false
  4. 按照类的实现目标完成比较,例如这里只要花色和数值一样,就认为是相同的牌
  5. 注意下调用其他引用类型的比较也需要 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两种方式。

  1. Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法
  2. 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现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;}}

http://www.mrgr.cn/news/12515.html

相关文章:

  • 【Linux入门】shell基础篇——while循环与until循环
  • 简易的 Websocket + 心跳机制 + 尝试重连
  • 全国首个高速公路5G-A通感一体基站在宁开通测试
  • Mysql的命令大全
  • 【附源码】Python :三棱柱建模
  • JVM经典的垃圾收集器
  • 自动化编译项目:使用 Bash 脚本与 CMake
  • 游戏开发中的打砖块反弹(godot)
  • 自动化刷题小练习
  • pytorch Dataset类代码学习
  • 牛客小白月赛99部分题解
  • 分布式系统
  • SpringBoot项目整合智谱AI + SSE推送流式数据到前端展示 + RxJava得浅显理解
  • WIFI驱动开发
  • 金融工程--基于akshare的数据获取
  • P1088 [NOIP2004 普及组] 火星人
  • Java | Leetcode Java题解之第376题摆动序列
  • MYSQL 优化
  • git安装及常用命令
  • asp.net core在win上的发布和部署