java 堆(优先级队列)详解

news2025/1/13 3:37:02

JAVA堆以及优先级队列详解

  • 一、堆的模拟实现
    • 1.1堆的概念
    • 1.2 堆的性质
    • 1.3堆的存储结构
    • 1.4堆的创建
      • 1.4.1 只有根节点不满足堆的特性
      • 1.4.2 不只有根节点不满足堆的特性
        • 1.4.2.1 建堆代码
        • 1.4.2.2 建堆过程图示
      • 1.4.3 建堆的时间复杂度
    • 1.5堆的插入
      • 1.5.1堆的插入的基本思想
      • 1.5.2堆的插入的完整代码
      • 1.5.3堆的插入的过程图示
    • 1.6堆的删除
      • 1.6.1堆的删除的基本操作
      • 1.6.2堆的删除的代码
      • 1.6.3堆的删除的过程图示
  • 二、优先级队列
    • 2.1优先级队列的概念
    • 2.2优先级队列的常用接口
    • 2.3PriorityQueue的特性
    • 2.4PriorityQueue的构造方法
    • 2.5PriorityQueue的基本方法
    • 2.6PriorityQueue的扩容机制
    • 2.7集合框架中PriorityQueue的比较方式
    • 2.8使用PriorityQueue创建大小堆,解决TOPK问题
      • 2.8.1问题背景
      • 2.8.2解决办法
      • 2.8.1找k个最大值
      • 2.8.2找k个最小值
      • 2.8.3利用比较器构建大根堆
  • 三、堆排序
    • 3.1问题背景
    • 3.2误区
    • 3.3解决办法
    • 3.4具体代码实现
    • 3.5执行过程图示

一、堆的模拟实现

1.1堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

1.2 堆的性质

堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
如下图就是大根堆和小根堆
在这里插入图片描述

1.3堆的存储结构

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
在这里插入图片描述注意:

  • 对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。
  • 将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:
    如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
    如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
    如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

1.4堆的创建

要完成堆的创建,首先需要了解几个完全二叉树的性质。

  • 如果一致完全二叉树有n个结点,那么最后一个叶子结点的坐标是n-1(根节点是从0开始的),最后一个父节点的坐标是(n-1-1)/2
  • 如果父节点的下标是i,那么其左孩子(如果有)的下标是2i+1;右孩子(如果有)的下标是2i+1。

1.4.1 只有根节点不满足堆的特性

在这里插入图片描述如上图,发现只有根节点没有满足小根堆的特性,那么我们如何将这个堆调整至符合小根堆特性呢?
要知道这些数据实际都是用数组的形式进行存储的,我们可以通过下标去访问每个数据。

  1. 我们先用一个parent指向根节点,那么其左孩子child的下标是(2parent+1),右孩子的下标是child+1;
  2. 我们无非是比较三者谁大谁小,选取最小的哪一个(其下标重新赋值为child),将其与父节点(也就是parent)的值进行交换。
  3. 但是这样就会有新的问题,这回导致根节点的子树不再满足小根堆的性质,所以还要继续对子树进行调整,所以我们此时将parent的值赋值为child,指向更改后的子树的根节点,在重复上述步骤即可
    代码如下
public void shutDown(int parent,int useSize){
        int child = 2*parent+1;//找到左孩子
        while(child<useSize){//child<useSize可以作为循环的终止条件,也可以作为判断父节点是否有左孩子的条件
            if(child+1<useSize && elem[child]>elem[child+1]) {//child+1<useSize判断是否有右孩子,
                child++;//elem[child]<elem[child+1]结合child++用于取得左右孩子的最小值。
            }
            //注意:如果不满足child+1<useSize,也就是所只有左孩子没有右孩子,那么此时去判断左孩子值与根节点谁大谁小,
            // 这里千万不能把if(elem[parent]>elem[child])写到if(child+1<useSize && elem[child]>elem[child+1])里面去,这样的话就会有最后一个只有左孩子的父节点没有调整的情况
            if(elem[parent]>elem[child]){//如果父节点的值比子节点最小值要大,那么就要交换
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                //调整好这个父节点之后有可能会导致子树不再满足小根堆的特性,所以要继续往下调整
                parent = child;
                child = 2*parent+1;
            }
            else{
                break;//如果父节点的值比子节点最小值要小,那么就不交换,直接break,注意此时是不需要再往下去看子树是否满足小根堆的,一定是
                //满足的,因为前面的if里面是从最后的父节点一路往前调整的,所以后面的一定满足小根堆的特性
                }
        }
    }

