堆的实际应用场景

482 阅读5分钟

堆的概念

  1. 堆是一颗完全二叉树

    完全二叉树:除了最后一层外的其他每一层的节点数都达到最大,且最后一层的节点都排列在最左边

  2. 每个子节点都值总是不小于其父节点(小顶堆,大顶堆则相反)

image-20230529222840235

堆的基本操作(以小顶堆为例)

  1. 创建堆(O(nlogn))

    思路:从最后一个非叶子节点开始,如果节点值a小于左右子节点的较小值b就将a和b交换,然后a再继续与其子节点比较,直到没有子节点或者子节点的值都大于a

  2. 插入元素

    思路:插入到原数组最后一个节点的后面,然后和其父节点比较,如果小于父节点则和父节点交换,直到成为根节点或者大于父节点

  3. 删除元素

    思路:删除堆顶元素,将最后一个元素交换到堆顶,将新的堆顶元素向下比较,调整形成新的堆

​
// 小顶堆
public class SmallTopHeap {
    private int[] nodes;
    private int size;
​
    // 创建一个长度为n的空堆
    public SmallTopHeap(int n) {
        this.nodes = new int[n];
        this.size=0;
    }
    // 将一个数组创建堆
    public SmallTopHeap(int[] nums) {
        nodes=Arrays.copyOf(nums,nums.length);
        this.size=nums.length;
        buildHead();
    }
    public void buildHead(){
        int n = nodes.length;
        // 找到最后一个非叶子节点
        int lastParent = (n+1)/2;
        // 从最后一个非叶子节点从后往前调整建堆
        for (int i = lastParent; i >=0 ; i--) {
            childAdjust(i);
        }
    }
    // 向下调整
    private void childAdjust(int index){
        int leftIndex=2*index+1;
        if (leftIndex>=this.size)
            return;
        int[] nums=this.nodes;
        int rightIndex=2*index+2;
        int minChildIndex=rightIndex<this.size&&nums[leftIndex]>=nums[rightIndex]?rightIndex:leftIndex;
        if (nums[minChildIndex]<nums[index]){
            swap(index,minChildIndex);
            childAdjust(minChildIndex);
        }
    }
    // 交换元素
    private void swap(int index1,int index2){
        int t = this.nodes[index1];
        this.nodes[index1] = this.nodes[index2];
        this.nodes[index2] = t;
    }
    // 插入元素
    public boolean insert(int num){
        if (this.size>= nodes.length)
            resize();
        int[] nums=this.nodes;
        nums[this.size++]=num;
        parentAdjust(this.size-1);
        return true;
    }
    // 向上调整
    private void parentAdjust(int index){
        if (index==0)
            return ;
        int parentIndex = (index-1)/2;
        if (this.nodes[parentIndex]>this.nodes[index]){
            swap(parentIndex,index);
            parentAdjust(parentIndex);
        }
    }
    // 扩容2倍
    private void resize(){
        int[] newNodes=new int[this.nodes.length<<1];
        System.arraycopy(this.nodes,0,newNodes,0,this.nodes.length);
        this.nodes=newNodes;
    }
    // 移除堆顶元素
    public int remove(){
        int result=this.nodes[0];
        swap(0,this.size--);
        childAdjust(0);
        return result;
    }
    // 打印堆
    public void printHeap(){
        for (int i = 0; i < this.size; i++) {
            System.out.print(this.nodes[i]+",");
        }
        System.out.println();
    }
}
// 测试
class Test{
   public static void main(String[] args) {
        int[] nums={1,5,3,2,6,7,4};
        SmallTopHeap heap=new SmallTopHeap(nums); // 建堆 1,2,3,5,6,7,4
        heap.insert(4); // 插入4  1,2,3,4,6,7,4,5
        heap.remove(); //删除堆顶  0,2,3,4,6,7,4
    }
}

