数据结构 优先级队列(堆)

一、概念

1.1类比现实生活

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.2与普通队列区别

在这里插入图片描述

二、基于二叉树的堆的特点

2.1关于存储结构

完全二叉树一般采用数组结构存储
在这里插入图片描述

2.2关于节点值

在这里插入图片描述
是否层次越高,根的节点值一定越大?
在这里插入图片描述
在这里插入图片描述

2.3关于节点的编号

在这里插入图片描述

2.4判断堆的条件

在这里插入图片描述

三、堆的三大核心操作

代码示例如下

/**
 * 基于动态数组实现最大堆
 * @author hide_on_bush
 * @date 2022/6/24
 **/
public class MaxHeap {
    //动态数组存储元素
    private List<Integer> data;
    //当前堆中元素个数
    private int size;

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

    public int getSize(){
        return size;
    }

    /**
     * 找到节点k对应的父节点索引
     * */
    private int parent(int k){
        return (k-1)>>1;
    }
    /**
     * 找到节点k对应的左子树索引
     * */
    private int leftChild(int k) {
        return (k<<1)+1;
    }
    /**
     * 找到节点k对应的右子树索引
     * */
    private int rightChild(int k) {
        return (k<<1)+2;
    }
}

3.1堆中添加元素 siftUp

在这里插入图片描述
新增元素的变化过程:

在这里插入图片描述

在这里插入图片描述
对应代码:
易错点提示:1.动态数组获取元素只能调用方法,不能简单的data[索引值]
2.交换元素也不能使用交换三连,而是调用方法

//添加元素siftUp
    public void add(int val){
        //动态数组add方法默认是尾插法
        data.add(val);
        size++;
        //添加完元素进行上浮操作
        siftUp(size-1);
    }

    private void siftUp(int i) {
        //上浮操作的两个终止条件
        //1.已上浮到根节点的位置
        //2.当前索引的节点值大于当前索引的父节点值
        while(i>0&& data.get(i) > data.get(parent(i))){
            swap(i,parent(i));
            i=parent(i);
        }
    }

    //交换元素
    private void swap(int i, int parent) {
        int child=data.get(i);
        int parentVal=data.get(parent);
        data.set(i,parentVal);
        data.set(parent,child);
    }

总结:
在这里插入图片描述

3.2 最大堆获取最大值

在这里插入图片描述
在这里插入图片描述
核心思路:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/**
     * 取出堆中最大值
     * */
    public int extractMax(){
        if(size==0){
            throw new NoSuchElementException("heap is empty!");
        }
        //取出最大值,根节点
        int max=data.get(0);
        //将数组末尾顶到堆顶
        data.set(0, data.get(size-1));
        //删除堆中末尾元素
        data.remove(size-1);
        size--;
        //将末尾元素进行下沉操作
        siftDown(0);
        return max;
    }

    private void siftDown(int i) {
        //判断是否存在左子树
        while(leftChild(i)<size){
            int j=leftChild(i);
            //此时存在右子树,且值比左子树大
            if(j+1<size&&data.get(j+1)>data.get(j)){
                j++;
            }
            //此时索引位置一定对应左右子树最大值的索引
            if(data.get(i)>= data.get(j)){
                //当前元素大于左右子树最大值,元素下沉结束
                break;
            }else{
                swap(i,j);
                i=j;
            }
        }
    }

    //交换元素
    private void swap(int i, int parent) {
        int child=data.get(i);
        int parentVal=data.get(parent);
        data.set(i,parentVal);
        data.set(parent,child);
    }

3.3 堆化 heapify(重点掌握思想)

在这里插入图片描述
第一种方法:构建一个新的最大堆,直接调用最大堆的add方法

时间复杂度为:nlogn,n代表遍历原数组,logn代表在二叉树中添加元素
空间复杂度为:O(n),因为开辟了新的空间
在这里插入图片描述
第二种方法:原地堆化

核心思想:从最后一个非叶子节点开始进行siftDown操作
原因依次从最后一棵子树开始调整为最大堆的子最大堆
在这里插入图片描述
注意事项:自底向上建堆是一个时间复杂度近似O(n)的方法
在这里插入图片描述

//原地堆化
    public MaxHeap(int[] arr){
        //将所有元素复制到data
        data=new ArrayList<>(arr.length);
        for (int i:arr){
            data.add(i);
            size++;
        }
        //从最后一个非叶子节点进行下沉操作
        //没有parent方法就(size-1-1)/2
        for (int i = parent(size-1); i >=0 ; i--) {
            siftDown(i);
        }
    }

