Lesson 1:优先级队列(堆)

目录

一、堆的相关概念

1.1 堆

1.2 下标关系

1.3 分类

二、手动实现堆

2.1 建立大根堆

2.2 添加数据

2.3 弹出堆顶数据

2.4 查看堆顶数据

2.5 topK问题

2.6 堆排序

2.7 寻找和最小的K对数字

三、Java中的优先级队列


一、堆的相关概念

1.1 堆

堆:逻辑上是一颗完全二叉树物理上保存在数组中。

堆逻辑上是棵完全二叉树,采用顺序存储的方式将值存入数组。

顺序存储:将二叉树采用“层序遍历”的方式存入数组。顺序存储一般适合于表示完全二叉树,非完全二叉树会有空间的浪费。

1.2 下标关系

这种下标关系针对完全二叉树.

已知:父亲节点下标 = parent,左孩子下标 = 2 * parent + 1,右孩子下标 = 2 * parent + 2

已知:孩子节点 = child,父亲节点下标 = (child - 1)/2

1.3 分类

小根堆:父亲节点 < 左右孩子节点,左右孩子大小没有关系。

大根堆:父亲节点 > 左右孩子节点,左右孩子大小没有关系。

只有整个二叉树的每棵子树都是大根堆/小根堆的时候,整棵树才是大根堆/小根堆。

二、手动实现堆

2.1 建立大根堆

建立一个大根堆主要分为两步。

第一步:定位到最后一棵子树。

假设数组的长度是len,最后一个元素的下标是len-1,也就是最后一棵树的孩子节点的下标是len-1,则最后一棵树的父亲节点下标是(len-1-1)/2。

 第二步:从最后一棵子树开始,判断是否是大根堆,并调整为大根堆,采取的策略是“向下调整”。

①假设最后一棵子树的根节点下标是parent,判断parent为根节点下标的树是否为大根堆,若不是大根堆需要调整为大根堆。接着,parent减去1,再判断新的parent为根节点的下标是否为大根堆,parent减去1,一直循环,循环结束条件是parent < 0。

②每一次调整大根堆,可能会导致子树不再满足大根堆的要求,因此,需要从parent向下调整,保证每棵子树都是大根堆。调整的结束条件是child >= len。

public class TestDemo {
    public int[] elem;
    public int usesize; // 表示数组中的元素个数
    public int capacity = 10;  // 数组的初始容量
    public TestDemo(){
        this.elem = new int[10];
    }
    public void createBigHeap(int[] array){
        if(array == null){
            return;
        }
        if(array.length >= this.capacity){ // 如果elem的长度大于capacity,需要扩容
            this.elem = Arrays.copyOf(this.elem,this.capacity*2);
        }
        for(int i=0;i<array.length;i++){
            this.elem[i] = array[i];
            usesize++;
        }
        for(int parent = (this.usesize-1-1)/2 ; parent>=0 ; parent--) {
            shiftDown(parent,this.usesize);
        }
    }
   // 向下调整 非递归
    public void shiftDown(int parent,int usesize){
        int child = 2*parent+1;
        while(child < this.usesize){
            if(child+1<this.usesize && this.elem[child] < this.elem[child+1]){
                child++; // 让child这个下标存放值最大的孩子节点的下标
            }
            if(this.elem[child]>this.elem[parent]){
                int tmp = this.elem[child];
                this.elem[child] = this.elem[parent];
                this.elem[parent] = tmp;
                // 交换完后需要向下调整/保证下面的每颗子树都满足大根堆的条件
                parent = child;
                child = 2*parent+1;
            }else{
                break;
            }
        }
    }
    // 向下调整 递归方式
    public void shiftDown2(int parent){
        int child = 2*parent+1;
        if(child >= this.usesize){
            return;
        }
        if(child+1 < this.usesize && this.elem[child]<this.elem[child+1]){
            child++; // 让child存放左右孩子值最大的下标
        }
        if(this.elem[child] > this.elem[parent]){
            int tmp = this.elem[child];
            this.elem[child] = this.elem[parent];
            this.elem[parent] = tmp;
            parent = child;
            shiftDown2(parent);
        }
    }
}

2.2 添加数据

往大根堆中添加数据后,需要保证添加后该堆还是个大根堆。采取“向上调整”的策略。

方法:将添加的数据放在队列最后,再向上调整.

