堆的解析,堆和优先级队列的实现,比较规则制定

华为OD机试高频题解析:测试用例执行计划的拓扑排序与优先级调度 在软件工程和算法面试中,任务调度是一个经典问题,其核心在于如何高效、合理地安排任务的执行顺序。这通常涉及对有依赖关系的任务进行排序,并在此基础上优化特定目标,如优先级或执行时间。拓扑排序是处理此类依赖约束的图论基础算法,它能将有向无环图中的节点排成一个线性序列,确保每个节点都在其所有前驱节点之后。结合优先级队列,可以在满足依赖的前提下,实现基于自定义规则(如高优先级优先、短作业优先)的贪心调度策略,从而在工程实践中平衡效率与业务需求。本文以华为OD机试中高频出现的“测试用例执行计划”问题为具体应用场景,深入 阅读详情

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
这篇博客要解析的是堆,堆是一种特殊的完全二叉树,堆的使用、底层构造都比较简单,基本上都不涉及递归,方法也是一看就懂,相比前面的二叉树,堆要简单很多🐵
这个专栏的数据结构是代码都是用Java来写的,JavaSE专栏现在已经全部更新完成,铁汁们复习基础知识时非常推荐使用,可以试一下💪💪💪
在这里插入图片描述
🎇个人主页:洋不写bug的博客
🎇所属专栏:数据结构专栏
🎇复习Java基础知识:Java学习之旅,从入门到进阶
🎇铁汁们对于数据结构基础的各种核心知识(不太常用的也有😆),都可以在上面的数据结构专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~

1,优先级队列的简介

正常的队列就是“先进先出”,在优先级队列中,出队列的顺序和入队列的顺序是不一样的,把队列中的每个元素,都约定了优先级,优先级高的就先出队列

就比如医院,正常看病就是要挂号、排队,但是急诊送过来的病人情况比较紧急,就需要赶快往前插队



优先级队列的的集合类为PriorityQueue,出队列的方法就是poll(),入队列的方法就是offer(),如果是数字类型,那就是值越小,优先级越高

import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        priorityQueue.offer(2);
        priorityQueue.offer(5);
        priorityQueue.offer(3);
        priorityQueue.offer(1);
        priorityQueue.offer(4);
        while(!priorityQueue.isEmpty()){
            System.out.println(priorityQueue.poll());
        }
    }
}

在这里插入图片描述

如果优先级队列中的元素是String类型,就是按照字典序来排优先级的,这个了解下即可

2,堆的简介

那优先级队列是如何实现的呢,可能有的铁汁会想到:把元素存储到顺序表或者链表中,每次出队列时遍历一遍,把优先级最高的元素给删除掉,但是这样的时间复杂度就是O(N),还是比较高的

优先级队列最经典、最高效的实现方式,就是用堆这个数据结构,堆就是一个完全二叉树

对于完全二叉树来说,可以不使用“孩子表示法”,通过数组的方式来表示这棵树,对这个树进行层序遍历,把遍历的结果依次添加到数组中,如果遇到空结点,就把空结点(null)也添加到数组中,如下图
假设父结点的下标是parent,那左子结点的下标就是2 * parent + 1,右子结点的下标就是2 * parent + 2(实际写代码的时候是不会把null也加入数组中的,)

在这里插入图片描述

普通二叉树很少使用这种数组存储法,因为普通二叉树中可能空结点比较多,用这种方法的空间利用率是很低的(完全二叉树的空结点的个数就比较少)



堆有一个重要的特征:任何一个结点的值都是大于(小于)两个子结点的值的(左右子结点谁大谁小无所谓)

  • 如果是大于,这个堆就是大根堆/大堆,在整棵树中,根结点的值是最大的
  • 如果是小于,这个堆就是小根堆/小堆,在整棵树中,根结点的值是最小的

下图就是一个大堆

在这里插入图片描述



对于堆/优先级队列,如果里面存放的是数字/String类型,比较规则就比较明确,如果是对象,那就要约定好按照什么来比较,例如下面的boy类,可以按照身高、颜值、资产等来比较,确定使用比较的属性,就称为key,也就是关键字

class Boy {
    String name;
    int id;
    int balance;
    int face;
    int height;
    int weight;
    int rank;
}

3,堆的实现

接下来分析下一个乱序的二叉树是如何调整为堆的
先考虑一个简单的情况,有一个数组,除了[0]元素之外,其他结点都满足小堆的要求,这个数组为[27,15,19,18,28,34,65,49,25,37],树结构如下图,如何调整这个二叉树,让它变成一个小堆

在这里插入图片描述


针对这种情况,要做的就是“向下调整”

  1. 先确定当前结点要和左子结点比较还是右子结点比较:
    a)如果只有左子树,要比较的就是左子结点
    b)如果右子树和左子树都有,要比较的就是左子结点和右子结点(完全二叉树是不可能只有右子树,没有左子树的🐵)
  2. 拿着左右子结点中更小的那个(只有左子树就直接拿左子结点),跟根结点进行比较,如果发现根结点的值大于子结点的,就交换这两个结点的位置

