【Java源码】PriorityQueue源码剖析及其应用

来源:互联网 发布:火影忍者ol先攻的算法 编辑:程序博客网 时间:2024/05/22 05:54

第0部分 本文概要

这一篇文章说说PriorityQueue, 之前只知道其底层维护着一个小根堆。

An unbounded priority queue based on a priority heap. The elements of the priority queue are ordered according to their natural ordering, or by a Comparator provided at queue construction time, depending on which constructor is used.

Java8 API 文档里说 PriorityQueue 是一个无界的优先堆结构。 元素的顺序是按照自然序, 或者通过构造时候设置Comparator比较器来决定是大顶堆还是小顶堆, 这个取决于构造函数的选择。


A priority queue does not permit null elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so may result in ClassCastException).

此外, PriorityQueue 不允许 null 元素, 可能会抛出 ClassCastException类转换异常的错误。


Note that this implementation is not synchronized. Multiple threads should not access a PriorityQueue instance concurrently if any of the threads modifies the queue. Instead, use the thread-safe PriorityBlockingQueue class.

记住:该实现是非线程安全的, 可以使用线程安全的实现类—— PriorityBlockingQueue

  • 第0部分 本文概要
  • 第1部分 类属性和方法
    • 1 add方法
    • 2 peek 出队不删除元素
    • poll 出队 删除最小元素
    • 4 removeAt 删除堆中某一个节点
    • 5 clear 清除
    • 6 contains 是否包含
    • 7 indexOfObject o 查找
  • 第2部分 TopK 应用

第1部分 类&属性和方法

//默认初始化大小 11private static final int DEFAULT_INITIAL_CAPACITY = 11;  //堆  private transient Object[] queue;  //当前大小  private int size = 0;  //比较器  private final Comparator<? super E> comparator;  //修改次数(增、删、改、查)  private transient int modCount = 0;  //增加  public boolean add(E e)   //出队(不删除)  public E peek()  //出队(删除)  public E poll()  //删除  public boolean remove(Object o)  //是否包含某元素  public boolean contains(Object o)  //清空  public void clear()  //扩容  private void grow(int minCapacity)  //查找  private int indexOf(Object o)  

1.1 add()方法

堆在增加元素后,需要进行调整才能维护其最大堆或者最小堆的性质,下面以最小堆为例:

这里写图片描述

  1. 增加元素26,默认是从队尾增加,即直接添加到数组最后。
  2. 下一步需要执行上滤。26首先和39比较,交换位置;再次和父节点30比较, 交换位置; 继续和父节点14比较, 发现比自己(26)小,停止比较和交换。
  3. 整个过程经过了3次比较。

贴出add()源码:

    /**     * Inserts the specified element into this priority      *///可以看出,add方法实际上是全部委托给offer(E)public boolean add(E e) {          return offer(e);      }public boolean offer(E e) {     // 第1步:判空     if (e == null)          throw new NullPointerException();      // 第2步:改变大小和扩容    modCount++;      int i = size;      //检查容量(扩容)      if (i >= queue.length)          grow(i + 1);      //改变size      size = i + 1;      // 第3步:添加元素并上滤    if (i == 0)          queue[0] = e;//无父节点 ,直接赋值      else          siftUp(i, e);//有父节点,需要上滤      return true;  }  

增加元素的关键的步骤是:grow()siftUp(),下面贴下代码:

