优先级队列(最大、最小堆)总结


前言

主要讲述通过优先级队列构造最大最小堆,来解决topK问题。


一、优先级队列

1.优先级队列看起来是一个队列,底层是基于堆实现的。
2.优先级队列可以按照元素间的优先级大小将入队的元素排到属于自己的位置。
3.优先级队列可以按照元素之间的优先级的大小动态顺序出队。
4.不像排序处理的集合元素个数是固定的,它处理元素个数是动态变化的,有进有出。

二、与普通队列的对比

普通队列(FIFO)优先级队列
实现基于链表基于堆
入队的时间复杂度O(N)O(logN)
出队(出最大值)的时间复杂度O(N)O(logN)

一般若时间复杂度是O(logN)级别的,则大概率是与“树”结构相关。

三、优先级队列的实现(最大堆)

1.最大堆的实现

public class MaxHeap {
    List<Integer> data;

    /**
     * 默认创建大小为10的堆
     */
    public MaxHeap() {
        this(10);
    }

    /**
     * 创建指定大小的堆
     * @param len
     */
    public MaxHeap(int len) {
        data = new ArrayList<>(len);
    }

    /**
     * 将数组arr转化为最大堆
     * @param arr
     */
    public MaxHeap(int[] arr) {
        if (arr == null || arr.length == 0) {
            return;
        }
        data = new ArrayList<>();
        for (int num : arr) {
            data.add(num);
        }
        // 从最后一个非叶子节点开始,对每个非叶子节点做下沉操作
        int k = (data.size() - 1 - 1) >>1; // 最后一个非叶子节点的编号
        while (k >= 0) {
            siftDown(k);
            k --;
        }
    }

    /**
     * 判断堆是否为空
     * @return
     */
    public boolean isEmpty() {
        return data.size() > 0;
    }

    /**
     * 返回k的父结点编号
     * @param k
     * @return
     */
    public int parent(int k) {
        return (k - 1) >> 1;
    }

    /**
     * 返回左孩子的编号
     * @param k
     * @return
     */
    public int leftChild(int k) {
        return (k << 1) + 1;
    }

    /**
     * 返回右孩子的编号
     * @param k
     * @return
     */
    public int rightChild(int k) {
        return (k << 1) + 2;
    }

    /**
     * 向堆中添加值为val的元素
     * @param val
     */
    public void add(int val) {
        // 先将元素添加至末尾
        data.add(val);
        // 对元素进行上浮操作
        siftUp(data.size() - 1);
    }

    /**
     * 元素的上浮操作
     * @param k
     */
    public void siftUp(int k) {
        while (k > 0 && data.get(k) > data.get(parent(k))) {
            swap(k, parent(k));
            k = parent(k);
        }
    }

    /**
     * 交换两元素位置
     * @param k1
     * @param k2
     */
    public void swap(int k1, int k2) {
        int tmp = data.get(k1);
        data.set(k1, data.get(k2));
        data.set(k2, tmp);
    }

    /**
     * 取出堆中最大值
     * @return
     */
    public int extraMax() {
        if (isEmpty()) {
            throw new NoSuchElementException("堆为空!");
        }
        int ret = data.get(0);
        // 将最后一个叶子结点顶上根结点位置然后做下沉操作
        int index = data.size() - 1;
        swap(0, index);
        data.remove(index);
        siftDown(0);
        return ret;
    }

    /**
     * 显示最大值
     * @return
     */
    public int peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("堆为空!");
        }
        return data.get(0);
    }

    /**
     * 元素的下沉操作
     * @param k
     */
    public void siftDown(int k) {
        // 存在子树
        while (leftChild(k) < data.size()) {
            int j = leftChild(k);
            // 判断是否存在右树且右树的值大于左树
            if (j + 1 < data.size() && data.get(j) < data.get(j + 1)) {
                j = j + 1;
            }
            if (data.get(j) > data.get(k)) {
                swap(j, k);
                k = j;
            } else {
                return;
            }
        }
    }

    public String toString() {
        return data.toString();
    }
}