按照这个思路,就是27和15的位置交换一下

在这里插入图片描述


这样交换完成后,根结点就满足要求了,但是27、18、28这三个结点构成的子树就不符合要求了,还是按照前面的规则进行调整,交换下27和18的位置

在这里插入图片描述


对于27、49、25构成的子树,还是按照这个规则比较,比较后交换27和25的位置,这个二叉树就调整成了一个小堆

在这里插入图片描述

在这里插入图片描述



这个向下调整使用时有个前提,那就是除了根结点之外,其他的结点都已经符合堆的要求了

如果给一个乱序的完全二叉树(数组),我们是不是就可以找到数组中的最后一个“非叶子结点”A,那A的左结点一定是叶子结点,右子结点可能为空,不为空的话也是叶子结点,对A进行向下调整后,A构成的这个子树就满足堆的要求了

就这样从后往前的进行调整,经过多轮调整后,除了根结点之外,其他的结点就都已经符合堆的要求,对根结点进行向下调整,二叉树就变成堆了



接下来模拟下这个过程,给一个乱序的完全二叉树[9,5,2,7,3,6,8,10],通过调整让它变成小堆,首先找到数组中的最后一个非叶子结点,就是7,在7为根结点的这个子树中,是不需要调整的

在这里插入图片描述


在数组中从后往前看,下个叶子结点是2,这个子树也不需要调整

在这里插入图片描述



下个叶子结点是5,这个子树需要调整下,将5的位置和3的位置交换下,交换后还要判断换下来的这个结点是否需要再次交换,这里5的左子结点和右子结点都是null,就不用考虑了

在这里插入图片描述



最后到了9这个结点,跟左右结点中较小的进行交换,跟2进行交换;交换后,9大于6,再和6这个结点进行交换,至此小堆就构建完成了

在这里插入图片描述



构建堆的基本思路:

  1. 从数组中的最后一个非叶子结点出发,从后往前遍历,针对每个结点,进行向下调整
  2. 向下调整:就是找出子树中的最小值,和根结点来比较,如果根结点的值更大,就和刚才的子树结点交换,持续往下进行到根结点比子树小,或者根结点没有子树的时候



创建个Heap类,把代码都写到Heap类中,shiftDown方法逻辑如下:

  1. 向下调整的方法shiftDown,方法中传入数组和要调整的父结点的下标,用 child = 2 * parent + 1来计算左子结点的下标,右子结点的下标是左子结点的下标加1,当然,右子树可能为空,或者左右子树都为空,需要写代码来判断一下
  2. 比较出左右子树的最小值后,和父结点的值进行比较,如果父结点的更大,就交换位置
  3. 父结点和子结点交换位置后,更改parent的值,parent = child,再更新child的值,child = 2 * parent + 1,继续进行判断(因为交换后父结点换到了子结点的位置,下面的树可能不符合要求,还是需要调整),重复前面的步骤,进行到parent左右子树都为空,就调整完成了
  4. parent的左右子树都为空,就是child >= arr.length,代码就写在while循环中,循环条件就是while(child < arr.length),出循环就表示向下调整过程完成了
    public static void shiftDown(int[] arr,int index){
        int parent = index;
        int child = 2 * parent + 1;
        while(child < arr.length){
            if(child + 1 < arr.length && arr[child] > arr[child + 1]){
                child = child + 1;
            }
            if(arr[parent] <= arr[child]){
                break;
            }else{
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }
        }
    }




接下来写一下creatHeap方法,方法逻辑如下:

  1. 找到数组中的最后一个非叶子结点,然后从这个结点开始,往前遍历数组,遍历时每个结点都调用一下shiftDown方法,进行向下调整,就能得到堆
  2. 重点就是计算出最后一个非叶子结点lastParent,铁汁们细品下,最后一个非叶子结点的值是不是就是最后一个叶子结点的父结点(如下图),最后一个叶子结点的下标lastLeaf就是arr.length - 1,lastParnt = 2 * lastLeaf + 1,就能推出lastLeaf = (lastParent - 1) / 2

在这里插入图片描述


有的铁汁可能会想:如果最后一个结点lastLeaf是右子结点,那lastParent = 2 * lastLeaf + 2,lastLeaf = (lastParent - 2) / 2,那是不是还要写代码判断lastLeaf是左子结点,还是右子结点?

最后一个结点是右子结点,同样可以直接用lastLeaf = (lastParent - 1) / 2来计算,如果最后一个结点是右子结点,那这个结点一定是双数,双数减去1就是单数,单数除以2,结果带有.5,但相除的是两个int类型,小数部分会被约掉,因此结果还是正确的🐵



