PriorityQueue分析

本文深入解析了PriorityQueue的数据结构和核心方法,包括offer和poll等关键操作的实现原理,以及内部的小顶堆结构如何维护和调整。

PriorityQueue分析

入门小demo

 @Test
    public void testPriorityQueue(){
       PriorityQueue<String> queue = new PriorityQueue<>();
        queue.offer("b");
        queue.offer("b");
        queue.offer("b");
        queue.offer("b");

        queue.offer("b");
        queue.offer("a");
        queue.offer("a");


        int size = queue.size();
        for (int i = 0; i < size; i++) {
            System.out.println(queue.poll());
        }
    }

结果

在这里插入图片描述

PriorityQueue 是一个优先级的队列,支持元素利用Comparable接口来排序。并且不支持存放null。

它基于数组实现了一个小头堆(小头堆意思就是根节点比子节点小,相反,大头堆就是根节点比子节点大。体现在Java里面就是Comparable接口的返回值)每次出队的时候都是队头,并且一直维护小头堆。入队都是在队尾,也一直维护小头堆。

并且基于数组的实现的堆,是一个完全二叉树,因为数组下标总是连续的,存放的时候都是连续的。

简单的介绍就是这样,下面看具体的代码分析。

1. 属性分析

  private static final long serialVersionUID = -7720805057305804111L;
   // 默认的容量,
    private static final int DEFAULT_INITIAL_CAPACITY = 11;

   
  // 利用数组实现堆,并且是小头堆,对于一个节点来说,如果更节点为数组 下标为n的元素,他两个子节点元素的下标的位置为[2*n+1](左子树)和 [2*(n+1)](右子树),如果指定了comparator,就用comparator,如果没有就用自然顺序,
    transient Object[] queue; 

   // 元素的数量
    private int size = 0;
   // 指定的comparator
    private final Comparator<? super E> comparator;

   // 快速失败机制,之前在ArrayList里面说过,这里就不在继续说了
    transient int modCount = 0; 

2. 构造方法分析

从构造方法可以看出,可以指定容量和comparator。并且在构造方法的时候就直接创建出数组了,不像ArrayList一样,一开始的时候不创建。并且还支持传入一个Collection对象,并且赋值操作,

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

如果参数类型是SortedSet,先赋值comparator,然后将SortedSet,变为数组,赋值给PriorityQueue的queue。同样的,对于PriorityQueue来说,也是这样的操作,对于其他的Collection,先是变为数组后,有一个构建堆的操作。

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

3. 主要的方法分析

offer

  public boolean offer(E e) {
      // 非空检查
        if (e == null)
            throw new NullPointerException();
     // 快速失败
        modCount++;
      // 这扩容居然是在一次操作之前,不像HashMap一样,在操作之后扩容。
        int i = size;
        if (i >= queue.length)
            // 扩容操作,具体会在下面扩容分析里面讲解
            grow(i + 1);
      // size++;
        size = i + 1;
      // 一开始肯定为0,队头元素存放值
        if (i == 0)
            queue[0] = e;
        else
           // 构建堆。
           // i表示要插入的位置,i是上一次队列的长度,也就是这一次要插入的位置的下标。
            siftUp(i, e);
        return true;
    }
siftUp 分析(新增元素的时候构建堆)

如果指定了comparator,就按照指定的comparator来比较。否则就按照默认的,其实这俩方法差不了多少,不过就是比较的部分,一个用的是自己的,一个用的是指定的comparator的。所以,这里值分析一个siftUpComparable方法

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

k表示要插入的位置,x表示要插入位置的值。

  private void siftUpComparable(int k, E x) {
       // 上来先强转,如果说元素没有实现Comparable接口或者它是null,这里肯定会报异常。
        Comparable<? super E> key = (Comparable<? super E>) x;
        // 不能在根节点位置上插入。
        while (k > 0) {
           // 找到要插入位置的父节点。
            int parent = (k - 1) >>> 1;
            // 拿到值
            Object e = queue[parent];
           // 如果说父节点比要插入的key小,那么直接在要插入的位置上插入就可以。否则就说明,要插入的值,比父节点还要小,那就要继续往上找了,一直找到一个符合的节点。
            if (key.compareTo((E) e) >= 0)
                break;
           // 走到这里,就说明不符合,只能继往上找了,继续往上找,肯定能找到一个位置,所以,就得把父节点往下移动移动,
           // 将父节点下移动
            queue[k] = e;
            // 下一次的操作就从父节点开始,继续找,
            k = parent;
        }
     // 赋值操作
        queue[k] = key;
    }