1.4.2 不只有根节点不满足堆的特性

其实这种情况是非常好处理的,只需要遍历一遍父节点即可,把每个父节点都当做当前子树的根节点去处理即可

1.4.2.1 建堆代码

所以完整的建立小根堆的代码如下

public class TestHeap {
    public int[] elem;
    public int useSize;

    public TestHeap(){
        this.elem = new int[10];
        this.useSize = 0;
    }
    //初始化一个数组,将输入的数组全部用elem数组存起来
    public void  initElem(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            useSize++;
        }
    }
    //创建一个大根堆
    public void createShortHeap(){
        //从最后一个父节点开始向下去调整
        for (int i = (elem.length-1-1)/2; i >=0 ; i--) {
            //elem.length-1表示最后一个结点的下标,最后一个结点下标减一再除以二就是最后一个父节点的坐标
            shutDown(i,useSize);

        }
    }

    //向下调整
    public void shutDown(int parent,int useSize){
        int child = 2*parent+1;//找到左孩子
        while(child<useSize){//child<useSize可以作为循环的终止条件,也可以作为判断父节点是否有左孩子的条件
            if(child+1<useSize && elem[child]>elem[child+1]) {//child+1<useSize判断是否有右孩子,
                child++;//elem[child]<elem[child+1]结合child++用于取得左右孩子的最小值。
            }
            //注意:如果不满足child+1<useSize,也就是所只有左孩子没有右孩子,那么此时去判断左孩子值与根节点谁大谁小,
            // 这里千万不能把if(elem[parent]>elem[child])写到if(child+1<useSize && elem[child]>elem[child+1])里面去,这样的话就会有最后一个只有左孩子的父节点没有调整的情况
            if(elem[parent]>elem[child]){//如果父节点的值比子节点最小值要大,那么就要交换
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                //调整好这个父节点之后有可能会导致子树不再满足小根堆的特性,所以要继续往下调整
                parent = child;
                child = 2*parent+1;
            }
            else{
                break;//如果父节点的值比子节点最小值要小,那么就不交换,直接break,注意此时是不需要再往下去看子树是否满足小根堆的,一定是
                //满足的,因为前面的if里面是从最后的父节点一路往前调整的,所以后面的一定满足小根堆的特性
                }
        }
    }
}

测试代码

