• 优先队列实现 大小根堆 解决top k 问题


     

     

    目录:[ - ]

    1、认识 PriorityQueue

    PriorityQueue是从JDK1.5开始提供的新的数据结构接口,它是一种基于优先级堆的极大优先级队列。优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头,字符串则按字典序排列(参阅 Comparable),也可以根据 Comparator 来指定,这取决于使用哪种构造方法。优先级队列不允许 null 元素。依靠自然排序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。

    比如队列 1 3 5 10 2 自动会被排列 1 2 3 5 10

    
    
    import java.util.Comparator;
    import java.util.PriorityQueue;
    import java.util.Queue;
     
    /*
     * 重写 Comparator<Integer>来决定
     * 优先队列是小根堆还是大根堆
     * */
    public class PriorityQueueExample {
     
           public static void main(String[] args) {
                 //实现小根堆
                Queue<Integer> qi = new PriorityQueue<Integer>();
                qi.add(5);
                qi.add(2);
                qi.add(1);
                qi.add(10);
                qi.add(3);
     
                 while (!qi.isEmpty()) {
                      System. out .print(qi.poll() + "," );
                }
                System. out .println();
                System. out .println("-----------------------------" );
           
                 // 自定义的比较器,可以让我们自由定义比较的顺序  Comparator<Integer> cmp;
                 // 生成最大堆使用e2-e1,生成最小堆使用e1-e2,
                Comparator<Integer> cmp = new Comparator<Integer>() {
                       public int compare(Integer e1, Integer e2) {
                             return e2 - e1; 
                      }
                };
                
                 //实现大根堆
                Queue<Integer> q2 = new PriorityQueue<Integer>(5, cmp); 
                q2.add(2);
                q2.add(8);
                q2.add(9);
                q2.add(1);
                 while (!q2.isEmpty()) {
                      System. out .print(q2.poll() + "," );
                }
     
          }
     
    }
    
    
    output 

    1,2,3,5,10, 
    ----------------------------- 
    9,8,2,1,

    此队列的头是按指定排序方式的最小元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。

    队列检索操作 poll、remove、peek 和 element 访问处于队列头的元素。
    优先级队列是无界的,但是有一个内部容量,控制着用于存储队列元素的数组的大小。
    它总是至少与队列的大小相同。随着不断向优先级队列添加元素,其容量会自动增加。无需指定容量增加策略的细节。
    注意1:该队列是用数组实现,但是数组大小可以动态增加,容量无限。
    注意2:此实现不是同步的。不是线程安全的。如果多个线程中的任意线程从结构上修改了列表, 则这些线程不应同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。
    注意3:不允许使用 null 元素。
    注意4:此实现为插入方法(offer、poll、remove() 和 add 方法)提供 O(log(n)) 时间;
    为 remove(Object) 和 contains(Object) 方法提供线性时间;
    为检索方法(peek、element 和 size)提供固定时间。
    注意5:方法iterator()中提供的迭代器并不保证以有序的方式遍历优先级队列中的元素。
    至于原因可参考下面关于PriorityQueue的内部实现
    如果需要按顺序遍历,请考虑使用 Arrays.sort(pq.toArray())。
    注意6:可以在构造函数中指定如何排序。如:
    PriorityQueue()
    使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用 Comparable)。
    PriorityQueue(int initialCapacity)
    使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用 Comparable)。
    PriorityQueue(int initialCapacity, Comparator comparator)
    使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器comparator来排序其元素。
    注意7:此类及其迭代器实现了 Collection 和 Iterator 接口的所有可选 方法。
    PriorityQueue的内部实现
    PriorityQueue对元素采用的是堆排序,头是按指定排序方式的最小元素。堆排序只能保证根是最大(最小),整个堆并不是有序的。
    方法iterator()中提供的迭代器可能只是对整个数组的依次遍历。也就只能保证数组的第一个元素是最小的。
    实例1的结果也正好与此相符。

    2、应用:求 Top K 大/小 的元素

    了解了优先队列之后,我们再来看它的一个应用:

    在面试的时候,问到算法,Top k 的问题是经常被问到的,网上已有很多种方法可以解决,今天来看看如何使用 PriorityQueue 构造固定容量的优先队列,模拟大顶堆,来解决 top K 小的问题。

    如果求top k 大的问题,就建立小根堆!!! 不是大根堆!!

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.Iterator;
    import java.util.List;
    import java.util.PriorityQueue;
    import java.util.Random;
     
    //固定容量的优先队列,模拟大顶堆,用于解决求topN小或 topk大的问题
    public class TopKwithPriorityQueue<E extends Comparable> {
           private PriorityQueue<E> queue ;
           private int K ; // 堆的最大容量,即 topk,所以maxsize=k
     
           public TopKwithPriorityQueue(int maxSize) {
                 if (maxSize <= 0)
                       throw new IllegalArgumentException();
                 this.K = maxSize;
           this.queue = new PriorityQueue(maxSize, new Comparator<E>() {
        public int compare(E o1, E o2) {
                    // 生成最大堆使用o2-o1,生成最小堆使用o1-o2, 并修改 e.compareTo(peek) 比较规则return (o2.compareTo(o1));
                }
            });
        }
          }
     
           public void add(E e) {
                 if (queue .size() < K) { // 未达到最大容量,直接添加
                       queue.add(e);
                } else { // 队列已满
                      E peek = queue.peek();        //取堆顶元素
                       if (e.compareTo(peek) < 0) { // 将新元素与当前堆顶元素比较,保留较小的元素
                             queue.poll();
                             queue.add(e);
                      }
                }
          }
     
           public List<E> sortedList() {
                List<E> list = new ArrayList<E>(queue );          //可以将整个优先队列传入 arraylist的构造方法做参数
                 Collections.sort(list); // PriorityQueue本身的遍历是无序的,最终需要对队列中的元素进行排序
                 return list;
          }
     
           public static void main(String[] args) {
                 final TopKwithPriorityQueue pq = new TopKwithPriorityQueue(10); //返回前k=10位
                Random random = new Random();
                 int rNum = 0;
                System. out.println("100 个 0~999 之间的随机数:-----------------------------------" );
                 for (int i = 1; i <= 100; i++) {
                      rNum = random.nextInt(1000);
                      System. out.print(rNum+" " );
                       pq.add(rNum);
                }
                System. out.println(" PriorityQueue 本身的遍历是无序的:返回的top10 最小堆是:-----------------------------------" );
                Iterable<Integer> iter = new Iterable<Integer>() {
                       public Iterator<Integer> iterator() {
                             return pq.queue.iterator() ;
                      }
                };
                 for (Integer item : iter) {
                      System. out.print(item + ", " );
                }
                System. out.println();
                System. out.println("PriorityQueue 排序后的遍历:返回的top10 最小堆是:-----------------------------------");
                 /*
                 * for (Integer item : pq.sortedList()) { System.out.println(item); }
                 */
                 // 或者直接用内置的 poll() 方法,每次取队首元素(堆顶的最大值)
                 while (!pq.queue .isEmpty()) {
                      System. out.print(pq.queue .poll() + ", ");
                }
          }
    }  

    由于仅仅保存了K个数据,有调整最小堆的时间复杂度为O(lnK),因此TOp K算法(问题)时间复杂度为O(nlnK).
    
    

    3、PriorityQueue  在 hadoop 中的应用:

    最后来聊下 “基于堆实现的优先级队列(PriorityQueue)” 在hadoop 中的应用:

    在 hadoop 中,排序是 MapReduce 的灵魂,MapTask 和 ReduceTask 均会对数据按 Key 排序,这个操作是 MR 框架的默认行为,不管你的业务逻辑上是否需要这一操作。

    MapReduce 框架中,用到的排序主要有两种:快速排序 和 基于堆实现的优先级队列

    Mapper 阶段: 

    从 map 输出到环形缓冲区的数据会被排序(这是 MR 框架中改良的快速排序),这个排序涉及 partition 和 key,当缓冲区容量占用 80%,会 spill 数据到磁盘,生成 IFile 文件,Map 结束后,会将 IFile 文件排序合并成一个大文件(基于堆实现的优先级队列),以供不同的 reduce 来拉取相应的数据。

    Reducer 阶段: 

    从 Mapper 端取回的数据已是部分有序,Reduce Task 只需进行一次归并排序即可保证数据整体有序。为了提高效率,Hadoop 将 sort 阶段和 reduce 阶段并行化,在 sort 阶段,Reduce Task 为内存和磁盘中的文件建立了小顶堆,保存了指向该小顶堆根节点的迭代器,并不断的移动迭代器,以将 key 相同的数据顺次交给 reduce() 函数处理,期间移动迭代器的过程实际上就是不断调整小顶堆的过程(建堆→取堆顶元素→重新建堆→取堆顶元素...),这样,sort 和 reduce 可以并行进行。

    了解了这个,你就明白为什么之前有同学提到遍历一遍 values 之后,值都不存在了,同时你也能更加理解之前提到的 二次排序。

    在 hadoop 中,用到了这一数据结构的类主要有如下:(hadoop-0.20.203.0) 

    core/org/apache/hadoop/io/SequenceFile.java
    hdfs/org/apache/hadoop/hdfs/server/namenode/UnderReplicatedBlocks.java
    mapred/org/apache/hadoop/mapred/join/CompositeRecordReader.java
    mapred/org/apache/hadoop/mapred/join/JoinRecordReader.java
    mapred/org/apache/hadoop/mapred/join/MultiFilterRecordReader.java
    mapred/org/apache/hadoop/mapred/join/OverrideRecordReader.java
    mapred/org/apache/hadoop/mapred/Merger.java
    tools/org/apache/hadoop/tools/rumen/DeskewedJobTraceReader.java

    可以看到,这一数据结构,在 hadoop 中用的还是比较广泛的。

    需要说明的是,求 Top k,更简单的方法可以直接用内置的 TreeMap 或者 TreeSet,这两者是基于红黑树的一种数据结构,内部维持 key 的次序,但每次添加新元素,其排序的开销要大于堆调整的开销。例如要找最大的10个元素,那么创建的是小根堆。小根堆的特性是根节点是最小元素。不需要对堆进行再排序,当堆的根节点被替换成新的元素时,需要进行堆化,以保持小根堆的特性。

     
     
  • 相关阅读:
    mac上python3安装HTMLTestRunner
    双目深度估计传统算法流程及OpenCV的编译注意事项
    深度学习梯度反向传播出现Nan值的原因归类
    1394. Find Lucky Integer in an Array
    1399. Count Largest Group
    1200. Minimum Absolute Difference
    999. Available Captures for Rook
    509. Fibonacci Number
    1160. Find Words That Can Be Formed by Characters
    1122. Relative Sort Array
  • 原文地址:https://www.cnblogs.com/williamjie/p/9478150.html
Copyright © 2020-2023  润新知