这里的操作看代码不好理解,这里画一个图便于理解,图中的例子和开头的代码例子对应。
在这里插入图片描述

也可以根据这个图来验证一下下标为n的元素,他两个子节点元素的下标的位置为[2*n+1](左子树)和 [2*(n+1)](右子树)这个是否正确。

第一次插入之后,根据上面的逻辑找到要插入节点(下标为1的位置)的父节点,判断父节点是否比要插入的key小,发现不满足,就直接在要插入节点(下标为1的位置)直接插入值就好了。

下面的几个都是这样的逻辑,一直到a插入的时候,发生了变化

在这里插入图片描述

开始插入a之后。

  1. 找到父节点开始判断和比较。

在这里插入图片描述

  1. 发现 不满足条件,这个时候将父节点下移,子节点上移,继续找。

在这里插入图片描述

  1. 一直找,找到满足条件之后放值
    在这里插入图片描述

    自然而然,在插入一个a的话,树应该长下面的这个样?
    在这里插入图片描述

    也产生了父节点下移。子节点上升。

这里就产生了一个问题,在遍历的时候如果按照数组的顺序遍历,很明显,顺序不对。所以,他出队的顺序是怎么样的。这就很有意思了。出队的时候他可是完全按照优先级的顺序的。

poll

将队头元素出队,并且将队尾元素变为null,将之前队尾元素所在下标位置变为null,将队尾元素插入到队头位置。

问题?

  1. 为啥要出队头元素(index=0的元素)

    因为这是一个堆,还是一个小头堆,所以,index=0的元素(队头)就是整个堆中最小的元素。让他出队没有问题

  2. 为啥要将队尾元素插入到index=0的位置。而不是随便的位置

    因为这是一个堆,还是一个小头堆,并且要保持堆的完整性,在第一个位置插入元素之后,会引起整个堆中元素结构的变动。也就是说,会再次调整堆。如果随便插入一个位置,那这个位置上面的部分就不需要调整了?所以,直接从0开始,要的就是要引起整个堆中元素的变动。

  public E poll() {
        if (size == 0)
            return null;
       // --size
        int s = --size;
       // 快速失败
        modCount++;
       // 拿到第一个元素(队头的元素是整个树的根节点,是最小的,每次出队也是出它,这没有问题。)
        E result = (E) queue[0];
      // 拿到队尾元素
        E x = (E) queue[s];
       // 队尾元素变为null
        queue[s] = null;
        if (s != 0)
           //将队尾元素插入到第一个位置(队头)
            siftDown(0, x);
        return result;
    }
siftDown分析

如果指定了comparator,就按照指定的comparator来比较。否则就按照默认的,其实这俩方法差不了多少,不过就是比较的部分,一个用的是自己的,一个用的是指定的comparator的。所以,这里值分析一个siftDownComparable方法

  private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
siftDownComparable分析

k表示要插入的元素的位置,

x表示元素

    private void siftDownComparable(int k, E x) {
       // 还是变为Comparable。
        Comparable<? super E> key = (Comparable<? super E>)x;
       // 停止查找的条件。 size/2 这就是树的高度
        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;
            // right< size 不满足,说明没有右子树。
           // 如果有右子树,并且右子树比左子树小,那么最小的元素就是右子树。
            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;
    }

这里的操作看代码不好理解,这里画一个图便于理解,图中的例子和开头的代码例子对应。

这里开始的图是offer里面的图。

在这里插入图片描述

先要要poll一次。根据上面的代码逻辑,看看会发送什么事情。
在这里插入图片描述

重新插入index为6的元素b

在这里插入图片描述

这个时候发现index为2的元素最小,可是index为6的元素没有它小,那就说明index为2的元素是整个堆中最小的。就需要上移,然后当前插入的位置变为了index为2。继续判断

在这里插入图片描述

最后这个树就变成了下面的这个样子

在这里插入图片描述

同样的,如果在调用一个poll方法,在出队一次,就变成

在这里插入图片描述

4. 扩容分析

只要设计到数组,肯定就有扩容操作。

