【Java学习—(20)优先级队列,以及优先级队列(TopK问题)的Leetcode问题】

(点击跳转即可哦)

java学习专栏

LeetCode刷题专栏



优先级队列(堆)

优先级队列(堆) :按照优先级的大小动态出队(动态指的是元素个数动态变化,而非固定)

普通队列:FIFO 按照元素的入队顺序出队,先入先出


​ 时间复杂度 入队 出队

普通的链式队列: O(1) O(n)

优先级队列(堆): O(logn) O(logn)

在计算机领域,若见到 logn 时间复杂度,近乎一定和 结构相关(并非一定要构造一棵树结构,算法过程逻辑上一定是一颗树)


二叉堆的特点

二叉堆:基于二叉树的堆(二叉堆,也是应用最广泛的堆),还有d叉堆,索引堆


二叉堆的特点:

1 完全二叉树

是一颗完全二叉树,基于数组 存储(元素都是靠左排列,数组中存储时不会浪费空间,不存储空节点)。只有完全二叉树适合使用数组这样的结构来存储,其他的二叉树都要链式结构。

2 关于节点值

堆中根节点值 >= 子树节点中的值(最大堆,大根堆)

堆中根节点的值 <= 子树节点的值(最小堆,小根堆)

JDK中的 Queue接口的PriorityQueue是基于最小堆的实现

拿最大堆举例来说,节点的层次 和节点的大小没有任何关系,只能保证当前树中,树根是最大值(比左子树树根,右子树树根大)。

其他节点层次不确定。

3 基于数组存储

因为堆是基于数组来存储的,节点之间的关系通过数组下标来表示。从0开始编号,数组下标也是从此开始。

假设此时节点编号为 i,且存在父子节点,自己实现时要注意边界条件。

父节点编号:parent = (i -1) / 2

左子树编号:left = 2 * i + 1

右子树编号:right = 2 * i + 2


节点之间通过数组的索引下标来找到父子节点


基于动态数组ArrayList实现的最大堆

1 向堆中添加元素

堆是数组实现的,添加元素就在末尾直接添加

此时这个堆仍然满足完全二叉树的性质,但是此时这个完全二叉树就不再是一个最大堆了,因此需要进行元素的上浮操作,让新元素上浮到合适位置。

siftUp 上浮操作:

上浮操作的终止条件:

1 当前已经上浮到了树根 -> 这个元素一定是最大值

2 当前元素 <= 父节点对应的元素值,此时元素落在正确位置