3.3 拓展:java对象比较方法

java大小关系比较:
在这里插入图片描述

comparaTo接口弊端

每次根据需求就必须更改源代码,因为不同场景下大小关系是不确定的
在这里插入图片描述

comparator接口

每次不同的需求就创建关于这个类的不同的比较器,实现不同的需求
在这里插入图片描述
在这里插入图片描述
核心思想:不碰原来写过的代码,需要怎么比较就新创建一个类,一个类就是一种比较方法。对其它比较方法不影响

//这个类是根据年龄越大就越大进行比较
class StudentSec implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge()-o2.getAge();
    }
}

//这个类是根据是年龄越大反而越小的类
class StudentDesc implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o2.getAge()-o1.getAge();
    }
}

public class Student {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public Student(int age) {
        this.age = age;
    }

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

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

    public static void main(String[] args) {
        Student[] students=new Student[]{
                new Student(19,"may"),
                new Student(20,"rose"),
                new Student(21,"jack")
        };
        //得到一个按年龄升序数组
        //student这个类不具备可比较的能力,传入比较器
        Arrays.sort(students,new StudentSec());
        System.out.println(Arrays.toString(students));
        //按年龄降序的数组
        Arrays.sort(students,new StudentDesc());
        System.out.println(Arrays.toString(students));
    }
}

四、基于堆的优先级队列的应用

4.1TopK问题(重点掌握)

在这里插入图片描述
第一种方法的时间复杂度是nlogn:仅供参考,不推荐使用

//时间复杂度为nlogn的算法
    public int[] smallestTop4(int[] data,int k){
        Arrays.sort(data);
        int[] ret=new int[k];
        for (int i = 0; i < k; i++) {
            ret[i]=data[i];
        }
        return ret;
    }

第二种方法时间复杂度近似O(n)但还是nlogk(重点掌握):
在这里插入图片描述
注意事项:1.边界条件有两个,一个判断数组为空,还有一个判断k==0
2.jdk默认的优先级队列是最小堆,要传入比较器对象变成最大堆

/**
 * 最小k个数
 * @author hide_on_bush
 * @date 2022/5/22
 */
public class Num17_14SmallestK {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        //边界条件别忘记k=0
        if(arr.length==0||k==0){
            return ret;
        }
        //内部类传入一个比较器对象,因为JDK默认最小堆
        //传入比较器后默认值越大反而越小排在队列之前
        Queue<Integer> queue=new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });
        //遍历数组集合
        for (int i = 0; i < arr.length; i++) {
            if(queue.size()<k){
                queue.offer(arr[i]);
            }else{
                int max=queue.peek();
                if(arr[i]<max){
                    queue.poll();
                    queue.offer(arr[i]);
                }
            }
        }
        int i=0;
        while(!queue.isEmpty()){
            ret[i++]=queue.poll();
        }
        return ret;
    }
拓展:内部类(匿名内部类)

实际上就是 class 无引用名称=new 接口(向上转型)
在这里插入图片描述

4.2 leetcode347号问题

在这里插入图片描述
难点:
在这里插入图片描述
补充支持:Map集合遍历方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
核心思路:
1.原数组扫描,将出现的元素和元素出现的频次保存到map集合
2.再扫描Map集合,将结果放入优先级队列中(最小堆)
3.遍历队列,将最大频次结果依次出队

几个关键点:
1.使用map是为了方便记录键值对
2.使用自定义类是为了方便保存在队列中

/**
 * @author hide_on_bush
 * @date 2022/6/27
 */
//在优先级队列中添加该元素对象即可
class Freq implements Comparable<Freq>{
    //数组出现的元素
    int key;
    //key出现的次数
    int value;

