work hard work smart

专注于Java后端开发。 不断总结,举一反三。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一、优先队列场景
1、系统中动态选择优先级最高的任务执行
2、医院根据患者的患病情况,选择哪个患者最先做手术。
3、游戏中,士兵去攻击优先级最高的那个敌人。

 

二、优先队列底层数据结构复杂度对比

 

三、堆

1、二叉堆Binary Heap 

使用二叉树表示的堆,二叉堆是一棵完全二叉树

 

 完全二叉树: 把元素顺序排列成树的形状。

二叉堆的性质

    堆中某个节点的值总是不大于其父节点的值。

 最大堆,父节点总是大于孩子节点值(相应的可以定义最小堆)

 

2、用数组存储二叉堆

数组索引从1开始存储

 

 

 

 

 父亲节点和孩子节点的索引关系

parent(i) = i/2

left child(i) = 2 * i;

right child(i) = 2 * i +1

 

数组索引从0开始存储

 

 

 

 

父亲节点和孩子节点的索引关系

parent(i) = (i-1)/2

left child(i) = 2 * i + 1;

right child(i) = 2 * i +2

 

2.1 堆的基础表示

元素E extends Comparable<E>,说明元素是可以比较大小的。

public class MaxHeap<E extends  Comparable<E>> {

    private CustomArray<E> data;

    private  MaxHeap(int capacity){
        data = new CustomArray<E>(capacity);
    }

    private  MaxHeap(){
        data = new CustomArray<E>();
    }

    // 返回堆中的元素个数
    public int size(){
        return  data.getSize();
    }

    //返回一个布尔值,表示堆中是否为空
    public  boolean isEmpty(){
        return  data.isEmpty();
    }

    //返回完全二叉树的数组表示,一个索引所表示的元素的父亲节点的索引
    private int parent(int index){
        if(index == 0){
            throw new IllegalArgumentException("index-0 doesn't have parent");
        }
        return  (index - 1 ) / 2;
    }

    //返回完全二叉树的数组表示,一个索引所表示的元素的左孩子节点的索引
    private int leftChild(int index){
        return  index * 2 + 1;
    }

    //返回完全二叉树的数组表示,一个索引所表示的元素的右孩子节点的索引
    private int rightChild(int index){
        return  index * 2 + 2;
    }
}

  

 

2.2 向数组中添加元素

加入已经有10个元素了,现在加入第11个节点52,我们把52放在index为10的数组里。

 

 然后index=10和它的父亲index=4进行比较,可以发现52大于16,根据最大堆的定义,52和16交互位置,交换后如下图所示:

 

  然后index=4和它的父亲index=1进行比较,可以发现52大于41,根据最大堆的定义,52和41交互位置,交换后如下图所示:

 

 然后index=1和它的父亲index=0进行比较,可以发现52小于62,根据最大堆的定义,52和62不用交换位置。这样插入节点52的完成就完成了,整个过程叫Sift up(元素的上浮)

 

代码实现:

  //向堆中添加元素
    public  void add(E e){
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    private void siftUp(int k){
        while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
            data.swap(k, parent(k));
            k = parent(k);
        }
    }

  

swap是动态数组CustomArray中新增的方法

    //交互索引为i和j的元素值
    public void swap(int i, int j){
        if(i < 0 || i >= size || j < 0 || j >= size){
            throw  new IllegalArgumentException("Index is illegal.");
        }
        E t = data[i];
        data[i] = data[j];
        data[j] = t;
    }

  

 2.3 向数组中取出元素

取出元素只能取出最大的元素,这里为62

 

 

 

取出62之后,如下图所示。有两棵子树,将两棵子树融合成一棵树,还是比较复杂的

 

这里我们使用一个小技巧,把堆中最后一个元素放在堆顶。把最后一个原素删除

 

 现在要把堆顶元素16往下调,这个过程叫Sift Down。选择两个孩子元素中最大的元素进行交换。这里16的孩子为52和30, 52比30大,那么16和52进行对调。

调整后如下图所示。

 

 对于16的新的位置,可能还是不满足最大堆的性质,要继续下沉下去

16的最大孩子的元素为41,那么16和41进行交换,交换后,如下图所示

 

  对于16的新的位置,可以发现它只有左孩子,而且16比左孩子9大,这样就不用交换了。下沉操作结束。

 