写代码时不建议写成lastParent = (arr.length - 2)/2,这样代码的可读性就会比较差

    public static void createHeap(int[] arr){
        int lastLeaf = arr.length - 1;
        int lastParent = (lastLeaf - 1) / 2;
        for(int i = lastParent;i >= 0;i--){
            shiftDown(arr,i);
        }
    }



在main方法中创建一个乱序的数组,将数组传入createHeap方法,接着打印数组,看下效果,整体代码如下:

import java.util.Arrays;

public class Heap {
    public static void shiftDown(int[] arr,int index){
        int parent = index;
        int child = 2 * parent + 1;
        while(child < arr.length){
            if(child + 1 < arr.length && arr[child] > arr[child + 1]){
                child = child + 1;
            }
            if(arr[parent] <= arr[child]){
                break;
            }else{
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }
        }
    }

    public static void createHeap(int[] arr){
        int lastLeaf = arr.length - 1;
        int lastParent = (lastLeaf - 1) / 2;
        for(int i = lastParent;i >= 0;i--){
            shiftDown(arr,i);
        }
    }

    public static void main(String[] args) {
        int[] array = {1,3,2,6,10,9,8,7,5,4};
        createHeap(array);
        System.out.println(Arrays.toString(array));
    }
}

在这里插入图片描述


根据调整后的数组还原下二叉树,发现确实是堆,如下图

在这里插入图片描述


对于一个乱序的二叉树(数组),要将其转化为堆,时间复杂度是多少呢?

先分析下shiftDown方法的时间复杂度,循环变量child,每次都是* 2,直到child >= arr.length,结束循环,所以shiftDown方法的时间复杂度就是O(logN)

在createHeap方法中,循环的调用shiftDown方法,铁汁可能会觉得时间复杂度就是O(NlogN),但是在这个for循环中,shiftDown中while的循环次数跟传入的是有很大关系的,当i = lastParent,shiftDown方法中的while循环最多执行一次,当i比较小的时候,shiftDown中的while循环可能要执行多次,但最多也只是执行logN次

 for(int i = lastParent;i >= 0;i--){
            shiftDown(arr,i);
        }



只有i最大时shiftDown的while循环才可能执行logN次,铁汁们细品下,整体的时间复杂度应该是比O(NlogN)要小的,那具体如何计算呢,数学推导如下,这个算是高中数学压轴题的难度(有兴趣的铁汁可以推导一下,如何推导面试是一定不会问到的😄),推导出时间复杂度就是O(N)

在这里插入图片描述

4,优先级队列的实现

优先级队列的核心操作就是入队列、出队列、返回堆顶元素,返回堆顶元素很简单,直接返回arr[0]即可,重点是入队列、出队列时二叉树如何调整

先分析入队列,入队列就是把一个元素添加到数组末尾,作为二叉树的最后一个叶子结点,入队列如何调整二叉树呢?有的铁汁可能会想:可以直接调用堆的createHeap方法
但是这里铁汁们细品下,就这一个子树不符合堆的性质,需要调整,其他子树都是符合的,但是createHeap方法会判断依次判断每个非叶子结点跟其左右子树是否符合要求,效率就比较低




入队列新的元素时,就可以采用“向上调整“,假设是小堆,步骤如下:

  1. 从最后一个元素的位置出发,找到该元素的父结点,父结点的下标就是(child - 1)/2,比较父结点和子结点的大小,如果父结点更小,说明新加入的元素没有破坏堆,就不需要进行调整了,如果父结点大,就交换两个元素
  2. 如果在第1步中交换元素了,更新child的值为parent,接着再用(child - 1)/2重新计算parent,继续第1步的比较,当元素到达根结点或者元素的值大于其父结点的值时,向上调整就完成了

例如这样一个小堆,加入10这个结点,加到叶子结点,跟父结点比较大小,交换10和28的位置,

在这里插入图片描述


交换10和28的位置后,再次进行判断,发现子结点的值还是小于父结点,还需要交换18和10的位置

在这里插入图片描述


交换18和10的位置后,再次判断,父结点的值还是大于子结点,还需要交换10和15的位置

在这里插入图片描述


交换10和15的位置后,就得到了小堆

在这里插入图片描述

代码如下,这个代码还是写到Heap类中

    public static void shiftUp(int[] arr,int index){
        int child = index;
        int parent = (child - 1) / 2;
        while(child > 0){
            if(arr[parent] > arr[child]){
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            }else{
                break;
            }
        }
    }

接着我们分析下出队列,出队列就是把arr[0]元素删除,如果是一个有序的顺序表的话,直接把后面元素往前移动即可,但是“堆”就不能这么搞了

这时候就可以转化下思路,删除队首元素不好删除,删除队尾元素是很简单的,直接删掉即可,那我们就可以把根结点和最后一个结点进行交换,接着删除最后一个结点的位置,这时候树中只有根结点是不符合堆的性质的,从根结点出发,进行以此向下调整即可



