一,目录

1,概念

2,堆的创建及相关方法的实现

3,常用接口介绍

4,top(k)问题

5,堆排序

一,概念

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

优先级队列底层是使用了堆这种数据结构,而堆就是在完全二叉树的基础上进行了一些调整

二,创建及相关方法

堆主要分为大根堆小根堆

这两种都是在完全二叉树的基础上进行调整得来的,大根堆就是每一棵树的根都是根及其左右子树中最大的,小根堆就是每一棵树的根都是根及其左右子树中最小的,所以也有一个特点就是大根堆中根节点是最大的元素,小根堆中根结点是最小的元素,如下80就是所有元素里最大的,13就是所有元素里最小的

一:创建

1,我们先根据堆的思想来创建一个大根堆,其中的parent和child均为下标

2,具体做法是先找到最后一个左孩子的父亲节点的下标:我们就按照上图的树,也就是找到5的下标,我们还要引入一些前情数据,usedsize为13,那么根据完全二叉树的公式就知道,不管是左孩子还是右孩子,他们的父亲节点都是-1再/2,所以父亲节点的下标为(usedsize-1-1)/2,所以初始的parent=(usedsize-1-1)/2,然后parent依次往上,然后进行一个向下的调整

3,接下来就是要完成向下调整的一个代码,具体操作就是在左右孩子节点中找到最大的,然后将他的下标标记为chlid,然后将他和父亲节点进行比较,观察是否需要交换,交换完,交换下来的那个值并不一定是待在那个地方的,可能还需要和下面的数值进行一个调整,所以让parent等于child,然后child乘2加1,继续进行一个调整,当不需要调整的时候,就退出循环

具体代码如下

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

    public TestHeap(){
        this.elem = new int[15];
    }
    public void init(int[] array){
        for (int i = 0; i < array.length; i++) {
            elem[i]=array[i];
            usedsize++;
        }
    }
    //
    public void creatheap(){
        //先找到最后一颗子树的根节点的下标
        for (int parent = (usedsize-1-1)/2; parent >=0; parent--) {
            shelldown(parent, usedsize);
        }
    }
    //向下调整
    private void shelldown(int parent,int usedsize){
        //1,先取得左孩子节点的下标
        int child=parent*2+1;
        //调整到child小于usedsize的时候
        while (child<usedsize){
            //先取得左右孩子中最大的,并且得保证有右孩子的情况下,没有就直接不用变
            if (child+1<usedsize && elem[child]<elem[child+1]){
                child++;
            }
            //现在child里面存储的就是左右孩子里的最大值的下标,然后进行交换
            if (elem[child]>elem[parent]){
                swap(elem,parent,child);
                //只能保证调整了的是正确的,但是还得向下调整保证换完的也是大根堆
                parent=child;
                child=parent*2+1;
            } else {
                break;
            }
        }
    }
    //交换
    private void swap(int[] elem,int i,int j){
        int tmp=elem[i];
        elem[i]=elem[j];
        elem[j]=tmp;
    }
}

二:相关方法

1,增加元素

public void offer(int val){
        //先判断有没有满,满了就得扩容
        if (isfull()){
            elem= Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedsize]=val;
        siftup(usedsize);
        usedsize++;
    }

    private void siftup(int child) {
        int parent=(child-1)/2;
        //调整到parent>=0或者child>0的时候
        while (child>0){
            if (elem[child]>elem[parent]){
                swap(elem,child,parent);
                child=parent;
                parent=(child-1)/2;
            }else {
                break;
            }
        }
    }

    public boolean isfull(){
        return usedsize==elem.length;
    }

2,删除元素(头删)

public void pull(){
        if (isempty()){
            return;
        }
        //让首元素和最后一个元素交换
        swap(elem,elem[0],elem[usedsize-1]);
        usedsize--;
        //让首元素进行向下调整
        shelldown(0,usedsize);
    }
    public boolean isempty(){
        return elem.length==0;
    }
}

三,常用接口介绍

Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列, PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本⽂主要介绍 PriorityQueue,PriorityQueue当中也有相应的方法,如offer,peek等

PriorityQueue<Integer> priorityQueue=new PriorityQueue<>();
        priorityQueue.offer(10);
        priorityQueue.offer(3);
        priorityQueue.offer(9);
        System.out.println(priorityQueue.peek());

我们通过peek方法可以知道系统自带的PriorityQueue是个小根堆,然后我们通过源码来看看它的构造方法,以及有没有方法来使其变成一个大根堆

通过源码的构造方法可以看出,有没有参数的也有两个参数的,其中的传入的比较器成为了关键

点入到offer方法里面可以发现有两种,一种是传入比较器,一种是使用默认的比较方法,所以我们只要写一个比较器,里面比较的方法根默认的是相反的话,那么就可以实现大根堆了,我来展示两种重写的比较方法

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

第一种就是写一个比较器,在PriorityQueue调用的时候传入这个imp类就行了

PriorityQueue<Integer> priorityQueue2=new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });

第二种方法通过匿名内部类来实现

 什么是匿名内部类

没有名字的局部内部类,只用一次、用完就丢,不用单独新建 .java 文件写类;

只能实现一个接口 / 继承一个父类

核心场景:快速实现函数式接口(Comparator、Runnable 等)。

四,top(k)问题

当我们要求一些数据的最大或者最小的k个的时候,我们可以用排序来找到前面的数据,当然也可以运用到堆,当我们要求前k个最小的数据的时候,我们可以创建一个大根堆,堆的数据个数为k,并且每次用下一个数据跟堆顶的元素进行比较,因为堆顶的数据肯定是k个中最大的,我们考虑换元素也是优先考虑它,具体代码如下

//top k问题
    public int[] smallk(int[] array,int k){
        //先判断数据
        int[] ret=new int[k];
        if (array==null||k==0){
            return null;
        }
        //创建一个大根堆
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        //将前k个数据放在大根堆里面
        for (int i = 0; i <= k; i++) {
            priorityQueue.offer(array[i]);
        }
        //将后面的数据跟堆顶进行比较,来调整堆
        int peekval=priorityQueue.peek();
        for (int i = k+1; i < array.length; i++) {
            if (peekval>array[i]){
                priorityQueue.poll();
                priorityQueue.offer(array[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            int val=priorityQueue.poll();
            ret[i]=val;
        }
        return ret;
    }

五,堆排序

我们以从小到大来排序,就紧接在我们刚刚自己实现的大根堆下面,堆排序主要的思路就是找到堆顶元素,因为它是最大的,然后将其和最后一个元素进行交换,可以拿一个end下标来记录大的元素,end就等于usedsize-1,然后进行向下调整,最后调整到堆顶元素为最小值,然后依次增大

//从小到大排
    public void heapsort(){
        int end=usedsize-1;
        while (end>0){
            swap(elem,0,end);
            shelldown(0,end);
            end--;
        }
    }

自己的小问题以及理解:怎么理解创建堆的时候,传入的parent是最后一颗子树的父亲节点,但是后面我要自己调整的时候可以直接传入0下标

因为建堆的时候,所有数据都是乱给的,没有一个数据是排序好的,所以从最后一个往前调整,但是排序的时候,我们交换了最后一个以及堆顶的元素的时候,堆顶的左右子树都是完整的大根堆,已经是排序好的,所以堆顶的三个元素组成的树是需要调整的,所以传入0下标,然后后续下面有问题再shelldown里面也会接着调整

更多推荐