    public Freq(int key, int value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public int compareTo(Freq o) {
    //谁出现的频次越大就越大
        return this.value-o.value;
    }
}

public class Num347_TopKK {
    public int[] topKFrequent(int[] nums, int k) {
        //返回的结果集
        int[] data=new int[k];
        //1.先扫描原数组,将出现元素保存到map集合,保存的是entry对象
        Map<Integer,Integer> map=new HashMap<>();
        for (int i:nums) {
            if(map.containsKey(i)){
                //此时元素i出现过,频次++
                //获取该元素i出现的频次
                int times=map.get(i);
                map.put(i,times+1);
            }else{
                //第一次出现,将元素保存map集合,频次设置为1
                map.put(i,1);
            }
        }
        //2.遍历map集合,找出频次最高的k次个元素保存到优先级队列中
        Queue<Freq> queue=new PriorityQueue<>();
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            if(queue.size()<k){
                queue.offer(new Freq(entry.getKey(),entry.getValue()));
            }else{
                //判断当前元素和堆顶元素出现的频次
                //当前元素出现频次大于堆顶元素的频次,入队
                Freq freq= queue.peek();
                if(entry.getValue()> freq.value){
                    queue.poll();
                    queue.offer(new Freq(entry.getKey(), entry.getValue()));
                }
            }
        }
        //此时队列保存出现频次最高的k个元素
        int index=0;
        while (!queue.isEmpty()) {
            data[index++] =queue.poll().key;
        }
        return data;
    }
}

注意事项:每次出队或者入队需要Freq对象,而不是简单的把key或者value数值简单的输出

4.3leetcode373 最小对元素

添加链接描述
在这里插入图片描述
注意事项:1.优先级队列保存的还是一对数(最大堆)
在这里插入图片描述
2.循环终止条件:应该是k和数组长度两者的最小值,因为这是有序排序,最小值一定是在k之前或者数组长度之前(在谁之前看两者大小关系)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

/**
 * 和最小的k个数对 - 用最大堆
 * @author yuisama
 * @date 2022/05/18 19:39
 **/
public class Num373_KSmallestPairs {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        // 1.创建优先级队列,其中队列中保存Pair对象,传入比较器,和越小反而越大
        Queue<Pair> queue = new PriorityQueue<>(new Comparator<Pair>() {
        //最大堆,反而最上面的值越大
            @Override
            public int compare(Pair o1, Pair o2) {
                return (o2.u + o2.v) - (o1.u + o1.v);
            }
        });
        // 2.遍历两个数组,其中u来自第一个数组,v来自第二个数组
        // 队列中保存了和最小的k个数对
        for (int i = 0; i < Math.min(nums1.length,k); i++) {
            for (int j = 0; j < Math.min(nums2.length,k); j++) {
                if (queue.size() < k) {
                    queue.offer(new Pair(nums1[i],nums2[j]));
                }else {
                    int add = nums1[i] + nums2[j];
                    Pair pair = queue.peek();
                    if (add < (pair.u + pair.v)) {
                        queue.poll();
                        queue.offer(new Pair(nums1[i],nums2[j]));
                    }
                }
            }
        }
        // 3.此时队列中就保存了和最小的前k个Pair对象,取出其中u和v即可
        List<List<Integer>> ret = new ArrayList<>();
        while (!queue.isEmpty()) {
            List<Integer> temp = new ArrayList<>();
            Pair pair = queue.poll();
            temp.add(pair.u);
            temp.add(pair.v);
            ret.add(temp);
        }
        return ret;
    }
}
//保存在优先级队列中的对象
class Pair {
    // 来自数组1
    int u;
    // 来自数组2
    int v;

    public Pair(int u, int v) {
        this.u = u;
        this.v = v;
    }
}

4.5原地堆排序

在这里插入图片描述
不使用最小堆的原因:层次的深度和大小无关,最小堆按编号顺序不一定是升序

/**
 * 七大排序
 * @author yuisama
 * @date 2022/05/18 20:21
 **/
public class SevenSort {
    public static void main(String[] args) {
        int[] arr = {19,27,13,22,3,1,6,5,4,2,110,65,70,98,72};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    // 原地堆排序
    public static void heapSort(int[] arr) {
        // 1.先将任意数组进行heapify调整为最大堆
        for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
            siftDown(arr,i,arr.length);
        }
        // 2.不断交换堆顶元素到数组末尾,每交换一个元素,就有一个元素落在了最终位置
        for (int i = arr.length - 1; i > 0; i--) {
            // arr[i] 就是未排序数组的最大值,交换末尾
            swap(arr,0,i);
            siftDown(arr,0,i);
        }
    }

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

    /**
     * 元素下沉操作
     * @param arr
     * @param i 下沉的索引
     * @param length 数组的长度
     */
    private static void siftDown(int[] arr, int i, int length) {
        while (2 * i + 1 < length) {
            int j = 2 * i + 1;
            if (j + 1 < length && arr[j + 1] > arr[j]) {
                j = j + 1;
            }
            if (arr[i] >arr[j]) {
                break;
            }else {
                swap(arr,i,j);
                i = j;
            }
        }
    }
}
  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值