private void siftUp(int i){//要上浮元素的索引
    while(i > 0 && data.get(i) > data.get((i-1)/2){//没有走到根节点,并且子节点大于根节点,进行交换
        swap(i,(i-1)/2);//交换节点的值
        i = (i-1)/2;//继续判断交换后的节点值是否小于根节点
    }
}
//使用动态数组提供的方法进行交换操作
private void swap(int i,int j){
    int iVal = data.get(i);
    int jVal = data.get(j);
    data.set(i,jVal);//将索引 i 对应的值 修改为 jVal
    data.set(j,iVal);//将索引 j 对应的值 修改为 iVal
}

2 在堆中取出最大值(最大堆)

1 最大堆的最大值一定处在树根节点,直接取出树即可。

2 将堆中最后一个元素顶到堆顶,然后进行元素的下沉操作。

siftDown(下沉操作) -> 使其仍然满足最大堆的性质

  • 需要融合左右两个子树,使得取出树根后这颗树仍然是最大堆。此时融合操作还是比较复杂的,因为左右子树大小关系不定,且节点的大小和层次没有比必然联系。
  • 使用最低的成本 来融合左右子树,使其仍然满足最大堆的性质。 移除数组末尾元素
//取出最大值,也就是堆顶元素
public int poll(){
    if(size == 0){
        throw new NoSuchElementException("heap is empty!");
    }
    //最大值就是堆顶元素
    int max = root.get(0);
    //将数组的末尾元素顶到堆顶
    root.set(0,root.get(size-1));
    //删除最后一个元素
    root.remove(size-1);
    size--;
    //从头开始进行元素的下沉操作
    siftDown(0);
    return max;
}

siftDown操作

private void siftDown(int i){
    while(2*i-1 < size){
        //右子树的索引
        int j = 2*i-1;
        //左子树的索引存在,并且左子树的值大于右子树时。进入循环
        if(j+1 < size && root.get(j) < root.get(j+1)){
            j++;
        }
        //此时j 对应的索引就是左右子树最大值的索引
        if(root.get(i) > root.get(j)){
            //父节点大于左右子树的节点,停止下沉操作
            break;
        }else{
            //交换索引的值,继续下沉
            swap(i,j);
            i = j;
        }
    }
}

3 heapify - 堆化

现在给你一个任意的整型数组 -> 都可以看作是一个完全二叉树,距离最大堆就差元素调整操作。

第一种方法:

将这数组中的n个元素 依次调用 add方法添加到一个新的最大堆中,遍历原数组,创建一个新的最大堆,

时间复杂度:nlogn

空间复杂度:O(n)

第二种方法:

原地heapify

从最后一个非叶子节点 开始进行元素的siftDown操作。

不断的将子树调整为最大堆,最终走到树根节点时,左右子树已经是最大堆了,只需要最后下沉根节点就能得到最终的最大堆

public void MaxHeap(int[] arr){
    root = new ArrayList<>(arr.length);
    //1 将所有元素复制到root 数组中
    for(int i : arr){
        root.add(i);
        size++;
    }
    //2 从最后一个非叶子节点开始进行siftDown操作
    for (int i = parent(size-1); i >= 0; i--) {
        siftDown(i);
    }
}

在Java中比较两个元素的大小关系:

在Java中比较两个元素相等 equals

比较两个自定义对象的大小关系,需要类覆写Comparable 接口,实现compareTo方法。

若一个类Student implements Comparable,则这个类具备可比较的能力。

int compareTo(Object o){
	//比较当前对象和传入对象的大小关系
    > 0 : 当前对象 大于 传入对象o
    < 0 : 当前对象 小于 传入对象o
    = 0 : 当前对象 等于 传入对象o
}

java.util.Comparator; 接口

一个类若实现了这个接口,表示这个类天生就是为别的类的大小关系服务的。

class StudentSec implements Comparactor<Student>{
    @Override
    public int compare(Student o1,Student o2){
        return 0;
    > 0 : 当前对象 大于 传入对象o
    < 0 : 当前对象 小于 传入对象o
    = 0 : 当前对象 等于 传入对象o
    }
}

策略模式

当把Student 类的大小关系从Student类中 “解耦”,此时的比较策略非常灵活,需要哪种方式,只需要创新一个新的类的实现Comparator 接口即可,根据此时大小关系的需要传入比较器对象。


基于堆的优先级队列到底如何实现, TopK问题

TopK问题 都可以使用优先级队列解决,取大用小,取小用大

若需要取出前K个最大元素 构造最小堆

若需要取出前K个最小元素 构造最大堆

面试题17.14 最小K个数

取出前4个最小元素 -> 构造一个大小为 4 的最大堆

思路:

1 若此时队列的元素个数 < k,直接添加到队列中。

2 若此时元素个数 == k

a 新扫描到的元素val >= 堆顶元素,一定大于此时堆中的所有元素,则val一定不是我要的结果,直接跳过

b 若此时val < 堆顶元素,堆顶元素出队,将新的元素val 入队

c 重复上述过程,直到整个集合被我们扫描完毕,队列中恰好就保存了前K个值

随着堆顶元素的不断交换,会把堆顶元素不断变小,最终队列扫描结束,就存放了最小 的k个数

public int[] smallestK(int[] arr, int k) {
    int[] ret = new int[k];
    if(arr.length == 0 || k == 0){
        return ret;
    }
    //JDK默认的最小堆,改为最大堆
    Queue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2-o1);
    for(int i = 0; i < arr.length; i++){
        if(i < k){
            queue.offer(arr[i]);
        }else{
            if(arr[i] < queue.peek()){
                queue.poll();
                queue.offer(arr[i]);
            }
        }
    }
    int i = 0;
    while(!queue.isEmpty()){
        ret[i++] = queue.poll();
    }
    return ret;
}          

o1,o2 -> o2-o1

所谓的内部类,就是一个类嵌套到另一个类的内部操作。

匿名内部类:

Queue<Integer> queue = new PriorityQueue<>(new Comparactor<Integer>()){
    public int compare(Integer o1,Integer o2){
        return o2-o1;
    }
}

Queue<Integer> queue = new PriorityQueue<>((o1,o2) -> o2-o1);

Leetcode-347 前K个高频元素

/**
 * 前 K 个高频元素
 */