/*** 1:扩容方式是:当前队列大小queue.length<64,则增加一倍容量;反之则增加一半容量。* 2:调用Arrays的copyOf函数 ,实际上调用了该函数*/    /**     * Increases the capacity of the array.     *     * @param minCapacity the desired minimum capacity     */    private void grow(int minCapacity) {        int oldCapacity = queue.length;        // Double size if small; else grow by 50%        int newCapacity = oldCapacity + ((oldCapacity < 64) ?                                         (oldCapacity + 2) :                                         (oldCapacity >> 1));        // overflow-conscious code        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        queue = Arrays.copyOf(queue, newCapacity);    } // siftUp()://根据不同的比较方式,采取不同比较策略。下面以使用默认comparable的方式分析private void siftUp(int k, E x) {      if (comparator != null)          siftUpUsingComparator(k, x);      else          siftUpComparable(k, x);  }  // 经典的堆 shiftUp 操作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;  }  

1.2. peek() 出队(不删除元素)

// 这个很简单,只是取出了其中的首位元素,但是并没有删除,不需要调整堆。public E peek() {      if (size == 0)          return null;      return (E) queue[0];  }  

3. poll() 出队 删除最小元素

出队过程:

这里写图片描述

  1. 堆顶元素(队首元素)移出, 最后一个叶子节点39 交换到队首。

  2. 下滤过程: 将39的两个子节点 26 和 20 比较大小, 选择较小的 20 ,并与39进行交换;接着将39和22 和 21 比较, 发现39大于22 同时大于 21, 因此选取22 和 21 中的较小值 21 与 39 再次交换;

    注意:如果孩子结点为null或者都比39大,则结束。

  3. 至此, 经过4次比较, 2次交换, 完成出队后再调整。

poll()源码如下:

public E poll() {      // 优先队列为空,返回null      if (size == 0)          return null;      int s = --size;      modCount++;      // 取出队首      E result = (E) queue[0];      E x = (E) queue[s];      // 队尾赋值为null      queue[s] = null;      // 判断是否执行下滤      if (s != 0)          siftDown(0, x); //核心方法,这里把最后一个元素传进去了,而且添加位置是0    return result;  }private void siftDown(int k, E x) {      if (comparator != null)      siftDownUsingComparator(k, x);      else      siftDownComparable(k, x);  }  /*** k = 0,所以相当于堆顶空降一个元素,然后执行经典的 ShiftDown*/private void siftDownComparable(int k, E x) {        Comparable<? super E> key = (Comparable<? super E>)x;        int half = size >>> 1;        // loop while a non-leaf        while (k < half) {            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];            if (key.compareTo((E) c) <= 0)                break;            queue[k] = c;            k = child;        }        queue[k] = key;    } 

1.4 removeAt() 删除堆中某一个节点

private E removeAt(int i) {      assert i >= 0 && i < size;      modCount++;      int s = --size;      // 如果删除的是最后一个元素,则将最后一个元素设为null    if (s == i)          queue[i] = null;      else {          // 如果删除的不是最后一个元素,取出最后一个元素,并将最后一个元素设为null。执行 ShiftUp        E moved = (E) queue[s];          queue[s] = null;          // 执行 ShiftDown         siftDown(i, moved);          // 如果下滤后元素位置没变,说明moved是该子树最小元素;之后需要执行上滤          // ShiftUp 和 ShiftDown 实际只会执行其中一个          if (queue[i] == moved) {              siftUp(i, moved);              if (queue[i] != moved)// iterator中会用到此处                  return moved;          }      }      return null;  }

1. 5. clear() 清除

    /**     * Removes all of the elements from this priority queue.     * The queue will be empty after this call returns.     */    public void clear() {        modCount++;        for (int i = 0; i < size; i++)            queue[i] = null;//将队列中的每一个元素置为null, 等待GC回收        size = 0;    }

1.6 contains() 是否包含

实际上就是查找过程, 代码逻辑很简单,没得说。

   public boolean contains(Object o) {        return indexOf(o) != -1;//调用1.7 部分内容的indexOf(Object o)    }    //后面有详细注释    private int indexOf(Object o) {        if (o != null) {            for (int i = 0; i < size; i++)                if (o.equals(queue[i]))                    return i;        }        return -1;    }

1.7 indexOf(Object o) 查找

private int indexOf(Object o) {  (o != null) {          //遍历数组查询          for (int i = 0; i < size; i++)              //如果是自定义的元素,重写equals方法是很有必要的              if (o.equals(queue[i]))                  return i;      }      return -1;  }

第2部分 TopK 应用

应用部分已经在之前的【Top K 问题】[Leetcode-215] Kth Largest Element in an Array 数组中第K大的数 一文中已经说过了。

小顶堆解决 Top K 问题的思路:
小顶堆维护当前扫描到的最大100个数,其后每一次的扫描到的元素,若大于堆顶,则入堆,然后删除堆顶;依此往复,直至扫描完所有元素。

Java实现第K大整数代码如下:

public static int findKthLargest(int[] nums, int k) {        PriorityQueue<Integer> myQueue = new PriorityQueue<>(k, new Comparator<Integer>(){            public int compare(Integer o1, Integer o2){                return o1 - o2;//小顶堆//              return o2 - o1;//大顶堆            }        });        for(Integer num: nums){            //如果堆大小小于K, 直接入堆  或者 元素大于小顶堆的堆顶元素,也入堆            if(myQueue.size() < k || num > myQueue.peek() ){                myQueue.offer(num);            }            if(myQueue.size() > k){                myQueue.poll();            }        }        return myQueue.peek();    }