Java集合中PriorityQueue 优先队列类源码分析

Java集合中PriorityQueue 优先队列类

1、PriorityQueue类的位置?

我想这个问题就是在讲PriorityQueue 继承了谁?它的父类特点是什么?

在官方的API文档中,我们可以清晰的看到这个结构
在这里插入图片描述

我们可以知道,PriorityQueue类继承了AbstractQueue类,AbstractQueue类继承了AbstractCollection

接下来,看看这张Java集合框架图。
java集合框架图,来源于网络

我们可以看出这个方法实现了Queue接口,同时这个Queue接口继承了Collection接口,所以PriorityQueue类中实现了很多Queue接口的方法与特点。

Queue接口的方法的特点:

​ Queue接口设计用于在处理之前保留元素的集合。 除了基本的Collection之外,队列还提供额外的插入,提取和检查操作。


2、PriorityQueue类有什么方法?

2.1 PriorityQueue是什么?

优先队列的作用是能保证每次取出的元素都是队列中权值最小的 ,该类是基于数组表示的二叉小顶堆实现的。 优先级队列的元素根据它们的默认有序规则,或由一个在队列构造的时候使用的构造方法提供的Comparator进行排序存放。 优先队列不允许null元素。 依靠默认排序的优先级队列也不允许插入不可比较的对象(这样做可能导致ClassCastException )。

​ 现在我们以5~11来举例子,数值在二叉小顶堆中排序,如下图
在这里插入图片描述

​ 他在数组的存放就是如下图所示:

在这里插入图片描述

我们可以看到 8的父节点是6、10的父节点是7,我们可以得到父子节点之间的关系公式为:

左孩子:leftNo = parentNo*2+1
右孩子:rightNo = parentNo*2+2
父节点:parentNo = (nodeNo-1)/2

所以在添加新节点的时候,我们是根据以上三个公式进行位置的更替与排序,以上便是PriorityQueue类的底层数据结构。


2.2 PriorityQueue的方法

首先简单地列出其方法,用思维导图展示,如下图所示。

在这里插入图片描述

2.2.1 构造方法
1、基础构造方法
1.1 PriorityQueue()

创建一个PriorityQueue ,具有默认的初始容量(11),根据它们的默认排序方法对其元素进行排序。

源码如下:

  //默认容量
  private static final int DEFAULT_INITIAL_CAPACITY = 11;
  public PriorityQueue() {
      //传入了容量和Null,不指定具体的排序方法,即成为默认排序方法
        this(DEFAULT_INITIAL_CAPACITY, null);
      //这里调用了默认构造器 
      //public PriorityQueue(int initialCapacity,
        //                 Comparator<? super E> comparator)
      
      
    }

1.2PriorityQueue(int initialCapacity)

创建具有PriorityQueue,具有指定容量initialCapacity,根据它们的默认排序方法对其元素进行排序 。

源码如下:

public PriorityQueue(int initialCapacity) {
    //这里也是调用了默认构造器
    this(initialCapacity, null);
}
2.自定义排序规则的构造器
2.1 PriorityQueue(int initialCapacity,Comparator<? super E> comparator)

创建具有 initialCapacity初始容量的PriorityQueue,根据指定的比较器对其元素进行排序。

值得一提的是:这个构造器是以上两个构造依赖的,通过this()进行调用和参数传递。

源码如下:

//transient关键字是用来指定不需要被序列化 ,定义的queue数组结构是用来实现二叉最小堆的,可以参考之前的分析。
transient Object[] queue; 
//定义比较器
 private final Comparator<? super E> comparator;

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    //定义的容量不能小于1
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    //给queue数组定义容量大小
    this.queue = new Object[initialCapacity];
    //给比较器赋值
    this.comparator = comparator;
}
2.2.PriorityQueue(Comparator<? super E> comparator)

创建具有默认初始容量为11的 PriorityQueue ,并根据指定的比较器对其元素进行排序。

源码如下:

  public PriorityQueue(Comparator<? super E> comparator) {
        this(DEFAULT_INITIAL_CAPACITY, comparator);
    }
3、从其他导入比较器
3.1 PriorityQueue(Collection<? extends E> c)