public class Test {
    public static void main(String[] args) {
        TestHeap testHeap = new TestHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        testHeap.initElem(array);
        for (int i = 0; i < testHeap.elem.length; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.createShortHeap();
        for (int i = 0; i < testHeap.elem.length; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
    }
}

测试结果
在这里插入图片描述

1.4.2.2 建堆过程图示

如下图是上面这个例子的建立小根堆的过程
在这里插入图片描述

1.4.3 建堆的时间复杂度

我们用满二叉树来进行分析,多几个结点不影响结果,满二叉树也方便计算
在这里插入图片描述
所以
第一层
在这里插入图片描述

1.5堆的插入

1.5.1堆的插入的基本思想

首先,堆的插入一定是从堆的末尾开始插入的,不能从中间开始插入
在对末尾插入之后该堆就不一定符合小根堆的特性了,需要向上调整。
如下图
在这里插入图片描述
首先需要知道的是,在完全二叉树里,如果子节点的下标是i,那么该子节点对应的父节点的下标就是(i-1)/2。

  1. 我们将插入结点的下标赋值为child,那么其父节点的下标为parent = (child-1)/2。
  2. 比较child的值与parent的值谁大谁小,如果child的值比较小,就需要调换child的值与parent的值,反之则不需要调整。
  3. 在一次调整之后,还需要往上继续调整,所以将child = parent;parent = (child-1)/2。

1.5.2堆的插入的完整代码


    //插入
    public void offer(int val) {
        if (isFull()) {//判断堆是不是满了,满了就调用copyof进行扩容;
            elem = Arrays.copyOf(elem, (elem.length) * 2);
        }
        //在堆的末尾插入
        this.elem[useSize] = val;
        //重新调整堆为小根堆
        shiftUp(useSize);
        //一定要注意这三句的顺序,最后useSize++
        useSize++;


    }

    //向上调整
    public void shiftUp(int child) {//child 就是插入的位置下标
        int parent = (child - 1) / 2;
        while (parent >= 0) {//注意这里是parent>=0,因为parent = 0时正好是根节点的情况,这是也是有可能需要调整的。
            if (elem[parent] > elem[child]) {//如果子节点的值比父节点的值要小,那么就是需要调整的。
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;//如果插入的数(孩子结点)的值比其父节点的值本来就要大,那么是满足小根堆特性的,不需要调整。
            }
        }

    }
    //判满
    public boolean isFull() {
        if (useSize == elem.length) {
            return true;
        }
        return false;
    }

测试代码

public class Test {
    public static void main(String[] args) {
        TestHeap testHeap = new TestHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        testHeap.initElem(array);
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.createShortHeap();
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.offer(10);
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
    }
}

测试结果
在这里插入图片描述

1.5.3堆的插入的过程图示

下图就是上述代码的执行过程
在这里插入图片描述

1.6堆的删除

1.6.1堆的删除的基本操作

首先堆删除的元素一定是堆首元素(也就是一定是数组首元素)
堆的删除的具体操作

  1. 将堆顶元素与堆尾元素互换,将堆尾元素删除(useSize-1)
  2. 从堆顶开始向下调整,重新调整至符合大根堆(小根堆)。

1.6.2堆的删除的代码

经过前面的代码学习,其实删除已经很简单了
向下调整的代码我们已经写过了,只需要调用即可

//删除
    public void poll(){
        if(!isEmpty()){
            elem[0] = elem[useSize-1];//堆尾元素赋值为堆首元素
            useSize--;//useSize-1表示删除最后一个元素
            shiftDown(0, useSize);
        }
        else{
            return;
        }
    }
    //判空
    public boolean isEmpty(){
        if(useSize == 0){
            return true;
        }
        return false;
    }
    //向下调整
    public void shiftDown(int parent, int useSize) {
        int child = 2 * parent + 1;//找到左孩子
        while (child < useSize) {//child<useSize可以作为循环的终止条件,也可以作为判断父节点是否有左孩子的条件
            if (child + 1 < useSize && elem[child] > elem[child + 1]) {//child+1<useSize判断是否有右孩子,
                child++;//elem[child]<elem[child+1]结合child++用于取得左右孩子的最小值。
            }
            //注意:如果不满足child+1<useSize,也就是所只有左孩子没有右孩子,那么此时去判断左孩子值与根节点谁大谁小,
            // 这里千万不能把if(elem[parent]>elem[child])写到if(child+1<useSize && elem[child]>elem[child+1])里面去,这样的话就会有最后一个只有左孩子的父节点没有调整的情况
            if (elem[parent] > elem[child]) {//如果父节点的值比子节点最小值要大,那么就要交换
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                //调整好这个父节点之后有可能会导致子树不再满足小根堆的特性,所以要继续往下调整
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;//如果父节点的值比子节点最小值要小,那么就不交换,直接break,注意此时是不需要再往下去看子树是否满足小根堆的,一定是
                //满足的,因为前面的if里面是从最后的父节点一路往前调整的,所以后面的一定满足小根堆的特性
            }
        }
    }

测试代码

public class Test {
    public static void main(String[] args) {
        TestHeap testHeap = new TestHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        testHeap.initElem(array);
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.createShortHeap();
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.offer(10);
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
        System.out.println();
        testHeap.poll();
        for (int i = 0; i < testHeap.useSize; i++) {
            System.out.print(testHeap.elem[i]+" ");
        }
    }
}

结果
在这里插入图片描述

1.6.3堆的删除的过程图示

在这里插入图片描述

二、优先级队列

2.1优先级队列的概念

在java中堆和优先级队列是一个概念,前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)(该段引用自比特讲义)。

2.2优先级队列的常用接口

Java集合框架中提供了PriorityQueuePriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安的,PriorityBlockingQueue是线程安全的。这里我们重点介绍PriorityQueue。
在这里插入图片描述

2.3PriorityQueue的特性

关于PriorityQueue的使用要注意:

