【Java数据结构及算法实战】系列011:数组实现的优先级队列PriorityQueue

简介: 【Java数据结构及算法实战】系列011:数组实现的优先级队列PriorityQueue

PriorityQueue是基于数组实现的无界优先级队列PriorityQueue的元素按其自然顺序排序,或由队列构造时提供的比较器根据所使用的构造函数排序。优先级队列不允许空元素,依赖自然顺序的优先级队列也不允许插入不可比较的对象。




PriorityQueue本质上就是一个最小堆存储结构数组,通过“极大优先级堆”实现的,即堆顶元素是优先级最大的元素。堆的操作,主要就是两个:siftUp(向上调整堆)和siftDown(向下调整堆)。






PriorityQueue的队首是相对于指定顺序来说优先级的值最小的元素。如果多个元素优先级的值都是最小,那么头部就是其中一个元素。PriorityQueue的检索操作pollremovepeek等都是访问位于队首的元素。




PriorityQueue是无界队列,因此插入的元素是无限制的,但其具有一个内部容量,它控制用于在队列上存储元素的数组的大小。它总是至少和队列一样大。当元素被添加到优先级队列时,其容量会自动增长。




PriorityQueue及其迭代器实现了CollectionIterator接口的所有可选方法。方法itilerator()提供的迭代器和方法spliterator()提供的分离器并不保证以任何特定的顺序遍历优先级队列的元素。如果需要有序遍历,请考虑使用Arrays.sort(pq.toArray()).




注意PriorityQueue是唯一一个非线程安全的队列实现类,适合用于单线程存放数据并且将数据排序。如果是在多个线程中有修改了队列的场景,那么不应该用线程PriorityQueue,而应该使用线程安全的java.util.concurrent.PriorityBlockingQueue类。




PriorityQueueJava Collections Framework的一个成员。






1.   PriorityQueue的声明


PriorityQueue的接口和继承关系如下




publicclassPriorityQueue<E> extends AbstractQueue<E>


   implements java.io.Serializable {


  …


}






完整的接口继承关系如下图所示。











从上述代码可以看出,PriorityQueue既实现了java.io.Serializable接口,又继承了java.util.AbstractQueue<E>








2.   PriorityQueue的成员变量和构造函数






以下是PriorityQueue的构造函数和成员变量。




// 默认数组容量


   privatestaticfinalintDEFAULT_INITIAL_CAPACITY = 11;


 


// 元素数组


   transient Object[] queue;


 


// 队列中的元素个数


   intsize;




  // 比较器


   privatefinal Comparator<? super E> comparator;




   // 结构性修改的次数


   transientintmodCount;




   public PriorityQueue() {


       this(DEFAULT_INITIAL_CAPACITY, null);


   }




   public PriorityQueue(intinitialCapacity) {


       this(initialCapacity, null);


   }




   public PriorityQueue(Comparator<? super E> comparator) {


       this(DEFAULT_INITIAL_CAPACITY, comparator);


   }




   public PriorityQueue(intinitialCapacity,


                        Comparator<? super E> comparator) {


       if (initialCapacity < 1)


           thrownew IllegalArgumentException();


       this.queue = new Object[initialCapacity];


       this.comparator = comparator;


   }




   public PriorityQueue(Collection<? extends E> c) {


       if (cinstanceof SortedSet<?>) {


           SortedSet<? extends E> ss = (SortedSet<? extends E>) c;


           this.comparator = (Comparator<? super E>) ss.comparator();


           initElementsFromCollection(ss);


       }


       elseif (cinstanceof PriorityQueue<?>) {


           PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;


           this.comparator = (Comparator<? super E>) pq.comparator();


           initFromPriorityQueue(pq);


       }


       else {


           this.comparator = null;


           initFromCollection(c);


       }


   }




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


       this.comparator = (Comparator<? super E>) c.comparator();


       initFromPriorityQueue(c);


   }




   public PriorityQueue(SortedSet<? extends E> c) {


       this.comparator = (Comparator<? super E>) c.comparator();


       initElementsFromCollection(c);


   }






从上述代码可以看出,构造函数有6种。构造函数中的参数含义如下




l  initialCapacity用于设置队列中内部数组的容量。如果没有指定,则会使用默认数组容量DEFAULT_INITIAL_CAPACITY的值。


l  comparator为比较器


l  c用于设置最初包含给定集合的元素,按集合迭代器的遍历顺序添加