// 既然是数组,而且还没有链表,那简单,直接拷贝就可了。 
private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // 这里就很清晰了,如果容量太小就就double,否则就增强50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity + 2) :
                                         (oldCapacity >> 1));
        // overflow-conscious code
      //private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 直接拷贝一个新数组就好了,核心方法
        queue = Arrays.copyOf(queue, newCapacity);
    }

  // 这就是一个保险措施。
   private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

别的方法就不分析了,核心说清楚就好了。

关于PriorityQueue的分析就分析到这里了。 如有不正确的地方,欢迎指出。谢谢。

<think>嗯,我现在要分析Java中的PriorityQueue类有哪些方法。PriorityQueueJava集合框架中的一个类,它实现了Queue接口,通常用于实现优先队列数据结构。优先队列中的元素按照自然顺序或者通过提供的Comparator进行排序,队首元素总是最小的(或者根据Comparator定义的顺序)。 首先,我应该回忆一下PriorityQueue的常用方法。记得它继承自AbstractQueue,而AbstractQueue实现了Queue接口。所以,PriorityQueue应该具备Queue的基本方法,比如add、offer、remove、poll、element、peek等。 接下来,我需要确认这些方法的具体作用。比如,add和offer都是添加元素,但可能在不同情况下有不同的返回值或抛出异常的情况。remove和poll都是移除并返回队首元素,但remove在队列为空时抛出异常,而poll返回null。element和peek用于查看队首元素,element在空队列时抛出异常,peek则返回null。 此外,PriorityQueue作为一个集合,应该还有来自Collection接口的方法,比如size、isEmpty、iterator、contains、clear、toArray等。不过需要注意的是,迭代器遍历时不保证元素的顺序,因为优先队列的结构可能使元素不以排序后的顺序被访问。 另外,PriorityQueue特有的方法可能包括构造方法,比如可以指定初始容量和Comparator。还有一些可能的方法比如comparator(),用于返回所使用的比较器,如果使用自然顺序则返回null。 不过,可能还有一些方法容易被忽略。比如,是否包含remove(Object o)这样的方法?因为在Collection接口中有这个方法,PriorityQueue作为其实现类应该也有,但这个方法的时间复杂度可能比较高,因为需要遍历队列来找到元素并删除。 还需要考虑是否有可能的方法如grow(),但这个方法应该是内部使用的,不是公共方法。所以公共方法可能不包含这个。 总结一下,PriorityQueue的方法主要可以分为几类: 1. 插入元素:add(E e), offer(E e) 2. 移除元素:remove(), poll(), remove(Object o) 3. 查看队首元素:element(), peek() 4. 查询:size(), isEmpty(), contains(Object o) 5. 转换:toArray(), toArray(T[] a) 6. 迭代:iterator() 7. 清空队列:clear() 8. 比较器相关:comparator() 9. 其他继承自Object的方法,比如clone()?不过PriorityQueue是否实现了clone方法?需要确认。 现在需要逐个验证这些方法是否存在,并正确描述它们的用途和注意事项。例如,add和offer在PriorityQueue中的区别可能不大,因为PriorityQueue是无界的(默认情况下),所以add和offer都会返回true,但在容量受限的队列中,offer可能在满时返回false,而add会抛出异常。不过PriorityQueue的容量是动态增长的,所以这两个方法的行为可能相同。 另外,构造方法可能包括: - 默认构造方法(初始容量11,自然顺序) - 指定初始容量 - 指定初始容量和比较器 - 通过另一个集合初始化 这些构造方法是否属于方法介绍的范围?用户可能指的是实例方法,而构造方法可能属于类结构的一部分,但有时候会被提及。 关于iterator()方法,需要注意它返回的迭代器不保证按特定顺序遍历元素。如果需要有序遍历,应该使用其他方式,比如依次poll元素。 再比如,PriorityQueue是否允许null元素?根据Java文档,PriorityQueue不允许插入null元素,否则会抛出NullPointerException。 此外,关于线程安全,PriorityQueue不是线程安全的,如果需要在多线程环境中使用,需要外部同步。 现在需要将这些信息组织起来,按照方法分类,描述每个方法的作用、参数、返回值及可能抛出的异常。例如: - add(E e):将元素插入队列,成功返回true。如果元素无法添加可能抛出异常(但PriorityQueue无界,通常不会失败,除非ClassCastException等)。 - offer(E e):同add,返回true。 - remove():移除并返回队首元素,队列空时抛出NoSuchElementException。 - poll():移除并返回队首元素,队列空时返回null。 - element():返回队首元素但不移除,队列空时抛出异常。 - peek():返回队首元素,队列空时返回null。 - size():返回元素个数。 - isEmpty():判断是否为空。 - contains(Object o):是否包含指定元素,时间复杂度O(n)。 - remove(Object o):删除指定元素,存在多个时只删除一个,返回是否成功。 - clear():清空队列。 - comparator():返回使用的比较器,自然顺序则返回null。 - toArray():返回包含所有元素的数组,顺序不保证。 - iterator():返回迭代器,遍历顺序不保证。 另外,是否还有其他方法?比如spliterator(),但可能属于Java 8后的特性,但通常用户可能不常用到。 确认Java文档中的方法列表是否有遗漏。例如,在Java 8的PriorityQueue中,确实包含上述方法,此外还有从AbstractQueue继承的如containsAll, addAll等,但PriorityQueue可能覆盖了某些方法,比如addAll可能会有特定的实现。 不过用户可能关心的是PriorityQueue自身的主要方法,而不是继承自更上层接口的通用方法,除非这些方法有特别之处。例如,addAll方法在PriorityQueue中可能用于将另一个集合的元素添加到队列中,并重新进行堆的调整。 综上,应该将这些方法分门别类地介绍,说明其用途和注意事项,比如线程不安全、迭代顺序不保证等。同时,可能需要提到构造方法的一些特点,比如如何指定比较器等。</think>Java中的`PriorityQueue`是基于优先级堆实现的无界优先队列,元素按自然顺序或自定义`Comparator`排序。以下是其主要方法分类介绍: --- ### **1. 插入元素** - **`boolean add(E e)`** 将元素插入队列。成功返回`true`。若元素为`null`抛出`NullPointerException`。 - **`boolean offer(E e)`** 与`add()`功能相同。因队列无界,始终返回`true`(除非发生异常)。 --- ### **2. 移除元素** - **`E remove()`** 移除并返回队首(最小)元素。队列为空时抛出`NoSuchElementException`。 - **`E poll()`** 移除并返回队首元素。队列为空时返回`null`。 - **`boolean remove(Object o)`** 删除指定元素(若存在)。时间复杂度为O(n),需遍历堆结构。 --- ### **3. 查看队首元素** - **`E element()`** 返回队首元素但不移除。队列为空时抛出`NoSuchElementException`。 - **`E peek()`** 返回队首元素,队列为空时返回`null`。 --- ### **4. 查询与遍历** - **`int size()`** 返回队列中元素数量。 - **`boolean isEmpty()`** 判断队列是否为空。 - **`boolean contains(Object o)`** 检查是否包含指定元素。时间复杂度O(n)。 - **`Iterator<E> iterator()`** 返回迭代器,但**不保证按优先级顺序遍历**。有序访问需通过重复调用`poll()`。 --- ### **5. 转换与清空** - **`void clear()`** 清空所有元素。 - **`Object[] toArray()`** 返回包含所有元素的数组,顺序不保证。 - **`<T> T[] toArray(T[] a)`** 将元素存入指定类型数组,若数组空间不足则返回新数组。 --- ### **6. 比较器相关** - **`Comparator<? super E> comparator()`** 返回队列使用的比较器。若按自然顺序排序,返回`null`。 --- ### **其他注意事项** 1. **不允许`null`元素**:插入`null`会抛出`NullPointerException`。 2. **非线程安全**:多线程环境需手动同步,或使用`PriorityBlockingQueue`。 3. **动态扩容**:默认初始容量11,容量不足时自动增长。 4. **构造方法**: - `PriorityQueue()`:默认容量11,自然排序。 - `PriorityQueue(int initialCapacity)`:指定初始容量。 - `PriorityQueue(Comparator<? super E> comparator)`:指定比较器。 - `PriorityQueue(Collection<? extends E> c)`:从集合初始化。 --- ### **示例代码** ```java PriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(5); pq.add(3); pq.offer(7); System.out.println(pq.peek()); // 输出3(队首最小元素) pq.poll(); // 移除3 System.out.println(pq); // 可能输出[5,7](实际顺序依赖堆结构) ``` 通过合理使用这些方法,可以高效管理按优先级处理的元素集合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值