  1. 使用时必须导入PriorityQueue,即import java.util.PriorityQueue;
  2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
  3. 不能插入null对象,否则会抛出NullPointerException异常
  4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  5. 插入和删除元素的时间复杂度为O(logN)
  6. PriorityQueue底层使用了堆数据结构
  7. PriorityQueue默认情况下是小堆—即每次获取到的元素都是最小的元素

2.4PriorityQueue的构造方法

PriorityQueue() 

创建一个PriorityQueue ,具有默认的初始容量(11),根据它们的natural ordering对其元素进行排序 。

PriorityQueue(Collection<? extends E> c) 

创建一个 包含了集合C中的所有元素的PriorityQueue。

PriorityQueue(int initialCapacity) 

创建PriorityQueue并指定的初始容量initialCapacity

示例

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Queue<Integer> priorityQueue1 = new PriorityQueue<>();
        priorityQueue1.offer(27);
        priorityQueue1.offer(15);
        priorityQueue1.offer(19);
        priorityQueue1.offer(18);
        priorityQueue1.offer(28);
        priorityQueue1.offer(34);
        priorityQueue1.offer(65);
        priorityQueue1.offer(49);
        priorityQueue1.offer(25);
        priorityQueue1.offer(37);
        //使用迭代去打印堆(数组)
        Iterator<Integer> iterator1 = priorityQueue1.iterator();
        while(iterator1.hasNext()){
            System.out.print(iterator1.next()+" ");
        }
        System.out.println();
//
        List<Integer> arraylist = new ArrayList<>();
        arraylist.add(27);
        arraylist.add(15);
        arraylist.add(19);
        arraylist.add(18);
        arraylist.add(28);
        arraylist.add(34);
        arraylist.add(65);
        arraylist.add(49);
        arraylist.add(25);
        arraylist.add(37);
        Queue<Integer> priorityQueue2 = new PriorityQueue<>(arraylist);
        Iterator<Integer> iterator2 = priorityQueue1.iterator();
        while(iterator2.hasNext()){
            System.out.print(iterator2.next()+" ");
        }
        System.out.println();
        
    }
}

结果
在这里插入图片描述

2.5PriorityQueue的基本方法

boolean add(E e) 

将指定的元素插入到此优先级队列中。

void clear() 

从此优先级队列中清空所有元素。

Comparator<? super E> comparator() 

返回用于为了在这个队列中的元素,或比较null如果此队列根据所述排序natural ordering的元素。

boolean contains(Object o) 

如果此队列包含指定的元素,则返回 true 。

Iterator<E> iterator() 

返回此队列中的元素的迭代器。

boolean offer(E e) 

将指定的元素插入到此优先级队列中。
与addq区别在于add堆满时直接抛出异常,而offer会自动扩容并且返回false;

E peek() 

检索但不删除此队列的头,如果此队列为空,则返回 null 。

E poll() 

检索并删除此队列的头,如果此队列为空,则返回 null 。

boolean remove(Object o) 

从该队列中删除指定元素的单个实例(如果存在)。

int size() 

返回此集合中的元素数。

Spliterator<E> spliterator() 

在此队列中的元素上创建late-binding和失败快速 Spliterator 。

Object[] toArray() 

返回一个包含此队列中所有元素的数组。

<T> T[] toArray(T[] a) 

返回一个包含此队列中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。

2.6PriorityQueue的扩容机制

以下是原生PriorityQueue的扩容原码

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//数组的最大长度是Integer所能表示的最大值减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));
	//从这个三目运算符就可以看出扩容的机制。
// 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来进行扩容

2.7集合框架中PriorityQueue的比较方式

集合框架中的PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了:
Comparble和Comparator两种方式。

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

java JDK中关于comparable和comparator的一些原生代码

// 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; 
 }
}

我们可以实际操作来看一下底层的运行逻辑

Queue<Integer> priorityQueue1 = new PriorityQueue<>();
priorityQueue1.offer(27);
priorityQueue1.offer(15);

那么底层实际上是如下图这样运行的
在这里插入图片描述而这里面调用的comparaTo实际上应该是在Integer类里面进行了覆写
在这里插入图片描述

