草庐IT

浅浅理解一下堆

yan扬 2023-11-26 原文

目录

一、堆的定义及本质

二、堆的核心操作

1、向下调整

2、堆的创建

3、向上调整

 三、堆的比较器传入及堆中简单函数的实现

四、堆的应用

1、用于OS调度进程

2、topk问题

 3、堆排序


一、堆的定义及本质

堆在Java中是以优先级队列来表现的(PrityQueue),不传入外部比较器则以小堆来实现(取出最小值)

前提:优先级队列中的元素具备比较能力(1.元素类型本身是可以比较的 2.通过构造方法传入一个外部比较器)

堆的作用:常用来在频繁变动的数据集中找出最值

堆的本质:逻辑上是完全二叉树,本质上是数组

堆的定义:

堆总是满足下列性质:

1.堆中某个节点的值总是不大于或不小于其父节点的值;

2、堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。堆是非线性数据结构,相当于一维数组,有两个直接后继。

 对于一个结点 2*index+1 是他的左节点 2*index+2是右节点   (index-1)/ 2 可以求得父节点

二、堆的核心操作

1、向下调整

当我们从堆顶取走一个最值时,这时堆的结构已经发生变化,我们往往用堆的最后一个结点来代替堆的根,但此时有可能这个值已经不满足堆的定义,我们需要对其进行向下调整,此时只有堆根部不满足堆的定义

 我们此时对根部进行调整

我们首先想到在根的左右结点中找到最小的值来判断是否大于根,如果根已经是最小值,则满足堆的定义,不需要进行交换,否则根与最小值进行交换

 //对堆的根部进行向下调整
    //结束调整的条件?  已经满足堆的结构,无需调整    当前结点已经是叶子结点,下边没有节点,无需调整
    public void justDown(int arr[],int size,int index){
        int left=2*index+1;//判断左子树是否存在,如果没有左子树,必没有右子树(完全二叉树性质)
        while (left<size){
            int min=left+1<size&&arr[left+1]<arr[left]?left+1:left;//找出左右子树中的最小值
            int largest=arr[index]<=arr[min]?index:min;//在根和最小值中再找出最小值
            if (largest==index) break;//如果根就是最小值 堆结构已满足 退出循环
            swap(arr,index,min);  //不满足交换根和最小值,根等于最小值进行下一次调整
            index=min;
            left=index*2+1;
        }
    }

    private void swap(int[] arr, int index, int min) {
        int temp=arr[index];
        arr[index]=arr[min];
        arr[min]=temp;
    }

2、堆的创建

在我们已经掌握向下调整的情况下,如何将一个完全二叉树调整为堆呢?

我们可以对每个结点进行向下调整,使每个结点都满足堆的定义

但我们知道向下调整的结束条件是当前结点已经满足堆的结构或者当前结点是叶子结点,因此我们没有必要对叶子结点进行调整,而在完全二叉树中我们很容易找到最后一个结点(size-1)是最后一个结点的下标 而他的父节点是(size-1-1)/ 2   那么自他的父节点之后就全是叶子节点

public void creatHeap(int[] arr,int size){
        for (int i=(size-2)/2;i>=0;i--){
            justDown(arr,size,i);
        }
    }

为什么从上到下不行?因为我们向下调整是由要求的,必须左右子树已经是一个堆结构了 如果不理解可以画图。。

 

在经历一次向下调整时我们发现最小的1永远没有走到顶部的机会,为什么呢,因为我们没有满足向下调整的定义。没有找到真正最小的那个数调整到堆顶 

3、向上调整

在我们创建完成一个堆的时候,往往需要往堆里边添加新的元素,由于我们从数组【0,size)是已经创建好的堆结构,在我们新添加一个元素时,往往需要向上调整

public void justUp(int[] arr,int index){
        int parent=(index-1)/2;//父节点下标
        while (arr[index]>arr[parent]){//当子节点大于父节点时我们需要向上调整
            swap(arr,index,parent);
            index=parent;//新的Index是他的父节点
            parent=(index-1)/2;//新的parent结点
        }
    }

 三、堆的比较器传入及堆中简单函数的实现

假设当前有一个person类,里边的属性有姓名和年龄,此时person是默认不支持比较的

 public class person{
        String name;
        int age;
    }

在不修改person类的情况下我们如何实现比较年龄呢?

答案是采用外部比较器

static class PersonComparator implements Comparator<Person>{

        @Override
        public int compare(Person o1, Person o2) {
            return o1.age-o2.age;
        }
    }
 static class PersonComparator implements Comparator<Person>{

        @Override
        public int compare(Person o1, Person o2) {
            return o1.age-o2.age;
        }
    }

    public static void main(String[] args) {
        PriorityQueue<Person> pq=new PriorityQueue(new PersonComparator());
        pq.add(new Person("小明",11));
        pq.add(new Person("小红花",9));
        pq.add(new Person("小刚",21));
        while (!pq.isEmpty()){
            System.out.println(pq.poll().toString());
        }

    }

