优先队列源码分析及相关算法题

1. 优先队列

优先队列其实也是一种队列,只不过和普通的队列不同的是,优先队列的出队顺序是按照优先级来的。Java中,优先队列其实就是一个可以自动调整的最小堆,优先队列PriorityQueue是通过堆实现的,堆就是一个完全二叉树,并且堆中所有的结点必须大于等于(或者小于等于)子节点,前者是最大堆,后者是最小堆,java中默认使用的是最小堆,也就是说,二叉树的根节点是最小的元素,如果想要用最大堆,只需new对象的时候传入一个比较器。另外,需要知道的是,使用优先队列的时候,当你使用poll()方法取出跟结点后,堆会自动调整为最小/最大堆。排序算法中的堆排序也是利用最大堆完成的,只不过是自己手写的堆,没有用java中的PriorityQueue。

1.1 源码分析

java中的优先队列,使用数组进行存储的,因为优先队列其实是一个堆,堆又是一个完全二叉树,而完全二叉树存放到数组中是有规律的,一个索引为n的结点,它的左右孩子的索引分别为2 x n - 1、2 x n + 1

 //数组默认大小为11
 private static final int DEFAULT_INITIAL_CAPACITY = 11;
 transient Object[] queue; 

构造函数
主要看有两个参数的构造器,因为无参的构造器和一个参数的构造器都是调用的带两个参数的构造器

//initialCapacity:指定的数组大小
//comparator:比较器对象,java中默认使用的是最小堆,如果想要使用最大堆的话,
//自己实现比较器接口,创建对象时传入构造器对象
 public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

另外创建PriorityQueue时,还可以传入一个容器对象或者PriorityQueue的子类对象,PriorityQueue会将元素存到自己的数组中,并且会自动调整为最小/最大堆,调整主要是靠heapify方法实现。

 public PriorityQueue(Collection<? extends E> c) {
        if (c instanceof SortedSet<?>) {
            SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
            this.comparator = (Comparator<? super E>) ss.comparator();
            initElementsFromCollection(ss);
        }
        else if (c instanceof PriorityQueue<?>) {
            PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
            this.comparator = (Comparator<? super E>) pq.comparator();
            initFromPriorityQueue(pq);//转移元素并且调整堆为最小堆/最大堆
        }
        else {
            this.comparator = null;
            initFromCollection(c); //转移元素并且调整堆为最小堆/最大堆
        }
    }

add()方法

 public boolean add(E e) {
        return offer(e);
    }

offer()方法

  public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        modCount++;
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
        size = i + 1;
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }

offer()方法在添加元素之前会判断是否需要对数组进行扩容,并且如果数组中没有元素的话,就直接加到数组中,如果有元素的话,会调用siftUp()方法

siftUp()方法

private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            siftUpComparable(k, x);
    }

如果创建PriorityQueue对象时,如果没有传入比较器,会调用siftUpComparable(k, x)

siftUpComparable(k, x)方法
这个方法主要是将要添加的元素和父节点作比较,如果大于父节点的话,就结束循环,将这个元素放大数组索引为k下,如果小于父节点的话,就把父节点放到k下,然后,继续和父节点的父节点比较,直到比完或者父节点小于这个元素才停止,最后将这个元素放到合适位置(最后一个大于该元素的长辈节点的位置)。比较时,是从下往上的,所以叫siftUp。

//k是将要添加的元素对应的数组索引
 private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

heapify()
这个方法调整一个完全二叉树为最小/最大堆,从最后一个父节点开始,保证每一个子树根节点都是最小/最大的。

private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

siftDown()方法