代码实现:

 // 查看堆中最大的元素
    public E findMax(){
        if(data.getSize() == 0){
            throw  new IllegalArgumentException("Can not findMax when heap i");
        }
        return  data.get(0);

    }

    //取出堆中最大的元素
    public E extractMax(){
        E ret = findMax();
        //交互第一个元素和最后一个原素
        data.swap(0, data.getSize() -1);
        //删除最后一个原素
        data.removeLast();
        siftDown(0);
        
        return ret;
    }

    private void siftDown(int k) {
        //如果k不是叶子节点
        while (leftChild(k) < data.getSize()){
            // 找出索引k中左右孩子中最大孩子的索引
            int j = leftChild(k);
            //如果有右孩子 并且右孩子比左孩子大
            if(j +1 < data.getSize() && data.get(j + 1).compareTo(data.get(j) )> 0){
                j = rightChild(k);
            }
            //此时, data[j] 是leftChild和rightChild中的最大值
            if(data.get(k).compareTo(data.get(j)) >= 0){
                break;
            }
            data.swap(k, j);
            //交换完成后,将j赋值给k,进行下一轮循环
            k = j;
        }
    }

  

测试:

 public static void main(String[] args) {
        int n = 1000000;
        MaxHeap<Integer> maxHeap = new MaxHeap<Integer>();
        Random random = new Random();
        for(int i = 0; i < n ; i++){
            maxHeap.add(random.nextInt(Integer.MAX_VALUE));
        }

        int[] arr = new int[n];
        for(int i = 0; i < n; i++){
            //从最大到最小进行排列
            arr[i] = maxHeap.extractMax();
        }

        //测试前一个元素比后一个大,否则抛出异常
        for(int i = 1; i < n; i++){
            if(arr[i - 1] < arr[i]){
                throw  new IllegalArgumentException("Error");
            }
        }

        System.out.println("Test MaxHeap completed.");

    }

  

测试结果:

Test MaxHeap completed.

没有抛出异常,说明取出元素正确。

 

2.4 堆的时间复杂度

add和extractMax时间复杂度都是O(logn)

因为堆是完全二叉树,所以它不会成为一个链表。

 

四、基于最大堆实现优先队列

public class PriorityQueue<E extends  Comparable<E>> implements IQueue<E> {

    private MaxHeap<E> maxHeap;

    public PriorityQueue(){
        maxHeap = new MaxHeap<E>();
    }

    public int getSize() {
        return maxHeap.size();
    }

    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }


    public E getFront() {
        return maxHeap.findMax();
    }

    public void enqueue(E e) {
        maxHeap.add(e);
    }

    public E dequeue() {
        return maxHeap.extractMax();
    }
}

  

五、leetcode 中 347. 前 K 个高频元素

https://leetcode-cn.com/problems/top-k-frequent-elements/

题目描述:

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。


示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

 

代码实现:

public class Solution {

    private class Freq implements  Comparable<Freq>{
        //元素
        int e;
        //频率(出现次数)
        int freq;

        public Freq(int e, int freq){
            this.e = e;
            this.freq = freq;
        }

        public int compareTo(Freq another) {
            //频率越小,优先级越高
            if(this.freq < another.freq){
                return  1;
            }else if(this.freq > another.freq){
                return  -1;
            }else {
                return 0;
            }
        }
    }

    // 返回数组nums中,前k个频率最大的元素
    public int[] topKFrequent(int[] nums, int k){
        TreeMap<Integer,Integer> map = new TreeMap<Integer, Integer>();
        for(int num : nums){
            if(map.containsKey(num)){
                map.put(num, map.get(num) + 1);
            }else {
                map.put(num , 1);
            }
        }


        PriorityQueue<Freq> pq = new PriorityQueue<Freq>();
        //算法复杂度 nlogh
        for(int key: map.keySet()){
            //将前k个元素放入优先队列
            if(pq.getSize() < k){
                pq.enqueue(new Freq(key, map.get(key)));
            }
            //如果可以对应的频次大于队首的频次
            else if(map.get(key) > pq.getFront().freq) {
                //队首元素出队(队首元素频率最小,优先级越高)
                pq.dequeue();
                //增加新的元素
                pq.enqueue(new Freq(key, map.get(key)));
            }
        }
        //以上操作之后,队列就是前k个频率最高的元素了。
        int[] arr = new int[pq.getSize()];
        int i = 0;
        while (!pq.isEmpty()){
            Freq f = pq.dequeue();
            arr[i] = f.e;
            i++;
        }
        return  arr;
    }


    public static void main(String[] args) {
        int[] nums = {4,1,-1,2,-1,2,3};  // 4 1次 1 1次  -1 2次  2  2次, 3 1次
        int[] res = new Solution().topKFrequent(nums,2);
        for(int i = 0; i < res.length; i++){
            System.out.print(res[i] + ",");
        }

    }

}

  

 六、d叉堆

d个孩子的完整d叉树,如下图的三叉堆

 

七、广义队列

这里我们学习了优先队列,已经前面的普通队列

栈,也可以理解成是一个队列