堆的简单函数全实现:

public class MyPriorityQueue{
    long arr[];
    int size;
    public MyPriorityQueue(){//由于我们的元素类型是long类型没有采用泛型,所以不涉及传入比较器
        arr=new long[20];
        size=0;
    }

    public void add(long val){//往堆中加入一个元素
        ensureCapacity();//保证数组空间够用
        arr[size]=val;
        justUp(arr,size);//新加入的元素需要向上调整维持一个堆结构
        size++;
    }

    public long peek(){//查看堆顶部元素
        if (size<0){
            throw new RuntimeException("队列是空的");
        }
        return arr[0];
    }

    public long poll(){//弹出堆顶部的元素
        if (size<0){
            throw new RuntimeException("队列是空的");
        }
        long res=arr[0];//先记录需要返回的元素
        arr[0]=arr[size-1];//把最后一个元素调整到堆的顶部并向下调整
        arr[size-1]=0;//最后一个元素交换到堆顶,那么它就要改为默认值
        size--;
        justDown(arr,size,0);
        return res;
    }

    public int size(){
        return size;
    }

    private void justDown(long[] arr, int size, int index) {
        int left=index*2+1;
        while (left<size){//如果还有子节点(不是叶子节点)
            int min=left+1<size&&arr[left+1]<arr[left]?left+1:left;//找到左右子结点中的最小值
            int largest=arr[min]<arr[index]?min:index;//找到最小的值
            if (largest==index) break;//如果index本身就是最小值说明他已经满足堆结构不需要调整
            swap(arr,min,index);//交换index和最小值
            index=min;//新的index
            left=index*2+1;//新的最小值
        }
    }

    public void check(){//检查是否满足堆结构
        if (size<0||size>arr.length){
            throw new RuntimeException("size越界");
        }
        for (int i=0;i<size;i++){
            int left = 2 * i + 1;
            int right = 2 * i + 2;

            if (left >= size) {
                // 说明是叶子,跳过
                continue;
            }

            // 左孩子破坏了规则
            if (arr[i] > arr[left]) {
                throw new RuntimeException(String.format("[%d] 位置的值大于其左孩子的值了", i));
            }

            // 右孩子破坏了规则
            if (right < size && arr[i] > arr[right]) {
                throw new RuntimeException(String.format("[%d] 位置的值大于其右孩子的值了", i));
            }
        }
    }

    private void justUp(long[] arr, int index) {
       while (index!=0){
           int parent=(index-1)/2;
           if (arr[parent]<=arr[index]) return;
           swap(arr,index,parent);
           index=parent;
       }
    }

    public boolean isEmpty(){
        return size==0;
    }

    private void ensureCapacity() {
        if (size<arr.length) return;
        arr= Arrays.copyOf(arr,size*2);
    }

    public void swap(long[] arr,int i,int j){
        long temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
}

四、堆的应用

1、用于OS调度进程

2、topk问题

topk问题其实在现实生活有很多场景,比如说淘宝的好物top10等等

假如说我们要在很多数据中找到前k个最大的数  我们有什么思路呢?

1.对所有的数据进行排序再取出前十

这种方法无疑消耗的时间成本和空间成本是是非常高的

时间复杂度达到了O(N*logN) 空间复杂度是O(N)

2.把所有的数据入堆,在取出前k个元素 

第二种方法的时间复杂度相较于第一种减少了很多 大概是O(k*logN),但我们忽略了在数据非常庞大时,堆的向下调整操作往往也需要耗费大量时间,并且空间复杂度也达到了惊人的O(N)

3.我们采用建小堆,小堆的size就是k,如果当前元素比小堆中最小元素大,我们就把它加进去,到最后堆里的元素就是topk

并且空间复杂度也只有O(k)

代码如下:

public class Solution {
    public int[] smallestK(int[] arr, int k) {
        if (k==0){ //处理特殊情况,防止堆为空造成的index越界
            return new int[0];
        }
        PriorityQueue<Integer> pq=new PriorityQueue<>(((o1, o2) -> o2-o1));
        for (int i=0;i<k;i++){ //添加k个元素进入堆
            pq.add(arr[i]);
        }
        for (int i=k;i<arr.length;i++){//维持堆中的元素是最小的k个
            if (arr[i]<pq.peek()){
                pq.poll();
                pq.add(arr[i]);
            }
        }
        int[] res=new int[k];//返回这k个元素
        int i=0;
        while (!pq.isEmpty()){
            res[i++]=pq.poll();
        }
        return res;

    }
}

