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

系列文章目录

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

常用接口介绍和堆的应用

  1. PriorityQueue的特性
  2. .PriorityQueue常用接口介绍
  3. top-k问题
  4. 堆排序
  5. PriorityQueue中插入对象
  6. 元素的比较
  7. .对象的比较
  8. .集合框架中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) {
        // 创建一个空的优先级队列,底层默认容量是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]

思路:

  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*log2n
        for(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>{

        @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.堆排序

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

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

  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 是最小的
        @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两种方式。

  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置为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;
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值