public void push(int val){
        // 把val放在最后,调换最后一个元素和堆顶的元素
        if(this.elem.length > this.usesize){ // 如果elem的长度大于capacity,需要扩容
            this.elem = Arrays.copyOf(this.elem,this.capacity*2);
        }
        this.elem[usesize] = val;
        usesize++;
        int parent = (this.usesize-1-1)/2;
        int child = this.usesize-1;
        while(parent >= 0){
            if(this.elem[parent] < this.elem[child]){
                int tmp = this.elem[parent];
                this.elem[parent] = this.elem[child];
                this.elem[child] = tmp;
                child = parent;
                parent = (child-1)/2;
            }else{
                break;
            }
        }
    }

2.3 弹出堆顶数据

弹出堆顶数据不是真的将元素从堆中删除,而是将堆顶元素放在堆的最后,堆的usesize-1。在逻辑上,堆不包含该元素,堆的长度减去1.

步骤:

step1:调换堆顶元素和最后一个元素。

step2:从堆顶开始,向下调整。确保每棵数都是一个大顶堆。 

public int pop(){
        // 先交换第一个元素和最后一个元素
        int tmp = this.elem[0];
        this.elem[0] = this.elem[this.usesize-1];
        this.elem[this.usesize-1] = tmp;
        this.usesize--;
        // 向下调整parent = 0 的这棵树
        shiftDown2(0);
        return this.elem[this.usesize];
    }

2.4 查看堆顶数据

public int peek(){
        if(this.usesize == 0){
            throw new RuntimeException("null!");
        }
        return this.elem[0];
    }

2.5 topK问题

问题描述:从一个数组中找出前K个最大/最小的元素。

问题变形:从一个数组中找出第K大/小的元素。

以找数组中前K个最小的元素为例。

传统思维:调用sort函数对数组降序排序,选择前K个。

遇见这个问题,应该想到优先级队列,即堆。

思路1:建立一个小根堆,取前K个。 

评价:该方法错误。小根堆只能保证堆顶元素是最小的,不能保证前K个元素都是最小的。

思路2:建立一个小根堆,将堆顶元素一个一个弹出

评价:方法正确。每次堆顶元素被弹出时,会将新的堆调整为小根堆,保证堆顶元素都是最小的。

但是这样时间复杂度高,为N*logN。

思路3:将前K个元素建成大根堆,从第K+1个元素起,将其与堆顶元素比较,比堆顶元素小,则堆顶元素出堆,该元素入堆。

评价:该方法没有对整体进行排序,也控制了建堆的大小。时间复杂度为N*logK。

// 调用建堆、pop、push这些已经写好的代码
public int[] topk1(int[] array,int k){
        TestDemo testDemo = new TestDemo();
        if(k > array.length){
            return array;
        }
        for(int i = 0;i<array.length;i++){
            if(i<k){
                testDemo.push(array[i]);
            }else{
                System.out.println(this.elem[0]);
                if(array[i]<this.elem[0]){
                    testDemo.pop();
                    testDemo.push(array[i]);
                }
            }
        }
        int[] lst = new int[k];
        for (int i = 0; i < lst.length; i++) {
            lst[i] = this.elem[i];
        }
        return lst;
    }
// 第二种方法
    public int[] topk(int[] array,int k){
        if(k > array.length){
            return array;  // 如果k>数组的长度,返回整个数组
        }
        for(int i = 0;i<array.length;i++){
            if(i<k){
                this.elem[i] = array[i]; // 建立一个大小为k的大根堆
                this.usesize++;
                shiftDown(0);
            }else{
                if(array[i]<this.elem[0]){
                    int tmp = array[i];
                    array[i] = this.elem[0];
                    this.elem[0] = tmp;
                    shiftDown(0,this.usesize);
                }
            }
        }
        int[] lst = new int[k];
        for (int i = 0; i < lst.length; i++) {
            lst[i] = this.elem[i];
        }
        return lst;
    }

最优方法总结:

2.6 堆排序

假设对一个数组从小到大排序。

思路:

用end作为数组中已排序部分和未排序部分的分界线。

最开始,是一个大根堆,整个数组都不满足从小到大的要求,所以end=数组的长度-1

当交换堆顶元素和end下标的元素后,将最大的元素放在end位置,最大的元素在最后放着,证明该元素已经有序,所以end-1,指向下一个未排序的元素。

建立一个大根堆—》将堆顶元素与end下标的元素交换—》end的下标减1—》向下调整—>再将堆顶元素与end下标的元素交换—》end的下标减1——》向下调整--------一直循环,直到end<0