2.8使用PriorityQueue创建大小堆,解决TOPK问题

2.8.1问题背景

假如说某市进行了一次统考,有很多学生(数据量很大),那么我们如何在众多学生中迅速找到这次考试中成绩最好的50人呢?
这就要用到堆这样的数据结构了。我们将问题简化,假如有7个学生,怎么去找前k名,或者怎么找k个分数最大值、怎么找第k大值。同理又怎么去找分数最低的k名学生。

2.8.2解决办法

2.8.1找k个最大值

先来说找k个分数最大值
我们可以定义一个只有k个元素的PriorityQueue,将7和元素顺序插入,在插入k后(比如说k = 3)那么我们得到结果3的元素的小根堆,并且此时堆顶元素是当前三个元素中最小的,那么我们再入堆时,就将该元素与堆顶元素进行比较,如果比堆顶大,那么就证明他在当前最大的数值这个集合中,我们就入堆,否则不如堆,继续往后。
此时这个思路里面的比较就需要我们重写comparable方法或者自己定义一个comparator比较器了。

具体代码如下

先定义一个学生类实现Comparable接口并且重写comparaTo方法

public class student implements Comparable<student>{
    String name;
    int age;
    double score;

    public student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    //重写comparableTo方法
    public int compareTo(student o) {
        return (int)(this.score-o.score);
    }
}
import java.util.*;

public class Main {
 public static void main(String[] args) {
        student student1 = new student("a", 27, 27.0);
        student student2 = new student("b", 15, 15.0);
        student student3 = new student("c", 19, 19.0);
        student student4 = new student("d", 18, 18.0);
        student student5 = new student("e", 28, 28.0);
        student student6 = new student("f", 34, 34.0);
        student student7 = new student("g", 65, 65.0);

        ArrayList<student> arrayList = new ArrayList<>();
        arrayList.add(student1);
        arrayList.add(student2);
        arrayList.add(student3);
        arrayList.add(student4);
        arrayList.add(student5);
        arrayList.add(student6);
        arrayList.add(student7);

        PriorityQueue<student> priorityQueue1  = new PriorityQueue<>(3);
        int i = 0;
        while(i<3){
            priorityQueue1.offer(arrayList.get(i));
            i++;
        }
        while(i<arrayList.size()){
            if(arrayList.get(i).score>priorityQueue1.peek().score){
                priorityQueue1.poll();
                priorityQueue1.offer(arrayList.get(i));
                i++;
            }
            else{
                i++;
            }
        }
        System.out.println();
}

测试结果
我们发现preorityqueue里面确实是成绩最高的三名学生
在这里插入图片描述

2.8.2找k个最小值

找k个最小值与找k个最大值的区别无非就在于要建立的是大根堆,每次都是比堆顶元素小才会入堆。
非常简单,只需要将comparaTo方法调转一下就可以。

student类的定义:

public class student implements Comparable<student>{
    String name;
    int age;
    double score;

    public student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    //重写comparableTo方法
    public int compareTo(student o) {
        return (int)(o.score-this.score);
    }
}

主函数

import java.util.*;

public class Main {
	public static void main(String[] args) {
        student student1 = new student("a", 27, 27.0);
        student student2 = new student("b", 15, 15.0);
        student student3 = new student("c", 19, 19.0);
        student student4 = new student("d", 18, 18.0);
        student student5 = new student("e", 28, 28.0);
        student student6 = new student("f", 34, 34.0);
        student student7 = new student("g", 65, 65.0);

        ArrayList<student> arrayList = new ArrayList<>();
        arrayList.add(student1);
        arrayList.add(student2);
        arrayList.add(student3);
        arrayList.add(student4);
        arrayList.add(student5);
        arrayList.add(student6);
        arrayList.add(student7);

        PriorityQueue<student> priorityQueue1  = new PriorityQueue<>(3);
        int i = 0;
        while(i<3){
            priorityQueue1.offer(arrayList.get(i));
            i++;
        }
        while(i<arrayList.size()){
            if(arrayList.get(i).score<priorityQueue1.peek().score){
                priorityQueue1.poll();
                priorityQueue1.offer(arrayList.get(i));
                i++;
            }
            else{
                i++;
            }
        }
        System.out.println();
    }
}

测试结果
在这里插入图片描述

2.8.3利用比较器构建大根堆

其实,PriorityQueue除了支持前面介绍过的三种构造方法外,其实还支持比较器作为参数参与构造,有以下两种

PriorityQueue(Comparator<? super E> comparator) 

创建具有默认初始容量(11)的 PriorityQueue ,并根据指定的比较器对其元素进行排序。

PriorityQueue(int initialCapacity, Comparator<? super E> comparator) 

创建具有 PriorityQueue初始容量的PriorityQueue,根据指定的比较器对其元素进行排序。

所以,除了student类实现comparable接口,重写compareTo方法,还可以自己创建一个比较器类,实现comparator接口,重写compare方法。然后实例化这个对象,将带实例化对象作为实参传入priorityqueue的构造函数。
比如下面就是采用重新构造比较器的形式去建立大根堆,去结果上述topK问题。
示例:
student类的定义

public class student {
    String name;
    int age;
    double score;

