• 优先队列和堆(动态数组实现最大二叉堆,使用最大二叉堆实现优先队列)


    一、优先队列场景
    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叉树,如下图的三叉堆

    七、广义队列

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

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

    作者:Work Hard Work Smart
    出处:http://www.cnblogs.com/linlf03/
    欢迎任何形式的转载,未经作者同意,请保留此段声明!

  • 相关阅读:
    POJ3233]Matrix Power Series && [HDU1588]Gauss Fibonacci
    [codeforces 508E]Maximum Matching
    [SDOI2011]染色
    [CSU1806]Toll
    [HDU4969]Just a Joke
    [HDU1071]The area
    [HDU1724]Ellipse
    [VIJOS1889]天真的因数分解
    [BZOJ3379] Turning in Homework
    [BZOJ1572] WorkScheduling
  • 原文地址:https://www.cnblogs.com/linlf03/p/14399817.html
Copyright © 2020-2023  润新知