分析清楚后,就创建一个MyPriorityQueue类,模拟实现下我们自己的优先级队列,代码逻辑如下:

  1. 在队列中固定长度的数组,通过size这个成员变量来记录数组的长度
  2. 在offer方法中加入判断,判断size是否等于arr.length,确定数组是否满了,满了的话可以进行自动扩容,这里我么就直接return
  3. offer方法就是调用Heap类中的shiftUp方法,poll方法就是调用Heap类中的shiftDown方法,注意,前面写的shiftDown是一直进行到child < arr.length,现在再传入一个size参数,一直进行到child < size
  4. 堆中的createHeap方法,创建优先级队列也是用不到的,就可以删除掉,Heap类和Mypriority类代码如下:
package package12;

public class Heap {
    public static void shiftDown(int[] arr,int size,int index){
        int parent = index;
        int child = 2 * parent + 1;
        while(child < size){
            if(child + 1 < size&& arr[child] > arr[child + 1]){
                child = child + 1;
            }
            if(arr[parent] <= arr[child]){
                break;
            }else{
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                parent = child;
                child = 2 * parent + 1;
            }
        }
    }


    public static void shiftUp(int[] arr,int index){
        int child = index;
        int parent = (child - 1) / 2;
        while(child > 0){
            if(arr[parent] > arr[child]){
                int tmp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = tmp;
                child = parent;
                parent = (child - 1) / 2;
            }else{
                break;
            }
        }
    }
}

package package12;

public class MyPriorityQueue {
    private int[] array;
    private int size = 0;

    public MyPriorityQueue() {
        array = new int[1000];
    }

    public void offer(int value){
        if(size == array.length){
            return;
        }
        array[size] = value;
        Heap.shiftUp(array,size);
        size++;
    }

    public Integer poll(){
        if(size == 0){
            return null;
        }
        int tmp = array[0];
        array[0] = array[size - 1];
        array[size - 1] = tmp;
        size--;
        Heap.shiftDown(array,size,0);
        return tmp;
    }

    public Integer peek(){
        if(size == 0){
            return null;
        }
        return array[0];
    }
}



接着在main方法中测试下MyPriorityQueue

package package12;

import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) {
        MyPriorityQueue myPriorityQueue = new MyPriorityQueue();
        myPriorityQueue.offer(4);
        myPriorityQueue.offer(3);
        myPriorityQueue.offer(1);
        myPriorityQueue.offer(2);
        System.out.println(myPriorityQueue.peek());
        System.out.println(myPriorityQueue.poll());
        System.out.println(myPriorityQueue.poll());
        System.out.println(myPriorityQueue.poll());
        System.out.println(myPriorityQueue.poll());
    }
}

在这里插入图片描述

5,比较规则的制定

如果优先级队列中的元素是对象,也可以把对象的某个属性作为比较标准进行排序,这个属性也被称为关键字

想要给优先级队列指定比较规则,就需要先指定比较器,也就是实现Comparable和Comparator接口(这两个接口的使用在JavaSE专栏中详细解析过,想要复习的铁汁可以看下🐵)

  • 如果队列中插入的是类的对象,就可以直接让这个类实现Comparable接口;
  • -如果队列中插入的是int或者String这种,就可以实现Comparator来完成上述比较规则



例如优先级队列中插入Integer元素时,默认是小堆,如果想要改一下比较规则,值大的元素靠前,就需要创建一个比较器类IntComparator,实现Comparator接口

重写compare方法,返回o2 - o1,compare方法是这样,如果返回值小于0,就说明o1排在o2前面,返回o2 - o1,就是o1大于o2的时候,o1排在前面(这部分内容铁汁们不想记也可以,写完在main方法中测试下,看是大堆还是小堆🐵)

package package12;

import java.util.Comparator;

public class IntComparator implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
}



在main方法中测试一下,确实就变成了大堆

package package12;

import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntComparator());
        priorityQueue.offer(1);
        priorityQueue.offer(3);
        priorityQueue.offer(2);
        priorityQueue.offer(4);
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
    }
}



使用Comparator时,还可以不创建比较器类,直接使用匿名内部类(在JavaSE专栏最后一篇博客的lambda表达式部分有解析,感兴趣的铁汁可以看下)

package package12;

import java.util.Comparator;
import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        priorityQueue.offer(1);
        priorityQueue.offer(3);
        priorityQueue.offer(2);
        priorityQueue.offer(4);
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
    }
}



创建个Student类,在队列中加入一些Student对象,如果不指定比较规则,那出队列时就会报错

package package12;

public class Student {
    public int id;
    public String name;
    public int score;

    public Student(int id, String name, int score) {
        this.id = id;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }
}

package package12;