类成员queue是一个数组,用于存储队列中的元素。size用于记录队列中的元素个数。




modCount主要用于记录PriorityQueue被结构性修改的次数,比如像offerclearpoll等操作的时候,modCount都会自增


3.   PriorityQueue的核心方法


以下对PriorityQueue常用核心方法的实现原理进行解释。






3.1.     offer(e)


执行offer(e)方法后有两种结果




l  队列未达到容量时,返回 true


l  队列达到容量时,先扩容,再返回 true




PriorityQueueoffer (e)方法源码如下:




publicbooleanoffer(E e) {


       if (e == null)    // 判空


           thrownew NullPointerException();


       modCount++;


       inti = size;


       if (i >= queue.length)


           grow(i + 1);  // 扩容


       siftUp(i, e);  // 插入数据


       size = i + 1;


       returntrue;


   }




从上面代码可以看出,执行offer(e)方法时,分为以下几个步骤:




l  判断待入队的元素e是否为null。为null则抛出NullPointerException异常。


l  判断当前队列中的元素是否已经大于等于队列的容量,如果是则证明队列已经满了,需要先通过grow ()方法扩容。


l  通过siftUp()方法插入数据元素


l  返回true




grow()方法源码如下:






privatevoid grow(intminCapacity) {


       intoldCapacity = queue.length;


       intnewCapacity = ArraysSupport.newLength(oldCapacity,


               minCapacity - oldCapacity,


               oldCapacity < 64 ? oldCapacity + 2 : oldCapacity >> 1


               );


       queue = Arrays.copyOf(queue, newCapacity);


}




siftUp()方法源码如下:




privatevoid siftUp(intk, E x) {


       if (comparator != null)


           siftUpUsingComparator(k, x, queue, comparator);


       else


           siftUpComparable(k, x, queue);


   }




   privatestatic <T> void siftUpComparable(intk, T x, Object[] es) {


       Comparable<? super T> key = (Comparable<? super T>) x;


       while (k > 0) {


           intparent = (k - 1) >>> 1;


           Object e = es[parent];


           if (key.compareTo((T) e) >= 0)


               break;


           es[k] = e;


           k = parent;


       }


       es[k] = key;


   }




   privatestatic <T> void siftUpUsingComparator(


       intk, T x, Object[] es, Comparator<? super T> cmp) {


       while (k > 0) {


           intparent = (k - 1) >>> 1;


           Object e = es[parent];


           if (cmp.compare(x, (T) e) >= 0)


               break;


           es[k] = e;


           k = parent;


       }


       es[k] = x;


}






在上述代码中,在位置k处插入项x,通过向上提升x到树形结构中来维护堆的不变性,直到x大于或等于它的父节点或根节点。


3.2.     add(e)


执行add(e)方法后有有两种结果




l  队列未达到容量时,返回 true


l  队列达到容量时,先扩容,再返回 true




PriorityQueueadd(e)方法源码如下:




   publicbooleanadd(E e) {


       return offer(e);


   }




从上面代码可以看出,add(e)方法等同于offer(e)方法的实现。









3.3.     poll ()


执行poll()方法后有两种结果:




l  队列不为空时,返回队首值并移除


l  队列为空时,返回 null






PriorityQueuepoll()方法源码如下:




public E poll() {


       final Object[] es;


       final E result;




       if ((result = (E) ((es = queue)[0])) != null) {


           modCount++;


           finalintn;


           final E x = (E) es[(n = --size)];


           es[n] = null;


           if (n > 0) {


               final Comparator<? super E> cmp;


               if ((cmp = comparator) == null)


                   siftDownComparable(0, x, es, n);


               else


                   siftDownUsingComparator(0, x, es, n, cmp);


           }


       }


       returnresult;


}




从上面代码可以看出,执行poll()方法时,分为以下几个步骤:




l  先取队列的队首元素。


l  如果队首元素不存在,直接返回null


l  如果队首元素存在,则返回该元素同时通过siftDownComparable() 或者siftDownUsingComparator()方法来移除元素。




siftDownComparable()siftDownUsingComparator()方法源码如下:






 