2.优先级队列的实现

基于最大堆的优先级序列,值越大优先级越高

// 自定义队列接口,设定队列基本方法
public interface Queue<T> {
    // 入队
    void offer(T val);
    // 出队
    T poll();
    // 堆顶元素
    T peek();
    // 判空
    boolean isEmpty();
}
// 实现优先级队列(最大堆的实现)
public class MyPriorityQueue implements Queue<Integer> {
    private MaxHeap heap;
    public MyPriorityQueue() {
        heap = new MaxHeap(); // 创建最大堆
    }

    @Override
    public void offer(Integer val) {
        heap.add(val); // 向最大堆中添加元素
    }

    @Override
    public Integer poll() {
        return heap.extraMax(); // 使用最大堆中的去除最大值的方法来弹出堆顶元素
    }

    @Override
    public Integer peek() {
        return heap.peek(); // 获取堆顶元素的值
    }

    @Override
    public boolean isEmpty() {
        return heap.isEmpty();
    }
}

四、优先级队列的应用

JDK中的优先级队列默认是最小堆的实现,队首元素是当前队列的最小值
应用:
1.基于最大堆实现优先级序列出队得到一个非递增的序列
2.基于最小堆实现优先级序列出队得到一个非递减的序列
topK问题(即最*的K个元素)一般都可以用优先级队列实现

1.创建优先级队列

Queue<元素类型> queue = new PriorityQueue<>(new Comparator<元素类型>() {
					// 使用Comparator比较器自定义优先级的设定,若不使用JDK默认最小堆实现的优先级序列
					// 默认是 o1的 - o2的 实现为最小堆,反过来减就是最大堆
            @Override
            public int compare(元素类型 o1, 元素类型 o2) {
                return 0; // 自定义优先级,需要依据情况定义
            }
        });

2.使用优先级队列

queue.offer(元素类型 ele); // 向队列按设定的中添加元素
queue.poll(); // 去除堆顶元素
queue.isEmpty(); // 判断优先级队列是否为空
元素类型 ele = queue.peek(); // 获取最优先级队列的最值(堆顶元素)

五、使用优先级队列解决问题

规律:取大用小、取小用大
当让求最大K个元素时用最小堆实现的优先级队列、求最小K个元素时用最大堆实现的优先级队列。

分析:1.取大时,若维护一个最小堆,可以知道当前队列中最小值,若优先级队列中元素个数已经大于K个且当前元素大于堆顶元素(最小值)就可以将堆顶出队、当前元素入队,队列中就保存了前面的所有最大个元素。当遍历完的时候队列中就剩余整个数组的最大K个元素。
2.取小时,从最大堆可以知道最大值,比较后就可以将较小者留在堆中,与上同理。

以下题目均来源于:力扣(LeetCode)

1.面试题 17.14. 最小K个数

题目描述:设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可

提示:

	    0 <= len(arr) <= 100000
	    0 <= k <= min(100000, len(arr))
class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k]; // 使用ret数组来保存最终结果
        // 因为上面提示给出的数据说明传入的数组可能为空所以需要判空
        if (arr == null || arr.length == 0 || k == 0) {
            return ret;
        }
        // 创建优先级队列
        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
            		// 取小用大。JDK中默认的是o1 - o2是最小堆,反着减就可以定义成最大堆
                return o2 - o1;
            }
        });
        // 遍历数组中的所有数
        for (int num : arr) {
            if (queue.size() < k) {
           		// 当queue中还不够K个元素时直接加入就行
                queue.offer(num);
            } else {
            	// 此时queue中元素已经达到了K个,需要与堆顶元素比较留较小者
                if (num < queue.peek()) {
                    queue.poll();
                    queue.offer(num);
                }
            }
        }
        for (int i = 0; i < k; i++) {
       		// 依次弹出堆顶元素可以得到一个递减的元素序列
            ret[i] = queue.poll();
        }
        return ret;
    }
}

2.1046. 最后一块石头的重量

题目描述:有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。