private void siftDown(int k, E x) {
        //如果创建PriorityQueue对象时传入比较器,就调用siftDownUsingComparator(k, x)
        //没有传入比较器,就调用siftDownComparable(k, x);
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

siftDownComparable()方法
这个方法是从上(索引为k,值为x的结点开始)往下调整,保证根节点是最大/最小值,从x这个结点的左右孩子结点中选一个最大/最小值(主要是看ComareTo方法是怎么定义的),假设是最小堆调整,选出最小的之后,和x这个结点比较,如果比x结点小的话,就把这个结点放到x结点的位置上,之后,再让x结点和最小孩子结点的子节点比(因为互换了位置,所以子树要重新调整),重复上述过程。

 private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        //half刚好比最后一个父节点的索引大
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            //x结点的左孩子索引
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            //选出最大/最小的孩子结点
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            //与x结点比较,如果比x结点小/大,就结束循环
            if (key.compareTo((E) c) <= 0)
                break;
            //x结点比选出的最大或者最小孩子小或者大的话,就互换
            queue[k] = c;
            //把互换的孩子结点的索引赋给k,再次比较
            k = child;
        }
        //最后把结点放到合适位置
        queue[k] = key;
    }

poll()方法
从队列中取出元素,会将最后一个元素取出,放到根结点,然后调用siftDown方法进行调整

public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);
        return result;
    }

2. 相关题目

2.1 随时找到数据流的中位数

题目

有一个源源不断地吐出整数的数据流,假设你有足够的空间来 保存吐出的数。请设计一个名叫MedianHolder的结构, MedianHolder可以随时取得之前吐出所有数的中位数。

要求

  1. 如果MedianHolder已经保存了吐出的N个数,那么任意时刻 将一个新数加入到MedianHolder的过程,其时间复杂度是 O(logN)。
  2. 取得已经吐出的N个数整体的中位数的过程,时间复杂度为 O(1)。

解题思路
题目要求可以随时取得中位数,可以使用两个优先队列,一个使用最大堆,一个使用最小堆,假设数排好序,最大堆中存前半部分的数,最小堆存后半部分的数,这样,中位数要么是两个堆中根节点的平均值,要么是两个堆中的其中一个。

那么问题就是,怎么保证两个堆中的数个数不超过1?
假如一个数字来了,先将这个数字放到最大堆中,然后如果来了比刚那个数大的数,就放在最小堆中,再来一个数,与最大堆中的数比较,小于的话,就放在最大堆中,如果再来一个数也是小于的话,也放在最大堆中,此时,判断两个堆中的元素个数,最大堆中的元素个数比最小堆中多了两个,这时候,就把最大堆中的根节点放在最小堆中,保证元素个数不超过1,最大堆会自己调整,重新元素位置再成为一个最大堆来.同理,当最小堆中元素个数比最大堆中多两个及两个以上时,就把根节点的元素放在最大堆中。

代码

public class Code_01_MedianHolder {
    
    public static class MedianHolder  {
        private PriorityQueue<Integer> maxHeap;
        private PriorityQueue<Integer> minHeap;
        public MedianHolder(){
            maxHeap = new PriorityQueue(new MaxHeapComparator());
            minHeap = new PriorityQueue();
        }
        //添加元素
        public void addNumber(int i){
           if(maxHeap.isEmpty()){
               maxHeap.add(i);
               return;
           }
		   //如果最大堆根节点小于这个数,就把数放在最小堆中,否咋放在最大堆中
           if(maxHeap.peek() < i){
                minHeap.offer(i);
            }else{
                maxHeap.offer(i);
            }
            //调整两个堆中元素个数
            modifySize(maxHeap,minHeap);
        }
        //调整堆中元素个数
        public void modifySize(PriorityQueue maxHeap, PriorityQueue minHeap){
            if(maxHeap.size() == minHeap.size() + 2){
                minHeap.add(maxHeap.poll());
            }
            if(minHeap.size() == maxHeap.size() + 2){
                maxHeap.add(minHeap.poll());
            }
        }

        //获得中位数
        public Integer getMedium(){
           int maxHeapSize = maxHeap.size();
           int minHeapSize = minHeap.size();
           if(maxHeapSize + minHeapSize == 0){
               return null;
           }
           if((maxHeapSize + minHeapSize) % 2 == 0){
               return (maxHeap.poll() + minHeap.poll()) / 2;
           }
           return maxHeapSize > minHeapSize ? maxHeap.peek():minHeap.peek();
        }

    }
    
    //比较器类
    public static class MaxHeapComparator implements Comparator<Integer>{