    public student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
 }

Main类

//构建一个用于大根创建的比较器
class maxScoreComprator implements Comparator<student>{
    @Override
    public int compare(student o1, student o2) {
        return (int)(o2.score- o1.score);
    }
}
public class Main {
public static void main(String[] args) {
        student student1 = new student("a", 27, 27.0);
        student student2 = new student("b", 15, 15.0);
        student student3 = new student("c", 19, 19.0);
        student student4 = new student("d", 18, 18.0);
        student student5 = new student("e", 28, 28.0);
        student student6 = new student("f", 34, 34.0);
        student student7 = new student("g", 65, 65.0);

        ArrayList<student> arrayList = new ArrayList<>();
        arrayList.add(student1);
        arrayList.add(student2);
        arrayList.add(student3);
        arrayList.add(student4);
        arrayList.add(student5);
        arrayList.add(student6);
        arrayList.add(student7);



        maxScoreComprator com1 = new maxScoreComprator();
        PriorityQueue<student> priorityQueue1  = new PriorityQueue<student>(3,com1);//将比较器作为实参传入

        int i = 0;
        while(i<3){
            priorityQueue1.offer(arrayList.get(i));
            i++;
        }
        while(i<arrayList.size()){
            if(arrayList.get(i).score<priorityQueue1.peek().score){
                priorityQueue1.poll();
                priorityQueue1.offer(arrayList.get(i));
                i++;
            }
            else{
                i++;
            }
        }
        System.out.println();
    }
}

测试结果
在这里插入图片描述
此外还可以简化以下传比较器时采用匿名内部类的形式

 PriorityQueue<student> priorityQueue1  = new PriorityQueue<student>(3,new Comparator<student>() {
            @Override
            public int compare(student o1, student o2) {
                return (int) (o2.score - o1.score);
            }
        });

三、堆排序

3.1问题背景

有一组数据:
27,15,191,18,28,34,65,49,25,37,对这一组数据按照从小到大排序

3.2误区

我们想用堆排序去解决,那么首先要确定的就是用大根堆还是小根堆呢?
一定要注意堆排序要想从大到小输出一组数据,用的不是大根堆,而是小根堆。因为实际上堆这个数据结构本身只有堆顶元素有排序意义,因为左右子树是并不知道谁大谁小的。如果用小根堆,我们只能建立另外一个数组,然后每次用这个数组去接受堆顶元素,每出堆一个就再次调整堆,这样无论是时间复杂度还是空间复杂度都是比较高的。
那么如何用小根堆得到从大到小的排序呢

3.3解决办法

  1. 我们得到一个数组,先将其调整为小根堆,这样堆顶元素就是这里面最小的元素了;
  2. 将堆顶元素与堆尾元素值互换,这样最小的元素就调整到了数组末尾;
  3. 然后再次调整至小根堆(排除掉最后一个元素进行调整),这样堆顶元素就是第二小元素以此重复。

3.4具体代码实现

堆排序的定义

//堆排序
    public void heapSort(){
        int end = useSize-1;
        while(end>0){
            swap(elem,0,end);
            shiftDown(0,end);//向下调整,具体代码看前面堆的模拟实现
            end--;
        }
    }
    //交换
    private void swap(int[] elem,int x,int y){
        int tmp = elem[x];
        elem[x] = elem[y];
        elem[y] = tmp;
    }

测试代码

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {27,15,191,18,28,34,65,49,25,37};
        TestHeap heap = new TestHeap();
        heap.initElem(arr);//初始化,具体细节可以看前面堆的模拟实现
        heap.createShortHeap();//构建小根堆,具体细节可以看前面堆的模拟实现
        heap.heapSort();
        System.out.println();
    }
}