import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) {
       PriorityQueue<Student> priorityQueue = new PriorityQueue<>();
       priorityQueue.offer(new Student(1,"helly",99));
       priorityQueue.offer(new Student(2,"burt",77));
       priorityQueue.offer(new Student(3,"tom",88));
       priorityQueue.offer(new Student(4,"david",100));
       System.out.println(priorityQueue.poll());
       System.out.println(priorityQueue.poll());
       System.out.println(priorityQueue.poll());
       System.out.println(priorityQueue.poll());
    }
}

在这里插入图片描述

异常原因是“类转化异常”,对于优先级队列来说,如果指定了Comparator,就按照Comparator的规则来比较,没有指定Comparator,就尝试把元素按照Comparable的规则来比较,但是这里都没有指定,就报错了



  1. 让Student类实现Comparable接口,重写compareTo方法,compareTo方法只传入一个参数,compareTo方法就是比较this和参数o,也就是比较自己和别人
  2. compareTo方法就是返回负数的话,this排在前面,小堆写法就是this.score - o .score(也没必要背,用的时候试一下就行了)
package package12;

public class Student implements Comparable<Student>{
    public int id;
    public String name;
    public int score;

    public Student(int id, String name, int score) {
        this.id = id;
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.score - o.score;
    }
}



接着在main方法中验证下,就是按照分数来排序的

在这里插入图片描述


那什么时候用Comparator,什么时候用Comparable呢?

使用Comparable:

  1. 要比较的类是自己写的,随时可以修改这个类
  2. 针对这个类的比较规则是唯一的

使用Comparator:

  1. 不要求这个类是自己写的,可能没办法直接修改这个类
  2. 比较规则可能有多种,就可以创建多个比较器类,制定不同的比较规则

6,最小k个数

在这里插入图片描述
力扣链接

这个题目可以使用优先级队列,先把数组的所有元素都入队列,再出队列k次即可

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] smallestK = new int[k];
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for(int num : arr){
            priorityQueue.offer(num);
        }
        for(int i = 0;i < k;i++){
            smallestK[i] = priorityQueue.poll();
        }
        return smallestK;
    }
}




这个题目还可以在空间上进行优化,普通解法是创建了一个长度为N的小堆,就可以优化为创建长度为k的大堆,具体步骤如下:

  1. 遍历数组,把前k个元素,加到堆中,因为是大堆,堆顶元素是堆中的最大值
  2. 从第k + 1个元素开始,比较下该元素与堆顶元素的大小,如果该元素小于堆顶元素,就把堆顶元素删除掉,该元素插入到堆中;如果该元素大于堆顶元素,那就不用管,继续往后遍历数组即可
  3. 遍历完数组后,这个大堆中存储的就是最小的k个元素
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] smallestK = new int[k];
        if(arr.length == 0 || k == 0){
            return smallestK;
        }
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>(){
            public int compare(Integer o1,Integer o2){
                return o2 - o1;
            }
        });
        for(int i = 0;i < k;i++){
            priorityQueue.offer(arr[i]);
        }
        for(int i = k;i < arr.length;i++){
            if(arr[i] < priorityQueue.peek()){
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }

        for(int i = 0;i < k;i++){
            smallestK[i] = priorityQueue.poll();
        }
        return smallestK;
    }
}



可能优化写法在力扣上提交后,消耗的内存空间更多了,力扣中内存空间受到评测系统采样、JVM 对象开销、服务器波动,仅供参考,如果数据很多,一定是优化写法消耗的空间更少

而且在面试中面试官问出这道题,肯定不仅仅是为了考察我们是否知道优先级的基础概念的,想考察的是我们如何去优化

结语

  • 堆的使用和底层实现都还是比较简单的,代码逻辑都不算复杂,重点就是制定比较规则的时候对于Comparator接口或Comparbale接口的实现
  • 堆在插入元素后,重新排序的时间复杂度还是非常低的,为O(logN)




以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

在这里插入图片描述

从规则怪谈到任务系统:如何将模糊叙事转化为可执行指令 在软件工程和系统设计领域,状态机与事件驱动架构是处理复杂逻辑的核心技术。其原理在于通过定义有限状态和触发事件,将模糊的业务需求转化为清晰、可执行的操作序列,从而实现流程的自动化与可控性。这种技术的价值在于能够将依赖人类直觉和经验的领域,如叙事规则或业务需求,解构成机器可识别、可验证的结构化任务。在游戏开发、自动化脚本设计、复杂业务流程SOP制定乃至AI智能体任务规划等应用场景中,该方法论尤为关键。本文以“后室规则怪谈”项目为例,深入剖析了如何通过实体属性化、规则条件化和任务原子化,将充满不确定性的恐怖叙事, 阅读详情

相关推荐

C++优先级线程池实现:从基础队列到智能任务调度