        @Override
        public int compare(Integer o1, Integer o2) {
            //返回-1,代表前面的数应该放在前面,返回1,代表后面的数应该放在前面
            //返回0,代表两个数相等
            if(o1 > o2){
                return -1;
            }else if(o1 < o2){
                return 1;
            }else{
                return 0;
            }
        }
    }
    

2.2 分金条问题

题目
一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?
例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60. 金条要分成10,20,30三个部分。
如果, 先把长度60的金条分成10和50,花费60 再把长度50的金条分成20和30,花费50,一共花费110铜板。
但是如果, 先把长度60的金条分成30和30,花费60 再把长度30金条分成10和20,花费30,一共花费90铜板。
输入一个数组,返回分割的最小代价。

解题思路
哈夫曼编码贪心策略:使用一个优先队列(最小堆),将数据都放入优先队列中,每次从优先队列中拿两个数,分两次拿,拿的都是最小数,将这两个数的和放入优先队列,重复上述过程,直到队列中只剩 一个数停止,所有取出的元素的和就是全部切割金条的代价。

public class Code_02_LessMoney {
   
    public static int lessMoney(int[] arr){
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
        for (int i : arr) {
            priorityQueue.add(i);
        }
        int temp = 0;
        int res = 0;
        while(priorityQueue.size() != 1){
            temp = priorityQueue.poll() + priorityQueue.poll();
            priorityQueue.add(temp);
            res = res + temp;
        }
        return res;
    }
}

2.3 最大钱数

输入:
参数1,正数数组costs
参数2,正数数组profits
参数3,正数k
参数4,正数m
costs[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
k表示你不能并行、只能串行的最多做k个项目
m表示你初始的资金
说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个 项目。
输出:你最后获得的最大钱数。

解题思路
定义一个小根堆,一个大跟堆,将所有的项目按花费存到小根堆中,然后将小于初始资金的全部项目按利润放到大跟堆中,那么大跟堆中的根节点就是在初始资金内能获得的最大利润,之后,初始资金增加,重复上述过程,最后获得的最大钱数就是做完最后一个项目之后的手里的资金。

代码

public class Code_03_IPO {
  
    //定义一个Node结点,封装项目花费和利润,便于计算
    public static class Node{
        public int c;
        public int p;
        public Node(int c, int p){
            this.c = c;
            this.p = p;
        }
    }
    //比较器
    public static class MinCostComparator implements Comparator<Node>{

        @Override
        public int compare(Node o1, Node o2) {
            //返回结果小于0,说明o1应该在前面
            //大于0,说明o2应该在前面
            //等于0,说明o1、o2相等
            return o1.c - o2.c;
        }
    }

    public static class MaxProfitComparator implements Comparator<Node>{

        @Override
        public int compare(Node o1, Node o2) {
           return o2.p - o1.p;
        }
    }

    public static int findMaxMoney(int[] costs, int[] profits, int k, int m){
       //将每个项目的花费和利润封装到Node结点中
        Node[] nodes = new Node[costs.length];
        for(int i = 0; i < costs.length; i++){
            nodes[i] = new Node(costs[i], profits[i]);
        }
        //创建基于花费的小根堆和基于利润的大跟堆
        PriorityQueue<Node> minCostQueue = new PriorityQueue<>(new MinCostComparator());
        PriorityQueue<Node> maxProfitQueue = new PriorityQueue<>(new MaxProfitComparator());
        //将所有项目存到小根堆中
        for(Node node : nodes){
            minCostQueue.offer(node);
        }
        /*从小根堆中取出所有花费小于m的项目,放到大根堆中,从大跟堆中取出利润最大的项目
           外层循环的是项目数,内层循环将所有小于花费的项目存到大根堆中
         */
       for(int i = 0; i < k; i++){
           while(!minCostQueue.isEmpty() && minCostQueue.peek().c < m){
               maxProfitQueue.offer(minCostQueue.poll());
           }
           if(maxProfitQueue.isEmpty()){
               return m;
           }
           m = m + maxProfitQueue.poll().p;
       }
        return m;
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章