堆的应用:

  1. 动态数据求topK

    静态数据:给你一个确定的数组,让你在求出这个数组的topK

    思路:静态数据可以选择快速排序或者堆排序,取排序后的第K个数据就可以了

    动态数据:随时都在产生新的数据,你需要在任意时刻都能返回截止到目前的topK

    思路:动态数据则需要维护一个大小为K的堆,如果求最大的第K个数则使用小顶堆,因为堆内的数据是截止目前最大的K个数,且最小的数就是堆顶元素也就是我们所需要的第K大的数,新的数字到来时只需要和堆顶元素比较,如果大于堆顶元素则可以把堆顶元素淘汰将新数据加入,重新调整为新的堆(同理如果求最小的第K个数则使用大顶堆,此处以求最大的K个数为例)

    // 求第K大个数
    public class TopKHeap {
        private int[] nodes;
        private int size;
    ​
        // 创建一个长度为n的空堆
        public TopKHeap(int n) {
            this.nodes = new int[n];
            this.size=0;
        }
        // 向下调整
        private void childAdjust(int index){
            int leftIndex=2*index+1;
            if (leftIndex>=this.size)
                return;
            int[] nums=this.nodes;
            int rightIndex=2*index+2;
            int minChildIndex=rightIndex<this.size&&nums[leftIndex]>=nums[rightIndex]?rightIndex:leftIndex;
            if (nums[minChildIndex]<nums[index]){
                swap(index,minChildIndex);
                childAdjust(minChildIndex);
            }
        }
        // 交换元素
        private void swap(int index1,int index2){
            int t = this.nodes[index1];
            this.nodes[index1] = this.nodes[index2];
            this.nodes[index2] = t;
        }
        // 插入元素
        public boolean insert(int num){
            int[] nums=this.nodes;
            // 如果当前堆内数据还未满则直接加入最后,然后向上调整
            if (this.size < nums.length){
                nums[this.size++]=num;
                parentAdjust(this.size-1);
    ​
            }// 到这里则堆已经满了,则比较是否新产生的数据大于堆顶元素,大于堆顶元素则取代顶堆元素,然后需要向下调整以满足堆的性质
            else if (num>nums[0]){
                nums[0]=num;
                childAdjust(0);
            }
            return true;
        }
        // 向上调整
        private void parentAdjust(int index){
            if (index==0)
                return ;
            int parentIndex = (index-1)/2;
            if (this.nodes[parentIndex]>this.nodes[index]){
                swap(parentIndex,index);
                parentAdjust(parentIndex);
            }
        }
        // 打印堆
        public void printHeap(){
            System.out.println("当前堆内数据为:");
            for (int i = 0; i < this.size; i++) {
                System.out.print(this.nodes[i]+",");
            }
            System.out.println();
        }
    }
    
// 测试
class Test{
    public static void main(String[] args) {
        TopKHeap topKHeap = new TopKHeap(3);
        Random random =new Random();
        for (int i = 0; i < 10 ; i++) {
            int randomNum=random.nextInt(100);
            topKHeap.insert(randomNum);
            System.out.println("第"+(i+1)+"次插入的数据为:"+randomNum);
            topKHeap.printHeap();
        }
    }
/**
第1次插入的数据为:89
当前堆内数据为:
89
第2次插入的数据为:92
当前堆内数据为:
89,92
第3次插入的数据为:56
当前堆内数据为:
56,92,89
第4次插入的数据为:87
当前堆内数据为:
87,92,89
第5次插入的数据为:99
当前堆内数据为:
89,92,99
第6次插入的数据为:18
当前堆内数据为:
89,92,99
第7次插入的数据为:33
当前堆内数据为:
89,92,99
第8次插入的数据为:78
当前堆内数据为:
89,92,99
第9次插入的数据为:52
当前堆内数据为:
89,92,99
第10次插入的数据为:35
当前堆内数据为:
89,92,99
  */
}
  1. 动态数据求中位数

其实和动态数据求topK的思想差不多,不过需要维护两个堆稍微复杂一些读者可以自己跟着思路尝试实现

思路:将数据按照大小分成两半,小的一半建立一个大顶堆,大的一半建立一个小顶堆,