在并发编程中,线程池是管理多线程执行、提升应用性能的核心组件。其基本原理是通过复用线程来避免频繁创建销毁的开销,从而构建高响应性系统。传统线程池通常采用先进先出队列,但在实际工程中,不同任务往往有紧急程度差异,这就需要引入优先级调度机制。通过结合互斥锁、条件变量和优先级队列数据结构,可以实现线程安全的智能任务调度,确保高优先级任务优先执行。这种技术在高性能服务器、实时系统等场景中价值显著,能有效处理如用户请求优先于后台任务等需求。本文以C++为例,深入探讨了优先级线程池的设计与实现,涵盖了线程安全、任务封装

weixin_30536861的博客 347

C++:std::greater()、std::less()、自定义比较函数的规则

文章目录一、要求:strict weak ordering二、例子三、bool返回值研究1.规则2.并不是想当然的位置交换3.代价Reference 一、要求:strict weak ordering 需要满足三种特性要求,否则使用中会报错: 反自反性:false true的互斥性:true则false(但不要求false则怎么样) 传递性:true+true则true 二、例子 #inc...

sandalphon4869的博客 4万+

基于Scrapy的农业数据爬虫完整设计与实现

本文还有配套的精品资源,点击获取 简介:本项目利用Python的Scrapy框架设计并实现了农业数据爬虫系统,用于自动化获取和处理农业相关网络数据。通过爬虫系统可以收集农业资讯、价格信息、气象数据等,旨在提升农业生产效率和质量。项目包括需求分析、Scrapy项目架构设计、实现步骤、挑战解决方法以及对农业科学研究、市场预测、政策制定等领域的支持。 1. Scrapy...

weixin_35755188的博客 1212

Trigger.dev任务优先级队列监控:确保调度系统正常运行的终极指南

在现代应用开发中,后台任务调度系统的稳定性和效率至关重要。✨Trigger.dev作为开源的TypeScript后台任务框架,提供了强大的任务优先级队列监控功能,让您能够实时掌握调度系统的运行状况。 ## 为什么需要任务优先级队列监控? 当您的应用处理大量后台任务时,任务队列可能变得复杂而难以管理。没有适当的监控,您可能会遇到: - 高优先级任务被低优先级任务阻塞 - 队列积压导致系统响应延

gitblog_00318的博客 862

Dataworks实现接口调用

在本地linux虚拟机(不用在意linux版本),安装Anaconda后配置对应的虚拟环境,将虚拟环境的python包整体打包(路径一般为:~/anaconda/envs/env_name/lib/pyyhon3.7/site-packages):上传资源,将第三方包上传至dataworks(注意:所有上传文件均要打包上传,直接上传文件会乱码导致无法使用):PyODPS,其中PyODSP2的python版本为2.7,PyODSP3的python版本为3.7。:在PyODPS中引用资源,然后开发接口。

hutc_Alan的博客 1680

通用策略04丨ORB魔改框架+自适应动量过滤模板

1、该策略第一次尝试使用低通滤波器进行行情择时过滤,同样该方法也可以用于主进场逻辑。2、出场逻辑适用于趋势、隔日波段、高低开等行情结构,对于日线级别长度K线一定积极作用,因此大家可以根据该逻辑进行反向品种筛选。3、在资金管理方面,我注意到一个问题,以往都是按照复权价格计算,但是资金却没有复权,所以这里实际上这里应该将价格除权计算,如下图所示:由于各平台差异,回测绩效以TBQ版本为准!!!本策略仅作学习、交流使用,实盘交易盈亏投资者个人负责!!!

松鼠宽客的博客 1176

编程中复数比较的数学原理与工程实践:从模长、字典序到自定义规则

在计算机科学和工程计算中,数据排序与比较是基础且关键的操作。对于实数,大小关系是直观的,但扩展到复数域时,情况变得复杂。从数学原理上讲,复数集是一个无序域,无法像实数那样定义兼容加法和乘法的全序关系,这直接影响了编程语言的设计。因此,在编程实践中,复数比较需要根据具体场景明确定义,常见的比较维度包括基于模长(绝对值)的比较、基于实部或虚部的分量比较、以及字典序比较等。这些方法在信号处理、科学计算和图形学等领域有重要应用价值。例如,在信号处理中,基于模长的比较可用于分析频率分量的强度;在需要全序的数据结构中,

weixin_33827731的博客 390

