優先隊列源碼分析及相關算法題

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;
    }

}

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