public int[] sorted(int[] array){
        if(array == null){
            return null;
        }
        int end = this.usesize;
        createBigHeap(array); 
        while(end >= 1){
            int tmp = this.elem[0];
            this.elem[0] = this.elem[usesize-1];
            this.elem[usesize-1] = tmp;
            end--;
            shiftDown(0,end);
        }
        int[] lst = new int[this.elem.length];
        for(int i = 0;i<this.elem.length;i++){
            lst[i] = this.elem[i];
        }
        return lst;
    }

2.7 寻找和最小的K对数字

1.问题描述和总结

public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> listList = new ArrayList<>();
        PriorityQueue<List<Integer>> priorityQueue = new PriorityQueue<>(new Comparator<List<Integer>>() {
            @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                return (o2.get(0)+o2.get(1))-(o1.get(0)+o1.get(1));
            }
        });
        for(int i = 0; i<Math.min(k,nums1.length) ;i++){
            for(int j =0; j<Math.min(k,nums2.length); j++){
                List<Integer> list =new ArrayList<>();
                list.add(0,nums1[i]);
                list.add(1,nums2[j]);
                listList.add(list);
                if(listList.size() <= k){
                    priorityQueue.add(list);
                }else{
                    List<Integer> ret = priorityQueue.peek();
                    int peeksum = ret.get(0)+ret.get(1);
                    if(nums1[i]+nums2[j] < peeksum){
                        List<Integer> list1 = new ArrayList<>();
                        list1.add(0,nums1[i]);
                        list1.add(1,nums2[j]);
                        priorityQueue.poll();
                        priorityQueue.offer(list1);
                    }
                }
            }
        }
        List<List<Integer>> list2 =new ArrayList<>();
        while(! priorityQueue.isEmpty()){
            List<Integer> lst = priorityQueue.poll();
            list2.add(lst);
        }
        return list2;
    }

三、Java中的优先级队列

Java中的优先级队列是PriorityQueue.

常用函数:

抛出异常返回特殊值
入队列addoffer
出队列removepoll
队首元素elementpeek
 public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        priorityQueue.offer(1);   //入队列
        priorityQueue.offer(-1);
        priorityQueue.offer(3);
        int b = priorityQueue.peek();  // 获取队首元素
        int a = priorityQueue.poll();  // 出队列
        System.out.println(a);
        System.out.println(b);
    }

接下来,通过几个小问题来深入了解PriorityQueue.

问题1: PriorityQueue底层是大根堆还是小根堆?

答案:是小根堆.

问题2 :PriorityQueue底层默认是一个小根堆,如何利用PriorityQueue建立一个大根堆?

答案:传入一个比较器.

PriorityQueue有多个构造函数 

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(-1);
        priorityQueue.offer(3);
        int b = priorityQueue.peek();  // 获取队首元素
        int a = priorityQueue.poll();  // 出队列
        System.out.println(a);
        System.out.println(b);
    }

问题3:PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();new了一个对象后,这个对象的默认大小是多少?

答案:11.

问题4: 入队列的元素超过队列长度,如何扩容?

答案:调用的是grow函数.

当数组长度 < 64, 新的数组长度 = 2 * 原来的数组长度 + 2 

当数组长度 > 64, 新的数组长度 = 1. 5* 原来的数组长度 

源代码:

①使用offer函数往队列中添加元素,  当数组长度超过默认长度,调用的函数是grow().

②接下来看看grow()函数,传入的参数是1.

 ③ 求grow()函数中的 newCapacity, 需要调用 ArraysSupport.newLength 函数. 

四、总结

首先,分享了如何实现创建大根堆、添加元素、删除堆顶元素、堆排序、topK问题、和最小的K对数字这些算法.其中创建大根堆、删除堆顶元素和堆排序的核心是向下调整,添加元素的核心是向上调整.topK问题和和最小的K对数字的核心是将前K个元素入堆,其他元素与堆顶比较.

然后,分享了java中的PriorityQueue,其中PriorityQueue默认建立的是小堆,且扩容方式为当数组长度 < 64, 新的数组长度 = 2 * 原来的数组长度 + 2 ,当数组长度 > 64, 新的数组长度 = 1. 5* 原来的数组长度. 如果需要建立一个大堆,再new PriorityQueue的对象时,可以传入一个比较器.

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘减减

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值