结果
在这里插入图片描述

3.5执行过程图示

在这里插入图片描述
至此,java堆的详解就暂告一段落啦!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/703691.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

非局部attention之Non-local

论文&#xff1a;Non-local Neural Networks Github&#xff1a;GitHub - AlexHex7/Non-local_pytorch: Implementation of Non-local Block. GitHub - facebookresearch/video-nonlocal-net: Non-local Neural Networks for Video Classification cvpr2018 凯明作品 论文提出…

Echarts自定义legend图例

legend: {icon: rect, // 形状itemHeight: 4, // 高度itemWidth: 18, // 宽度itemGap: 15, //设置图例的间距...... }, 效果&#xff1a;

QT进度条代理的实现

目的 进度条也是一个基本的功能&#xff0c;这里进度条想在Tableview里面实现&#xff0c;让Tableview其中一个单元格显示进度条&#xff0c;因为就用了代理。 什么是代理&#xff0c;官方说&#xff1a; 代理(Delegate)就是在视图组件上为编辑数据提供编辑器&#xff0c;如在…

Kindle刷机安装微信读书。

1、越狱 2、安装软件 3、刷机 4、安装微信读书 刷机参考链接&#xff1a; https://www.xiaohongshu.com/explore/63d3f730000000001f027d0e?sourcequestion Kindle 通用越狱教程&#xff1a;适用固件版本 5.12.2.2~5.14.2 – 书伴 正式开始了。 1、越狱&#xff0c;参考…

[Flask] Flask会话

由于HTTP的无状态性&#xff0c;为了使某个域名下的所有网页能够共享某些数据&#xff0c;Cookie和Session应运而生 1.Cookie对象 HTTP是无状态(stateless)协议&#xff0c;一次请求响应结束后&#xff0c;服务器不会留下任何关于对方状态的信息 也就是说&#xff0c;尽管在…

2023年数字人产业发展趋势报告:好看的皮囊到有趣的灵魂

报告来源&#xff1a;腾讯研究院 报告看点&#xff1a;随着交互智能技术的加速发展&#xff0c;数字人正在从技术创新走向产业应用。 在腾讯2022数字生态大会腾讯云智能专场上&#xff0c;腾讯研究院、腾讯云智能、创业黑马经过产业研究、实地调研&#xff0c;共同提炼发布《数…

【Spring】Spring Bean 容器的生命周期是什么样的?

文章目录 一、什么是 Spring Bean 的生命周期&#xff1f;二、Spring 中的 bean 的作用域有哪些?三、Spring Bean 的生命周期四、Spring Bean 的生命周期流程图 一、什么是 Spring Bean 的生命周期&#xff1f; 对于普通的 Java 对象&#xff0c;当 new 的时候创建对象&#…

智头条|2023TUYA开发者大会精彩不断,IDC预测:2026年全球AI服务支出将增至646亿美元

1、行业动态 奢侈品巨头LVMH宣布将推出元宇宙项目“The Journey” 近日&#xff0c;奢侈品巨头LVMH宣布推出元宇宙项目“The Journey”&#xff0c;允许旗下用户进入一个拥有专属产品和体验的全新虚拟世界&#xff0c;目前官网显示该元宇宙项目下包括The Showroom、The Tech …

HotSpot 垃圾收集器

HotSpot 垃圾收集器 HotSpot 虚拟机提供了多种垃圾收集器&#xff0c;每种收集器都有各自的特点&#xff0c;虽然我们要对各个收集器进行比较&#xff0c;但并非为了挑选出一个最好的收集器。我们选择的只是对具体应用最合适的收集器。 新生代垃圾收集器 Serial 垃圾收集器&am…

Tomcat的部署及优化

目录 1.Tomcat概述 1.1 Tomcat的组件构成​编辑 1.2 Tomcat功能组件结构 1.3 Tomcat的请求过程 2. Tomcat服务的部署 2.1 搭建Tomcat运行环境 3.设置JDK环境变量​ 3.1测试java环境 4.安装启动Tomcat 1.Tomcat概述 Tomcat 是 基于Java 语言开发的Tomcat 属于轻量级应用服务…

数据结构 | 顺序线性表

一、数据结构定义 typedef int SqlTableType;typedef struct seqTable{int MAXNUM; // 最大元素个数 int curNum; // 元素的个数SqlTableType* element; // 连续空间的起始地址 }*SqlTable; 二、方法概览 SqlTable CreateSqlTable(int max); // 创建空顺序表 …

MySQL——mha高可用

MHA的概念 什么是 MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过…

【Android -- 面试】程序员面试必备软技能,值得收藏!

文章目录 一、准备技术型简历1. 准备简历模板2. 准备个⼈信息3. 准备专业技能4. 准备⼯作经历5. 准备项⽬经历 二、面试前的准备1. 利用脑图来梳理知识点2. 收到面试邀请后的准备 三、面试的注意事项1. ⾃我介绍2. 技术考察3. 向⾯试官提问4. 面试礼仪 四、如何通过 HR ⾯1. HR…

数据结构 | 顺序栈

一、数据结构定义 typedef int StackType; typedef struct seqStack{int MAXNUM; // 最大元素个数 int top; // 栈顶位置&#xff0c;初始化为0或-1StackType element[100]; // 元素的连续空间的起始地址 } *SeqStack; 二、方法概览 SeqStack CreateSqlStack…

【服务器数据恢复】ZFS文件系统下raid5数据恢复案例

服务器数据恢复环境&#xff1a; 某公司一台EMC某型号存储中有一组由12块硬盘组建的raid5磁盘阵列&#xff0c;其中有2块盘作为热备盘使用。 服务器故障&分析&#xff1a; raid5磁盘阵列中有2块磁盘离线&#xff0c;只有1块热备盘成功启用&#xff0c;另外一块热备盘未启用…

Elastic 栈中的 Kibana安装

Kibana 是一个开源分析和可视化平台&#xff0c;旨在与 Elasticsearch 协同工作。 你使用 Kibana 搜索&#xff0c;查看和与存储在 Elasticsearch 索引中的数据进行交互。 你可以轻松执行高级数据分析&#xff0c;并在各种图表&#xff0c;表格和地图中可视化你的数据。 官方链…

文件批量下载方法 文件批量下载工具

软件版本&#xff1a;Internet Download Manager 在当今数字化时代&#xff0c;我们日常使用电脑进行各种工作和娱乐活动。有时候&#xff0c;我们需要从互联网上下载大量文件&#xff0c;例如电影、音乐、文档等。在这种情况下&#xff0c;手动逐个下载文件将变得繁琐而耗时。…

梯度下降法求函数的解

题目 例如 y x^ 5 e^x3x−3&#xff0c;求解y 0的解 问题分析 首先要构造y 0的损失函数&#xff0c;让这个损失函数是凸的&#xff0c;也就是可以有最优解&#xff0c;并且是可到的&#xff0c;比较容易想到的是mse平方误差&#xff0c;我们要让y和0之间绝对误差最小。lo…

变压器铜铝材质分析仪技术参数

一、主要技术指标 1.输入特性 有源部分&#xff1a; 电压测量范围&#xff1a;0~10V 电流测量范围&#xff1a;0~10A 无源部分&#xff1a; 电压测量范围&#xff1a;0~750V 宽量限&#xff08;可以外接电压互感器&#xff09;。 电流测量范围&#xff1a;0~100A内部自动…

【计算机视觉 | 目标检测】arxiv 计算机视觉关于目标检测的学术速递(6月 28 日论文合集)

文章目录 一、检测相关(6篇)1.1 Detector-Free Structure from Motion1.2 Irregular Change Detection in Sparse Bi-Temporal Point Clouds using Learned Place Recognition Descriptors and Point-to-Voxel Comparison1.3 Transferability Metrics for Object Detection1.4 …