创建一个Collection集合中的元素的PriorityQueue。 如果指定的集合是一个实例SortedSet或者是另一种PriorityQueue ,这个优先级队列将按照相同的顺序进行排序。 否则,此优先级队列将根据其元素的默认顺序进行排序。

源码:

public PriorityQueue(Collection<? extends E> c) {
    //判断c是否为SortedSet的一个实例
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        //给构造器添上具体的方法
        this.comparator = (Comparator<? super E>) ss.comparator();
        //给PriorityQueue 实例加上ss的数值
        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);
    }
}

你可以看下面的示例,可以帮你好的理解:

 @Test
public void PriorityQueueConstructor(){

		//SortedSet集合的一个实例 ,这里规定的排序为从大到小
     SortedSet<Integer> sortedSet=new TreeSet<>((a,b)->(b-a) );
     sortedSet.add(11);
     sortedSet.add(56);
     sortedSet.add(9);
     sortedSet.add(87);
     System.out.println(sortedSet.toString());//[87, 56, 11, 9]
    //使用构造器,按照SortedSet集合实例中的构造器对里面的元素进行排序
      PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(sortedSet);

      System.out.println(priorityQueue.poll()); //87
      System.out.println(priorityQueue.poll());//56
      System.out.println(priorityQueue.poll());//11
      System.out.println(priorityQueue.poll());//9
      System.out.println(priorityQueue.poll());//null

		//PriorityQueue集合的一个实例,这里规定排序为从到小
     PriorityQueue<Integer> priorityQueue1=new PriorityQueue<>((a,b)->(b-a));
     priorityQueue1.add(5);
     priorityQueue1.add(4);
     priorityQueue1.add(9);
     priorityQueue1.add(1);
   //注意这里输出的是[9, 4, 5, 1],可以思考下为什么是这个 
      System.out.println(priorityQueue1.toString());
    
    ///使用构造器,按照PriorityQueue集合实例中的构造器对里面的元素进行排序
      PriorityQueue<Integer> pq=new PriorityQueue<>(priorityQueue1);

      System.out.println(pq.poll());//9
      System.out.println(pq.poll());//5
      System.out.println(pq.poll());//4
      System.out.println(pq.poll());//1
      System.out.println(pq.poll());//null

		
		//定义ArrayList实例
      Collection<Integer> collection= new ArrayList<>();
      collection.add(10);
      collection.add(34);
      collection.add(11);
      collection.add(1);
      System.out.println(collection.toString());//[10, 34, 11, 1]

      //使用该构造器,并按照默认排序的方式对数值从小到大进行排序
      PriorityQueue<Integer> pq1=new PriorityQueue<>(collection);

      System.out.println(pq1.poll());//1
      System.out.println(pq1.poll());//10
      System.out.println(pq1.poll());//11
      System.out.println(pq1.poll());//34
      System.out.println(pq1.poll());//null

 }

**思考题:**在上面例子中按顺序添加[5,4,9,1] 时,输出的数组顺序为什么是[9, 4, 5, 1]?
在这里插入图片描述

	所以在数组中的顺序变为:9、4、5、1								
3.2public PriorityQueue(SortedSet<? extends E> c)

用于创建一个SortedSet指定排序集中的元素的PriorityQueue。 该优先级队列将按照与给定排序集相同的顺序进行排序。

​ 源码:

    public PriorityQueue(PriorityQueue<? extends E> c) {
        this.comparator = (Comparator<? super E>) c.comparator();
        initFromPriorityQueue(c);
    }

3.3 public PriorityQueue(PriorityQueue<? extends E> c)

用于创建一个PriorityQueue指定排序集合中的元素的PriorityQueue。 该优先级队列将按照与给定排序集相同的顺序进行排序。

源码:

    public PriorityQueue(PriorityQueue<? extends E> c) {
        this.comparator = (Comparator<? super E>) c.comparator();
        initFromPriorityQueue(c);
    }