privatestatic <T> voidsiftDownComparable(intk, T x, Object[] es, intn) {


       Comparable<? super T> key = (Comparable<? super T>)x;


       inthalf = n >>> 1;


       while (k < half) {


           intchild = (k << 1) + 1;


           Object c = es[child];


           intright = child + 1;


           if (right < n &&


               ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)


               c = es[child = right];


           if (key.compareTo((T) c) <= 0)


               break;


           es[k] = c;


           k = child;


       }


       es[k] = key;


   }




   privatestatic <T> void siftDownUsingComparator(


       intk, T x, Object[] es, intn, Comparator<? super T> cmp) {


       inthalf = n >>> 1;


       while (k < half) {


           intchild = (k << 1) + 1;


           Object c = es[child];


           intright = child + 1;


           if (right < n && cmp.compare((T) c, (T) es[right]) > 0)


               c = es[child = right];


           if (cmp.compare(x, (T) c) <= 0)


               break;


           es[k] = c;


           k = child;


       }


       es[k] = x;


   }




3.4.     remove()


执行remove()方法后有两种结果:




l  队列不为空时,返回队首值并移除


l  队列为空时,抛出异常




PriorityQueueremove()方法其实是调用了父类AbstractQueueremove ()方法,源码如下:




public E remove() {


       E x = poll();


       if (x != null)


           returnx;


       else


           thrownew NoSuchElementException();


}




从上面代码可以看出,remove()直接调用了poll()方法。如果poll()方法返回结果为null,则抛出NoSuchElementException异常。




poll()方法此处不再赘述。








3.5.     peek()


执行peek()方法后有两种结果:




l  队列不为空时,返回队首值但不移除


l  队列为空时,返回null






peek()方法源码如下:




public E peek() {


       return (E) queue[0];


}




从上面代码可以看出,peek()方法比较简单,直接就是获取了数组里面的索引为0的元素。




3.6.     element()


执行element()方法后有两种结果:




l  队列不为空时,返回队首值但不移除


l  队列为空时,抛出异常






element()方法其实是调用了父类AbstractQueueelement()方法,源码如下:




public E element() {


       E x = peek();


       if (x != null)


           returnx;


       else


           thrownew NoSuchElementException();


}




从上面代码可以看出,执行element()方法时,先是获取peek()方法的结果,如果结果是null,则抛出NoSuchElementException异常。






4.   PriorityQueue的单元测试




PriorityQueue的单元测试如下:








package com.waylau.java.demo.datastructure;




importstatic org.junit.jupiter.api.Assertions.assertEquals;


importstatic org.junit.jupiter.api.Assertions.assertNull;


importstatic org.junit.jupiter.api.Assertions.assertThrows;


importstatic org.junit.jupiter.api.Assertions.assertTrue;




import java.util.NoSuchElementException;


import java.util.PriorityQueue;


import java.util.Queue;




import org.junit.jupiter.api.Test;




/**


* PriorityQueue Tests


*


* @since 1.0.0 2020524


* @author <a href="https://waylau.com">Way Lau</a>


*/


class PriorityQueueTests {


   @Test


   void testOffer() {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列未满时,返回 true


       booleanresultNotFull = queue.offer("Java");


       assertTrue(resultNotFull);




       // 测试队列达到容量时,会自动扩容


       queue.offer("C");


       queue.offer("Python");


       booleanresultFull = queue.offer("C++"); // 扩容


       assertTrue(resultFull);


   }




@Test


   voidtestAdd() {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列未满时,返回 true


       booleanresultNotFull = queue.add("Java");


       assertTrue(resultNotFull);




       // 测试队列满则扩容,返回 true


       queue.add("C");


       queue.add("Python");


       booleanresultFull = queue.add("C++"); // 扩容


       assertTrue(resultFull);


}




   @Test


   void testPoll() throws InterruptedException {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列为空时,返回 null


       String resultEmpty = queue.poll();


       assertNull(resultEmpty);




       // 测试队列不为空时,返回队首值并移除


       queue.add("Java");


       queue.add("C");


       queue.add("Python");


       String resultNotEmpty = queue.poll();


       assertEquals("C", resultNotEmpty);


   }




   @Test


   void testRemove() throws InterruptedException {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列为空时,抛出异常


       Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {


           queue.remove();// 抛异常


       });




       assertEquals(null, excpetion.getMessage());




       // 测试队列不为空时,返回队首值并移除


       queue.add("Java");


       queue.add("C");


       queue.add("Python");


       String resultNotEmpty = queue.remove();