深度学习之基于Pytorch手指静脉识别系统(

一、项目背景与目标手指静脉识别技术是一种新兴的生物特征识别技术,它依据人类手指中流动的血液可吸收特定波长的光线这一科学原理,利用手指内的静脉分布图像来进行身份识别。与传统的生物识别技术(如指纹识别)相比,手指静脉识别技术具有不易伪造、识别精度高、非接触性等特点,因此在安全性要求较高的场景(如金融、医疗、公安等)具有广阔的应用前景。本项目旨在利用PyTorch深度学习框架,构建一套高效、准确的手指静脉识别系统。

Q1744828575的博客 830

pfSense防火墙规则架构设计与安全策略优化指南

在当今复杂的网络环境中,企业面临着日益严峻的网络安全挑战。pfSense作为一款功能强大的开源防火墙和路由器解决方案,其防火墙规则配置是构建网络安全防线的核心环节。本文针对中级用户和技术决策者,深入探讨pfSense防火墙规则的架构设计、安全策略优化和性能调优,帮助您构建更加安全、高效的企业级网络防护体系。 ## 技术挑战与业务痛点分析 企业网络环境面临着多层次的网络安全威胁,传统防火墙规则配

gitblog_01165的博客 885

Android开发Intent详解

本文详解Android开发Intent。

qq_30725967的博客 2413

微信小程序-如何根据缩放屏幕大小自适应icon图标更改marker

微信小程序如何根据用户的缩放屏幕来改变marker图标icon的大小,实现icon自适应屏幕缩放的效果

Bartender_VA11的博客 3980

1-智能QOS方案介绍

1. QOS是什么 在传统的 IP 网络中,所有的报文都被无区别的同等对待。即每个网络设备对所有的报文均采用 FIFO(First In First Out,先进先出)的策略进行处理,依照报文到达时间的先后次序分配所需的资源,尽最大的努力(Best-Effort)将报文送到目的地。 这种方式下,对报文传送的可靠性、传递延迟、丢包率等性能不能提供任何保证,仅适用于对服务性能不敏感的普通业务,如WWW...

Creator_Ly的博客 5074

Kafka 系列 —(17)Kafka 流式处理:Kafka Streams架构、使用场景、框架选择

本文介绍了KafkaStreams的架构特点、应用场景及与其他流处理框架的比较。KafkaStreams作为嵌入式分布式流处理库,其核心架构包含流处理器、任务、拓扑和本地状态存储,具备自动容错和伸缩能力。适用于实时监控、ETL、聚合、Join、物化视图等低延迟场景。与Flink、Spark相比,KafkaStreams适合简单架构和微服务,而Flink适合大规模处理,Spark更适合批处理。选择框架需根据具体场景需求,如微服务推荐KafkaStreams,大规模处理推荐Flink。

moton2017的博客 1631

Path of Building PoE2:流放之路2离线构建引擎架构深度解析

Path of Building PoE2(PoB2)作为流放之路2社区最强大的离线构建规划工具,其核心价值在于构建了一个完整的角色模拟计算引擎。不同于简单的数值计算器,PoB2通过模块化的Lua架构实现了游戏机制的精确模拟,为玩家提供了从天赋规划、装备优化到技能组合的全面构建解决方案。本文将深入解析PoB2的技术架构、核心模块设计以及实际应用中的最佳实践。 ## 技术架构与核心设计理念 Po

gitblog_00740的博客 726

simulink的电动汽车制动系统热管理仿真

本文系统研究了电动汽车制动系统热管理仿真技术。通过建立机电液多物理场耦合模型,分析了不同工况下的动态热平衡特性。提出基于MPC的动态调节和NSGA-II多目标优化算法,使电池温度波动降低60%。创新性采用石墨烯相变材料与微通道液冷复合技术,冷却系统能耗减少40%。研究构建了完整的工程验证平台,涵盖200kW电制动测试台和高精度热成像系统。展望指出,未来将结合Transformer预测和强化学习算法,进一步优化热管理策略。该研究为电动汽车制动系统热管理提供了从理论建模到工程实施的完整解决方案。

xiaoheshang_123的博客 121

第3版•系统集成项目管理工程师(中级) │ 告别迷茫!案例分析高分攻略

🥏第3版•系统集成项目管理工程师(中级) │ 告别迷茫!案例分析高分攻略

✨ 欢迎来到【Seal ^_^ 的优快云博客】!✨ 3867

栈和队列问题

本题是一道经典的使用栈的问题 分析括号不匹配的情况有三种: 1、左括号多余了 ()( 2、右括号多余()) 3、括号不匹配(} 解题方法:遍历字符串,当遇到左括号时,把相应的右括号压入栈,当遇到右括号时,查看是否与栈顶元素匹配,不匹配返回false(上面第三种情况);当遇到右括号但是栈此时为空,返回false(上面第二种情况); 遍历完字符串若栈不为空,返回false(上面第一种情况) public boolean isValid(String s) { Stack<Characte.

weixin_44311466的博客 248

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

Sourcetrail索引系统深度调试与性能优化技术解析

在源代码探索工具Sourcetrail的实际应用中,索引系统作为核心引擎,其稳定性和性能直接影响着代码分析的质量与效率。本文将从技术架构层面深入剖析索引错误的诊断方法,并提供系统化的优化策略。 ## 索引系统技术架构深度剖析 Sourcetrail的索引引擎采用分层架构设计,从文件解析到符号关系构建形成完整的处理链路。索引失败通常源于架构中的关键组件异常,主要包括: **语法解析层异常**:

gitblog_00861的博客 766

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

并发队列之PriorityBlockingQueue

这就是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素(这里规则可以自己制定),内部是使用平衡二叉树实现的,遍历不保证有序; 其实也比较容易,就是基于数组实现的一个平衡二叉树,不了解平衡二叉树的可以先了解一下,别想的太难,原理跟链表差不多,只不过链表中指向下一个节点的只有一个,而平衡二叉树中有两个,一个左,一个右,还有左边的节点的值小于当前节点的值,右边节点的值大于当前节点的值;看看...

ting2909的博客 284

如何构建A股市场深度行情系统:从数据采集到FPGA加速的完整方案深度解析

在金融市场中,实时准确的订单簿数据是理解市场动态和制定交易策略的基础。AXOrderBook项目通过结合Python模型与FPGA硬件加速技术,为A股市场提供了一套从逐笔行情数据处理到千档快照生成的完整解决方案。本文将深入探讨该系统的核心价值、技术实现原理、实际应用场景以及部署实践指南,帮助读者全面理解如何利用该工具构建高性能的市场深度分析平台。 ## 核心价值:解决金融数据处理的关键挑战 金

gitblog_00803的博客 386

3大创新方案解决UP主视频投稿频率限制难题

你是否遇到过这样的场景:深夜时分,当你用Bilive工具批量处理完十几个直播切片视频,满怀期待地点击"全部投稿",却被系统无情地弹出"137022错误:投稿过于频繁"的提示?这种平台保护机制就像突然亮起的红灯,让你的投稿计划戛然而止。视频投稿频率限制已成为影响UP主创作效率的关键瓶颈,本文将从限制原理到创新解决方案,为你提供一套完整的突破策略。 ## 诊断视频投稿频率限制类型 视频投稿频率限制

gitblog_00057的博客 2137

华为OD机试C卷:测试用例执行计划的多关键字排序C++实现

在软件开发和算法设计中,多关键字排序是一种基础且核心的技术,它通过定义元素的比较规则来决定其排列顺序。其原理在于利用自定义比较器,为排序算法(如C++中的std::sort)提供明确的优先级判断逻辑,从而实现按多个属性(如优先级、编号)进行有序排列。这项技术的价值在于能够高效解决资源调度、任务编排等实际工程问题,将复杂规则转化为清晰的排序逻辑。在应用场景上,它广泛出现在测试用例执行计划、学生成绩排名、服务器负载分配等需要综合多个维度进行决策的场合。本文以华为OD机试中的“测试用例执行计划”题目为例,深入剖析

weixin_34391445的博客 422

线程池核心原理与实战解析

本文系统介绍了Java线程池的核心知识体系。主要内容包括:1.线程池的接口层级与实现类(Executor、ThreadPoolExecutor等);2.ThreadPoolExecutor的核心配置参数(核心线程数、工作队列等);3.线程池的任务处理流程(核心线程→工作队列→非核心线程→拒绝策略);4.四种拒绝策略及其特点;5.线程池的创建、任务提交与关闭方法;6.线程池的五种状态转换;7.核心源码分析(execute()和addWorker()方法)。通过深入解析线程池的工作原理和实现机制,为开发者合理使

2402_85030292的博客 680

【多线程-从零开始-拾】Timer-定时器

关于定时器你想知道的一切细节

Yeeear的博客 1502

C++ STL priority_queue容器适配器

什么是容器适配器? 再举一个例子,假设一个代码模块 A,它的构成如下所示: class A{ public: void f1(){} void f2(){} void f3(){} void f4(){} }; 现在我们需要设计一个模板 B,但发现,其实只需要组合一下模块 A 中的 f1()、f2()、f3(),就可以实现模板 B 需要的功能。其中 f1() 单独使用即可,而 f2() 和 f3() 需要组合起来使用,如下所示: class B{ privat..

422

大麦网自动抢票终极指南:告别手动抢票的烦恼

还在为抢不到心仪的演唱会门票而烦恼吗?每次热门演出开票瞬间秒光,手动操作根本来不及?今天我要为你介绍一个开源神器——Autoticket大麦网自动抢票工具,这是一个基于Python的自动化解决方案,能够让你告别抢票焦虑,轻松获得热门演出门票。本指南将带你全面了解这个工具的功能、配置方法和使用技巧,让你在下次抢票中占据先机。 ## 为什么你需要一个自动抢票工具? 想象一下这个场景:周杰伦演唱会门

gitblog_00755的博客 155
上一篇: 二叉树(四)经典算法题目解析,公共祖先,二叉树构造,非递归遍历
下一篇: 排序(一)基础排序,插入|希尔|冒泡|直接选择排序详解
洋不写bug
博客等级 码龄2年 87粉丝 102原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值