2.2.2 主要方法
1、插入
  1. add(E e)和offer(E e)

    在PriorityQueue类中没有任何区别,插入成功返回true,如果插入的为null,则返回异常。

    add(E e)offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false

    可以看下源码,我们就知道add()方法其实是调用的offer方法,故两者都没区别。

    public boolean add(E e) {
        return offer(e);
    }
    
        public boolean offer(E e) {
            //元素为null时会抛出异常
            if (e == null)
                throw new NullPointerException();
            //队列中元素数量加1
            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;
        }
    

    siftUp(i, e)方法源码:

       private void siftUp(int k, E x) {
           //有指定比较器的情况下
            if (comparator != null)
                siftUpUsingComparator(k, x);
            else
                //没有比较器,默认优先级情况下
                siftUpComparable(k, x);
        }
    

    siftUpUsingComparator(k, x)的源码,告诉了我们优先级队列是如何对插入数据进行比较的,如下:

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

    我们可以得到在插入数据的时候,要与其父节点进行比较,如果不满足比较器的关系,则交换数据,这个就是我们之前思考题里回答的问题。

2、查询
2.1 查询(首元素)后不删除元素
  • E peek()与E element()

    说明:两者都是查询首元素后不删出元素,不同的是peek()在查不到元素后会返回null,而element()会抛出异常。

    注意:element()方法并不在PriorityQueue类中定义,而是从父类AbstractQueue类中继承而来。

    看他们的源码也很有意思,element也是peek()方法进行了封装。

    在AbstractQueue类中的element()方法的源码:

    public E element() {
        //调用peek()进行判断
            E x = peek();
            if (x != null)
                return x;
            else
                throw new NoSuchElementException();
        }
    

    peek()源码,如下:

       public E peek() {
            return (size == 0) ? null : (E) queue[0];
        }
    
2.2 查询任意元素是否存在,不删除,contains(Object o)方法源码如下:
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

如果存在就返回该元素索引,如果不存就返回-1。indexOf函数也是用的循环,可见对数组的查询,基本就是遍历。

   private int indexOf(Object o) {
        if (o != null) {
            for (int i = 0; i < size; i++)
                if (o.equals(queue[i]))
                    return i;
        }
        return -1;
    }
2.3 查询首元素后,删除。

remove()poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。

  • poll()源码分析:
 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;//最后元素置为null
        if (s != 0)
            siftDown(0, x);//调整元素位置
        return result;
    }

siftDown()分析

在这里插入图片描述

​ 从图中,我们可以看到,最后一位元素拿到首位置后,从头向下依次与其左子树和右子树进行比较,直到找到合适的位置。

  • remove

对remove()方法分析,

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();
}
3、删除
3.1 remove(Object o)

remove(Object o)方法用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。

具体来说,remove(Object o)可以分为2种情况:

  1. 删除的是最后一个元素。直接删除即可,不需要调整。

  2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。此处不再赘述。