       assertEquals("C", resultNotEmpty);


   }




   @Test


   void testPeek() throws InterruptedException {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列不为空时,返回队首值并但不移除


       queue.add("Java");


       queue.add("C");


       queue.add("Python");


       String resultNotEmpty = queue.peek();


       assertEquals("C", resultNotEmpty);


       resultNotEmpty = queue.peek();


       assertEquals("C", resultNotEmpty);


       resultNotEmpty = queue.peek();


       assertEquals("C", resultNotEmpty);




       // 测试队列为空时,返回null


       queue.clear();


       String resultEmpty = queue.peek();


       assertNull(resultEmpty);


   }




   @Test


   void testElement() throws InterruptedException {


       // 初始化队列


       Queue<String> queue = new PriorityQueue<String>(3);




       // 测试队列不为空时,返回队首值并但不移除


       queue.add("Java");


       queue.add("C");


       queue.add("Python");


       String resultNotEmpty = queue.element();


       assertEquals("C", resultNotEmpty);


       resultNotEmpty = queue.element();


       assertEquals("C", resultNotEmpty);


       resultNotEmpty = queue.element();


       assertEquals("C", resultNotEmpty);




       // 测试队列为空时,抛出异常


       queue.clear();


       Throwable excpetion = assertThrows(NoSuchElementException.class, () -> {


           queue.element();// 抛异常


       });




       assertEquals(null, excpetion.getMessage());


   }


}




5.   PriorityQueue的应用案例:英雄战力排行榜


以下是一个英雄战力排行榜的示例。该示例模拟了6个英雄,可以根据英雄的战力由高至低排序。




以下是Hero类,用来代表英雄:






package com.waylau.java.demo.datastructure;






/**


* Hero


*


* @since 1.0.0 2020523


* @author <a href="https://waylau.com">Way Lau</a>


*/


publicclass Hero {




   private String name;


 


   private Integer power; // 战力


 


   public Hero(String name, Integer power) {


       this.name = name;


       this.power = power;


   }


 


   public String getName() {


       returnname;


   }




   publicvoid setName(String name) {


       this.name = name;


   }




   public Integer getPower() {


       returnpower;


   }




   publicvoid setPower(Integer power) {


       this.power = power;


   }




   @Override


   public String toString() {


       return"Hero [name=" + name + ", power=" + power + "]";


   }




}






以下是应用主程序:










package com.waylau.java.demo.datastructure;




import java.util.Comparator;


import java.util.PriorityQueue;


import java.util.Queue;




/**


* PriorityQueue Demo


*


* @since 1.0.0 2020524


* @author <a href="https://waylau.com">Way Lau</a>


*/


publicclass PriorityQueueDemo {


   publicstaticvoid main(String[] args) {


       intn = 6;


     


       Queue<Hero> queue = new PriorityQueue<Hero>(n, new Comparator<Hero>() {


           // 战力由大到小排序


           @Override


           publicint compare(Hero hero0, Hero hero1) {


               returnhero1.getPower().compareTo(hero0.getPower());


           }


       });




       queue.add(new Hero("Nemesis", 95));


       queue.add(new Hero("Edifice Rex", 88));


       queue.add(new Hero("Marquis of Death", 91));


       queue.add(new Hero("Magneto", 96));


       queue.add(new Hero("Hulk", 85));


       queue.add(new Hero("Doctor Strange", 94));




     


       for (inti = 0; i<n ; i++) {


           System.out.println(queue.poll());


       }


   }




}










运行上述程序,输出内容如下:




Hero [name=Magneto, power=96]


Hero [name=Nemesis, power=95]


Hero [name=Doctor Strange, power=94]


Hero [name=Marquis of Death, power=91]


Hero [name=Edifice Rex, power=88]


Hero [name=Hulk, power=85]


6.   参考引用


本系列归档至《Java数据结构及算法实战》:https://github.com/waylau/java-data-structures-and-algorithms-in-action

《数据结构和算法基础(Java语言实现)》(柳伟卫著,北京大学出版社出版):https://item.jd.com/13014179.html



目录
相关文章
|
5天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
26 5
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
47 6
|
1月前
|
存储 Java 索引
Java中的数据结构:ArrayList和LinkedList的比较
【10月更文挑战第28天】在Java编程世界中,数据结构是构建复杂程序的基石。本文将深入探讨两种常用的数据结构:ArrayList和LinkedList,通过直观的比喻和实例分析,揭示它们各自的优势与局限,帮助你在面对不同的编程挑战时做出明智的选择。
|
3天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
29 6
|
18天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
16天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
12天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
12天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
34 3