public class Leetcode_347 {
    public int[] topKFrequent(int[] nums, int k) {
        //要返回的数组
        int[] ret = new int[k];
        //1 扫描原数组,把元素的值以及出现的频次存储到Map集合中
        Map<Integer,Integer> map = new HashMap<>();
        for(int i : nums){
            //map集合中存在这个 i
            if(map.containsKey(i)){
                //找到key->i 所对应的 频次,进行+1
                int num = map.get(i) + 1;
                map.put(i,num);
            }else {//此时map集合中没有这个元素i,
                //添加元素
                map.put(i,1);

            }
        }
        //2 扫描Map集合,将出现频次最高的前K个元素放入优先级队列

        //使用最小堆,这样根节点保存的永远都是 堆中频次最小的元素
        //堆中传入自定义的类Arr,类中存储 元素以及元素出现的频次
        Queue<Arr> queue = new PriorityQueue<>();
        //遍历map集合
        for(Map.Entry<Integer,Integer> entry : map.entrySet()){
            //堆内元素小于k时,直接入队
            if(queue.size() < k){
                queue.offer(new Arr(entry.getKey(), entry.getValue()));
            }else {//此时堆内已经存储了 k 个元素
                //map集合中的频次 和堆内的根节点的频次进行比较
                //大于堆内根节点,就进行替换,否则继续进行比较
                if(entry.getValue() > queue.peek().val){
                    queue.poll();
                    queue.offer(new Arr(entry.getKey(), entry.getValue()));
                }
            }
        }
        //遍历优先级队列,输出
        int i = 0;
        while (!queue.isEmpty()){
            ret[i++] = queue.poll().key;
        }
        return ret;
    }
}
class Arr implements Comparable<Arr>{
    //存储的元素
    int key;
    //元素出现的频次
    int val;
    public Arr() {
    }

    public Arr(int key, int val) {
        this.key = key;
        this.val = val;
    }

    @Override
    public int compareTo(Arr o) {
        return this.val - o.val;
    }
}

Leetcode-373 查找最小的K对数字

import java.util.*;
/**
 * 查找和最小的 K 对数字
 */
public class Leetcode_373 {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        Queue<Pair> queue = new PriorityQueue<>((o1, o2) -> (o2.n1+ o2.n2) - (o1.n1+ o1.n2));
        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 sum = nums1[i] + nums2[j];
                    Pair test = queue.peek();
                    if(sum < test.n1+ test.n2){
                        queue.poll();
                        queue.offer(new Pair(nums1[i], nums2[j]));
                    }
                }
            }
        }
        //此时队列中已经存储了k对和最小的数字
        List<List<Integer>> list = new LinkedList<>();
        while (!queue.isEmpty()){
            List<Integer> list1 = new LinkedList<>();
            Pair pair = queue.poll();
            list1.add(pair.n1);
            list1.add(pair.n2);
            list.add(list1);
        }
        return list;
    }
}
class Pair{
    //第一个数组的值
    int n1;
    //第二个数组的值
    int n2;

    public Pair() {
    }

    public Pair(int n1, int n2) {
        this.n1 = n1;
        this.n2 = n2;
    }
}

Leetcode-692 前K个高频单词

核心:频次不同时,比较大小时比较次数使用最小堆的比较方法,频次相同时,按照字典码排序,使用最大堆

import java.util.*;

//前K个高频单词
public class Leetcode_692_MaxWord {
    public List<String> topKFrequent(String[] words, int k) {
        Map<String,Integer> map = new HashMap<>();
        for(String x : words){
            if(map.containsKey(x)){
                map.put(x,map.get(x)+1);
            }else {
                map.put(x,1);
            }
        }
        //按照频次比较,就是使用最小堆,当频次相同时,保存字节码小的,就采用最大堆排序
        Queue<word> queue = new PriorityQueue<>();
        for(Map.Entry<String,Integer> entry : map.entrySet()){
            if(queue.size() < k){
                queue.offer(new word(entry.getKey(),entry.getValue()));
            }else {
                if(queue.peek().num < entry.getValue()){
                    queue.poll();
                    queue.offer(new word(entry.getKey(),entry.getValue()));
                }
                else if(queue.peek().num == entry.getValue()){
                    if(queue.peek().words.compareTo(entry.getKey())>0){
                        queue.poll();
                        queue.offer(new word(entry.getKey(),entry.getValue()));
                    }
                }
            }
        }
        List<word> list = new ArrayList<>();
        while (!queue.isEmpty()){
            list.add(queue.poll());
        }
        List<String> list1 = new ArrayList<>();
        for(int i = list.size()-1; i >= 0; i--){
            list1.add(list.get(i).words);
        }
        return list1;
    }
}
class word implements Comparable<word>{
    String words;
    int num;

    public word(String words, int num) {
        this.words = words;
        this.num = num;
    }
	//比较
    @Override
    public int compareTo(word o) {
        int i = this.num - o.num;
        if(i == 0){
            return o.words.compareTo(this.words);
        }
        return i;
    }
}

要是对大家有所帮助的话,请帮我点个赞吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值