 3、堆排序

堆排序是一种利用堆结构找到最值然后不断取出进行排序,本质上是一种选择排序,不过由于堆找出并维持最值的时间复杂度是O(N*longN)所以他是一种高效的排序

我们把需要排序的数组看  无序加有序的形式 一旦我们找到最值元素 我们就把它交换到最后边

因此总的排序要进行arr.length-1次交换

public void swap(long[] arr,int i,int j){
        long temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

    public long[] heapSort(long[] nums){
        //首先我们需要建立大堆,把大的元素找出并交换到最后边
        buildHeap(nums);
        for (int i=0;i<nums.length-1;i++){
            swap(nums,0,arr.length-1-i);
            justDown(nums,arr.length-i-1,0);
        }
        return nums;
    }

    private void buildHeap(long[] nums) {
        for (int i=(nums.length-2)/2;i>=0;i--){
            justDown(nums,nums.length,i);
        }
    }

 

有关浅浅理解一下堆的更多相关文章

  1. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  2. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  3. ruby - 易于初学者理解的 Ruby 库 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭3年前。Improvethisquestion我正处于学习Ruby的阶段,我想查看一些小型库的源代码以了解它们是如何构建的。我不知道什么是小型图书馆,但希望SO能推荐一些易于理解的图书馆来学习。因此,如果有人知道一两个非常小的库,这是新手Rubyists学习的好例子,请推荐!我想使用Manveru'sInnatelib,因为它试图保持在2000LOC以下,但我还不熟悉其中经常使用的Ruby速记。也许大约100-5

  4. ruby - 无法理解 `puts{}.class` 和 `puts({}.class)` 之间的区别 - 2

    由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A

  5. ruby - 有人可以解释一下在 Ruby 中注入(inject)的真实、通俗易懂的用法吗? - 2

    我正在学习Ruby,遇到了inject。我正处于理解它的风口浪尖,但当我是那种需要真实世界的例子来学习一些东西的人时。我遇到的最常见的例子是人们使用inject来添加一个(1..10)范围的总和,我不太关心这个。这是一个任意的例子。在实际程序中我会用它做什么?我正在学习,所以我可以继续使用Rails,但我不必有一个以Web为中心的示例。我只需要一些我可以全神贯注的目标。谢谢大家。 最佳答案 inject有时可以通过它的“其他”名称reduce更好地理解。它是一个对Enumerable进行操作(迭代一次)并返回单个值的函数。它有许多有

  6. ruby - 如何理解 Ruby 中的发送者和接收者? - 2

    我很难理解Ruby中sender和receiver的实际含义。它们一般是什么意思?到目前为止,我只是将它们理解为方法调用和获取其返回值的调用。但是,我知道我的理解还远远不够。谁能给我一个Ruby中发送者和接收者的具体解释? 最佳答案 面向对象中的一个核心概念是消息传递和早期概念化,这在很大程度上借鉴了计算的Actor模型。艾伦·凯(AlanKay)创造了面向对象一词并发明了最早的OO语言之一SmallTalk,他拥有voicedregretatusingatermwhichputthefocusonobjectsinsteadofo

  7. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  8. ruby - 你如何理解 Ruby 中的这个三元条件? - 2

    我在某些代码中遇到了三元组,但我无法理解条件:str.split(/',\s*'/).mapdo|match|match[0]==?,?match:"somestring"end.join我确实理解我是在某些点上拆分字符串并将总结果转换为数组,然后依次处理数组的每个元素。除此之外,我不知道发生了什么。 最佳答案 一种(稍微)不那么令人困惑的写法是:str.split(/',\s*'/).mapdo|match|ifmatch[0]==?,matchelse"somestring"endend.join我认为多行三元语句很糟糕,尤其是

  9. ruby - 您如何将 S3 理解为 Ruby 中的分层目录结构? - 2

    有没有人成功地将S3存储桶读取为子文件夹?文件夹1--子文件夹2----文件3----文件4--文件1--文件2文件夹2--子文件夹3--文件5--文件6我的任务是读取文件夹1。我希望看到子文件夹2、文件1和文件2,但看不到文件3或文件4。现在,因为我将存储桶键限制为prefix=>'folder1/',你仍然会得到file3和4,因为它们在技术上具有folder1前缀。似乎真正做到这一点的唯一方法是吸收folder1下的所有键,然后使用字符串搜索从结果数组中实际排除file3和file4。有没有人有过这方面的经验?我知道像Transmit和Cyber​​duck这样的FTP风格的S3

  10. 关于yolov5训练时参数workers和batch-size的理解 - 2

    关于yolov5训练时参数workers和batch-size的理解yolov5训练命令workers和batch-size参数的理解两个参数的调优总结yolov5训练命令python.\train.py--datamy.yaml--workers8--batch-size32--epochs100yolov5的训练很简单,下载好仓库,装好依赖后,只需自定义一下data目录中的yaml文件就可以了。这里我使用自定义的my.yaml文件,里面就是定义数据集位置和训练种类数和名字。workers和batch-size参数的理解一般训练主要需要调整的参数是这两个:workers指数据装载时cpu所使

随机推荐