数据的个数是偶数时:两个堆的数据个数相同,大顶堆的堆顶是较小的一半数据中的最大值,小顶堆的堆顶是较大的一半数据中的最小值,中位数为两个堆顶的平均数

数据的个数是奇数时:两个堆的数据个数相差1,返回数据较多的堆的堆顶则是中位数

当新的数据到来时将新数据和大顶堆(较小的一半数组成的堆)的堆顶比较,如果小于堆顶则加入大顶堆,如果大于堆顶则加入另一个堆,每次加入数据后都需要判断两个堆的个数是否相差超过1,如果超过了则将个数较多的堆remove一个数,加入到个数较少堆的一方

  1. 利用堆实现优先级队列-高性能定时任务列表

    思路:将任务按照时间大小进行形成一个堆,每次只查看堆顶的任务是否到达执行时间即可,无须扫描所有的任务,具体细节看代码

    public class ScheduleX {
    ​
        private Node[] nodes;
        private int size;
    ​
        private static volatile ScheduleX scheduleSingleton;
    ​
        // 创建一个长度为n的空堆
        private ScheduleX(int n) {
            this.nodes = new Node[n];
            this.size=0;
        }
        // 懒汉式实现单例
        public static ScheduleX getInstance(){
            if (scheduleSingleton==null){
                synchronized (ScheduleX.class){
                    if (scheduleSingleton==null)
                        scheduleSingleton=new ScheduleX(10);
                }
            }
            return scheduleSingleton;
        }
    ​
        // 向下调整
        private void childAdjust(int index){
            int leftIndex=2*index+1;
            if (leftIndex>=this.size)
                return;
            Node[] nums=this.nodes;
            int rightIndex=2*index+2;
            int minChildIndex=rightIndex<this.size&&nums[leftIndex].time>=nums[rightIndex].time?rightIndex:leftIndex;
            if (nums[minChildIndex].time<nums[index].time){
                swap(index,minChildIndex);
                childAdjust(minChildIndex);
            }
        }
        // 交换元素
        private void swap(int index1,int index2){
            Node t = this.nodes[index1];
            this.nodes[index1] = this.nodes[index2];
            this.nodes[index2] = t;
        }
        // 向上调整
        private void parentAdjust(int index){
            if (index==0)
                return ;
            int parentIndex = (index-1)/2;
            if (this.nodes[parentIndex].time>this.nodes[index].time){
                swap(parentIndex,index);
                parentAdjust(parentIndex);
            }
        }
        // 扩容2倍
        private void resize(){
            Node[] newNodes=new Node[this.nodes.length<<1];
            System.arraycopy(this.nodes,0,newNodes,0,this.nodes.length);
            this.nodes=newNodes;
        }
        // 移除堆顶元素
        private Node remove(){
            Node result=this.nodes[0];
            swap(0,--this.size);
            childAdjust(0);
            return result;
        }
        // 查看堆顶元素的剩余延迟时间
        private long getDelay(){
            // 生产者在第一次注册任务时认为堆顶的距离执行时间为无限大,则需要唤醒之前进入无限期等待的线程
            if (this.size <= 0 )
                return Long.MAX_VALUE;
            Node result=this.nodes[0];
            return result.time-System.currentTimeMillis();
        }
    ​
        // 消费者阻塞式获取任务
        // 当堆内没有任务时进入无限时等待
        // 堆内有任务时查看堆顶的任务是否到底调度时间,如果到了则执行任务,如果没到则线程wait(距离调度剩余时间)
        public synchronized Node get() throws InterruptedException {
            while(true){
                if (this.size<=0)
                    wait();
                long delay ;
                if ((delay=getDelay())<=0){
                    return remove();
                }
                wait(delay);
            }
        }
        // 生产者增加调度任务
        public synchronized void register(long time,String task){
            // 当堆满时则扩容
            if (this.size>= nodes.length)
                resize();
            Node[] nums=this.nodes;
            // 计算当前线程距离调度的时间,如果小于堆顶元素的时间则需要唤醒线程计算休眠时间
            long currentDelay=System.currentTimeMillis()-time;
            long delay = getDelay();
            nums[this.size++]=new Node(time,task);
            parentAdjust(this.size-1);
            if (currentDelay<delay)
                // 当前线程距离调度的时间小于堆顶元素的时间唤醒线程计算休眠时间
                notifyAll();
        }
        // 任务节点
        class Node{
            long time;
            String task;
    ​
            public Node(long time, String task) {
                this.time = time;
                this.task = task;
            }
    ​
            @Override
            public String toString() {
                return "Node{" +
                        "time=" + time +
                        ", task='" + task + ''' +
                        '}';
            }
        }
    }
    ​
    

    生产者:

    public class Producer {
        public void start() {
            ScheduleX scheduleX = ScheduleX.getInstance();
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Random random = new Random();
            for (int i = 0; i < 10; i++) {
                long currentTime = System.currentTimeMillis();
                long scheduleTime = currentTime + random.nextInt(20) * 1000;
                String task = "任务" + (i + 1);
                scheduleX.register(scheduleTime, task);
                System.out.println("producer register " + task + ",schedule time :" + dateFormat.format(scheduleTime) + " at " + dateFormat.format(currentTime));
            }
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    消费者:

    public class Consumer {
        public void start(){
            ScheduleX scheduleX = ScheduleX.getInstance();
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                for (int i = 0; i < 10; i++) {
                    ScheduleX.Node taskNode = scheduleX.get();
                    System.out.println(taskNode.task+" schedule time: "+ dateFormat.format(taskNode.time)+
                            " execution time: "+dateFormat.format(System.currentTimeMillis()));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    测试:

    class Test{
        public static void main(String[] args) {
            Producer producer=new Producer();
            Consumer consumer=new Consumer();
            Thread p1 = new Thread(producer::start,"producer");
            Thread c1 = new Thread(consumer::start,"consumer");
            try {
                p1.start();
                c1.start();
                p1.join();
                c1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    /**
    执行结果如下:
    producer register 任务1,schedule time :2023-06-01 21:30:33 at 2023-06-01 21:30:20
    producer register 任务2,schedule time :2023-06-01 21:30:29 at 2023-06-01 21:30:20
    producer register 任务3,schedule time :2023-06-01 21:30:20 at 2023-06-01 21:30:20
    producer register 任务4,schedule time :2023-06-01 21:30:22 at 2023-06-01 21:30:20
    任务3 schedule time: 2023-06-01 21:30:20 execution time: 2023-06-01 21:30:20
    producer register 任务5,schedule time :2023-06-01 21:30:30 at 2023-06-01 21:30:20
    producer register 任务6,schedule time :2023-06-01 21:30:23 at 2023-06-01 21:30:20
    producer register 任务7,schedule time :2023-06-01 21:30:35 at 2023-06-01 21:30:20
    producer register 任务8,schedule time :2023-06-01 21:30:27 at 2023-06-01 21:30:20
    producer register 任务9,schedule time :2023-06-01 21:30:34 at 2023-06-01 21:30:20
    producer register 任务10,schedule time :2023-06-01 21:30:21 at 2023-06-01 21:30:20
    任务10 schedule time: 2023-06-01 21:30:21 execution time: 2023-06-01 21:30:21
    任务4 schedule time: 2023-06-01 21:30:22 execution time: 2023-06-01 21:30:22
    任务6 schedule time: 2023-06-01 21:30:23 execution time: 2023-06-01 21:30:23
    任务8 schedule time: 2023-06-01 21:30:27 execution time: 2023-06-01 21:30:27
    任务2 schedule time: 2023-06-01 21:30:29 execution time: 2023-06-01 21:30:29
    任务5 schedule time: 2023-06-01 21:30:30 execution time: 2023-06-01 21:30:30
    任务1 schedule time: 2023-06-01 21:30:33 execution time: 2023-06-01 21:30:33
    任务9 schedule time: 2023-06-01 21:30:34 execution time: 2023-06-01 21:30:34
    任务7 schedule time: 2023-06-01 21:30:35 execution time: 2023-06-01 21:30:35
    */