class Solution {
    public int lastStoneWeight(int[] stones) {
        // 最大堆
        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for (int stone : stones) {
            queue.offer(stone);
        }
        // 当优先级队列中有2个及2个以上石头
        while (queue.size() > 1) {
            int a = queue.poll();
            int b = queue.poll();
            if (a != b) {
            	// a是先从最大堆中弹出的所以a的值肯定大于b的值,由将前后之差存入队列中
                queue.offer(a - b);
            }
        }
        if (queue.size() == 0) {
            return 0;
        }
        return queue.poll();
    }
}

3.347. 前 K 个高频元素

题目描述:给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

class Solution {
    // 除了使用比较器Comparator来定义优先级,还可以使用专门的内部类来实现自定义对象的比较,设置优先级
    // 此内部类实现Comparable<>接口,实现compareTo()方法来定义优先级。
    class Freq implements Comparable<Freq> {
        int num;
        int time;

        public Freq(int num, int time) {
            this.num = num;
            this.time = time;
        }


        @Override
        public int compareTo(Freq o) {
            return this.time - o.time;
        }
    }
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            map.put(num, map.getOrDefault(num, 1) + 1);
        }
//        Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
//            @Override
//            public int compare(Integer o1, Integer o2) {
//                return map.get(o1) - map.get(o2);
//            }
//        });
//        for (int num : map.keySet()) {
//            queue.offer(num);
//            if (queue.size() > k) {
//                queue.poll();
//            }
//        }
//        int[] ret = new int[k];
//        for (int i = 0; i < k && !queue.isEmpty(); i++) {
//            ret[i] = queue.poll();
//        }
//        return ret;

        // 使用内部类
        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 {
                if (map.get(entry.getKey()) > queue.peek().time) {
                    queue.poll();
                    queue.offer(new Freq(entry.getKey(), entry.getValue()));
                }
            }
        }
        int[] ret = new int[k];
        for (int i = 0; i < k && !queue.isEmpty(); i++) {
            ret[i] = queue.poll().num;
        }
        return ret;
    }
}

根据不同的需求,配置不同的比较器

Comparator相对于Comparable的优点:Comparator相较于Comparable来说更加灵活无需修改哟啊比较类的代码,是一种无侵入模式
补充:sort(需要排序的序列, 排序策略)可以对序列实现特定的排序。排序策略可以是规定排序的方式的专门类(实现Comparable接口)的对象。

4.373. 查找和最小的 K 对数字

题目描述:给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。

请找到和最小的 k 个数对 (u1,v1), (u2,v2) … (uk,vk) 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-k-pairs-with-smallest-sums
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

class Solution {
		// 自定义一个数对类,方便保存值,和计算
    private class Pair {
        int a;
        int b;

        Pair(int a, int b) {
            this.a = a;
            this.b = b;
        }
        
        int sum() {
            return this.a + this.b;
        }
    }
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        // 最大堆
        Queue<Pair> queue = new PriorityQueue<>(new Comparator<Pair>() {
            @Override
            public int compare(Pair o1, Pair o2) {
                return (o2.a + o2.b) - (o1.a + o1.b);
            }
        });
        // 由于两个数组都是升序,所以取最小和最多只用遍历每个数组的 前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 sum = nums1[i] + nums2[j];
                    if (sum < queue.peek().sum()) {
                        Pair pair = new Pair(nums1[i], nums2[j]);
                        queue.poll();
                        queue.offer(pair);
                    }
                }
            }
        }
        // 注意这里不能写为List<List<Integer>> ret = new ArrayList<>(queue);这样会得到一个二叉树层序遍历的结果而不是一个排序后的结果
        List<List<Integer>> ret = new ArrayList<>();
        while (!queue.isEmpty() && ret.size() <= k) {
            List<Integer> list = new ArrayList<>();
            Pair pair = queue.poll();
            list.add(pair.a);
            list.add(pair.b);
            ret.add(list);
        }
        return ret;
    }
}

总结

1.遇到topK问题使用优先级队列解决,时间复杂度为O(NlogK)。
2.学会使用内部类作为辅助工具。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值