在这里插入图片描述

   public boolean remove(Object o) {
        int i = indexOf(o);
        if (i == -1)
            return false;
        else {
            removeAt(i);
            return true;
        }
    }

    private E removeAt(int i) {
        // assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            //调整顺序
            siftDown(i, moved);
            if (queue[i] == moved) {
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }
3.2 clear()全部清除
public void clear() {
//记录队列操作的次数
    modCount++;
    for (int i = 0; i < size; i++)
        queue[i] = null;
    size = 0;
}

3、简单介绍Queue接口

3.1 Queue接口中的方法有哪些独特的?

这里简单介绍下Queue接口的一些方法,能够让我们很好地理解PriorityQueue类中的方法
在这里插入图片描述

我们可以看到每种操作下都有两个方法,这里对讲出他们之间的区别:

  • 插入方法

    • add方法:将指定的元素插入到此队列中,如果不违反容量限制,可以立即执行此操作,并且在成功后返回 true, 如果当前没有可用空间, 则IllegalStateException
    • offer方法:如果在不违反容量限制的情况下,会立即执行,并将指定的元素插入到此队列中,执行成功后返回true。 当使用容量限制队列时,将无法执行,并返回false
    • 两者区别:对于插入元素失败的情况,处理方式不一样,add会返回异常,offer会返回false。
  • 提取方法

    • remove方法:检索并删除此队列的头。 如果此队列为空,它将抛出异常。
    • poll方法:检索并删除此队列的头,如果此队列为空,则返回 null
    • 两者区别:队列为空的时候,remove抛出异常,poll返回null。
  • 查询方法

    • element方法:查询队列中首元素,但不删除,查询到了返回元素,如果队列为空则返回异常。
    • peek方法:查询队列中首元素,但不删除,查询到了返回元素,如果队列为空则返回null
    • 两者区别: 查询不到元素的时候,element返回异常,peek返回null。

2.2 为什么介绍Queue接口?

介绍了那么多Queue接口的方法,这跟PriorityQueue 有什么关系么?

通过了解父类和接口的一些方法,我们能更好地了解这个类的一些性质。

在第一部分讲了PriorityQueue的继承,它的父类AbstractQueue类,继承了AbstractCollection类同时也实现了Queue接口,因而PriorityQueue 类拥有着Queue接口的方法(当然也有Collection接口)。

public abstract class AbstractQueue<E>
    extends AbstractCollection<E>
    implements Queue<E>

当然我们知道在java中,子类继承父类之后,便拥有三个最重要的特点:

  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

父类AbstractQueue类的方法

public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
  public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
  public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
   public void clear() {
        while (poll() != null)
            ;
    }
    public boolean addAll(Collection<? extends E> c) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

4、 来看Leetcode中一道题

1、题目23

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

示例2:

输入:lists = []
输出:[]

示例3:

输入:lists = [[]]
输出:[]

2、解答

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length==0) return null;
        ListNode mylist=new ListNode(-1);
        ListNode p =mylist; 
        //这里用到了PriorityQueue类
        PriorityQueue<ListNode> priorityQueue =new PriorityQueue<>(lists.length,(a,b)->(a.val-b.val));

        for(ListNode head:lists){
            if(head!=null){
                //这里添加
                priorityQueue.add(head);
            }

        }
        while(!priorityQueue.isEmpty()){
            //弹出首元素
            ListNode p1=priorityQueue.poll();
            p.next=p1;
            if(p1.next!=null){
                priorityQueue.add(p1.next);
            }
            p=p.next;
        }
 
 
        return mylist.next;
   
    }
}

这个用到了PriorityQueue类,可以用来稳固下之前学习的内容。

参考

https://docs.oracle.com/javase/8/docs/api/ 官方文档
https://www.cnblogs.com/CarpenterLee/p/5488070.html 一个大佬写的,很不错。我这里有很多用的他写的内容

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
PriorityQueue Java 的一个实现了优先级队列的数据结构,它可以根据元素的优先级自动进行排序。优先级队列的元素按照一定的顺序进行排列,每次从队列取出的元素都是优先级最高(或者最低)的元素。 在 PriorityQueue ,元素的排序可以通过自然排序(自然顺序)或者通过 Comparator 接口来指定。当元素没有实现 Comparable 接口时,需要使用 Comparator 来指定元素的排序规则。 PriorityQueue 使用了堆这种数据结构来实现,堆是一种完全二叉树,具有以下性质: 1. 父节点的值总是大于(或小于)其子节点的值,这取决于所使用的比较器。 2. 完全二叉树的最小(或最大)元素总是在根节点。 PriorityQueue 提供了一些常用的方法,如插入元素、删除元素、获取队列大小等。你可以使用 add() 或 offer() 方法将元素添加到队列,使用 remove() 或 poll() 方法从队列删除并返回第一个元素,使用 peek() 方法来获取但不删除队列的第一个元素。 下面是一个使用 PriorityQueue 的简单示例: ```java import java.util.PriorityQueue; public class PriorityQueueExample { public static void main(String[] args) { // 创建一个整数型的优先级队列 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 添加元素到队列 pq.offer(5); pq.offer(2); pq.offer(8); pq.offer(1); // 依次获取并删除队列的元素 while (!pq.isEmpty()) { System.out.println(pq.poll()); } } } ``` 这个示例将输出排序后的整数序列:1、2、5、8。注意,PriorityQueue 默认使用自然排序,即元素的自然顺序。你也可以通过提供自定义的 Comparator 来指定元素的排序规则。 希望这个例子能够帮助你理解 PriorityQueue 的用法。如有更多问题,请随时提问!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下次一定少写BUG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值