如何根据条件从优先级队列中轮询值

Posted

技术标签:

【中文标题】如何根据条件从优先级队列中轮询值【英文标题】:How to do poll values from Priority queue based on a condition 【发布时间】:2021-08-18 05:47:29 【问题描述】:

我有地图 Map 队列是根据分数排序的(反向)。我从一个 List 中填充了地图,其中键是 data.getGroup,值是 Dataitself。

现在我的用例是,

    如果地图的大小为 如果地图的大小 > 3,那么我需要根据分数从地图中获取 3 个值(1 个值/键)。

例如:

// output should be just Data(17.0, "five", "D"), Data(4.0, "two", "A"), Data(3.0, "three", "B") though there will be only 4 keys (A,B,C,D) 
      ArrayList<Data> dataList = new ArrayList<Data>();
        dataList.add(new Data(1.0, "one", "A"));
        dataList.add(new Data(4.0, "two", "A"));
        dataList.add(new Data(3.0, "three", "B"));
        dataList.add(new Data(2.0, "four", "C"));
        dataList.add(new Data(7.0, "five", "D"));
        dataList.add(new Data(17.0, "five", "D"));
        
// output should be just Data(5.0, "six", "A"), Data(3.14, "two", "B"), Data(3.14, "three", "C") as there will be only 3 keys (A,B,C)
      ArrayList<Data> dataList2 = new ArrayList<Data>();
        dataList2.add(new Data(3.0, "one", "A"));
        dataList2.add(new Data(5.0, "six", "A"));
        dataList2.add(new Data(3.14, "two", "B"));
        dataList2.add(new Data(3.14, "three", "C"));
 

我尝试了以下方法,但在 Java 中是否有更好/更智能(优化)的方法?

// n = 3
public List<Data> getTopN(final List<Data> dataList, final int n) 

   private static final Comparator< Data > comparator = Comparator.comparing(Data::getScore).reversed();

   Map<String, PriorityQueue<Data>> map = Maps.newHashMap();

   for (Data data : dataList) 
            String key = data.getGroup();

            if (key != null) 
                if (!map.containsKey(key)) 
                    map.put(key, new PriorityQueue<>(comparator));
                
                map.get(key).add(data);
            
      
     
     if (map.size <= n) 
         List<Data> result = new ArrayList<Data>();
      
         for (Map.Entry<String, PriorityQueue<Data>> entrySet: map.entrySet())

               PriorityQueue<Data> priorityQueue = entrySet.getValue();
               result.add(priorityQueue.peek());
               
         
      return result;
      else if (map.size > n) 
    
              List<Data> result = new ArrayList<Data>();
      
         for (Map.Entry<String, PriorityQueue<Data>> entrySet: map.entrySet())

               PriorityQueue<Data> priorityQueue = entrySet.getValue();
               result.add(priorityQueue.peek());
               
         

         return result.stream()
               .sorted(Comparator.comparingDouble(Data::getScore).reversed())
               .limit(n)
               .collect(Collectors.toList());
  

数据对象如下所示:

public class Data 
     double score;
     String name; 
     String group;
     
      
    public void setName(String name) 
        this.name = name;
    
     
    public void setGroup(String group) 
        this.group = group;
    
    
    public void setScore(double score) 
        this.score = score;
    
    
    public String getName() 
        return name;
    
     
    public String getGroup() 
        return group;
    
    
    public double getScore() 
        return score;
    
    

【问题讨论】:

您说“我有地图Map&lt;String, PriorityQueue&gt;”,但在您的示例代码中,起点是List&lt;Data&gt; 【参考方案1】:

由于您的起点是List&lt;Data&gt;,因此当您只对每个键的一个值(即最大值)感兴趣时,将元素添加到Map&lt;String, PriorityQueue&lt;Data&gt;&gt; 没有多大意义。在这种情况下,您可以简单地存储最大值。

此外,值得考虑映射方法keySet()values()entrySet() 之间的差异。仅当您对循环体内的键和值都感兴趣时,使用后者才有用。否则,请使用keySet()values() 来简化操作。

仅当尝试从地图中获取前 n 个值时,使用 PriorityQueue 可能会提高性能:

private static final Comparator<Data> BY_SCORE = Comparator.comparing(Data::getScore);
private static final BinaryOperator<Data> MAX = BinaryOperator.maxBy(BY_SCORE);

public List<Data> getTopN(List<Data> dataList, int n) 
    Map<String, Data> map = new HashMap<>();

    for(Data data: dataList) 
        String key = data.getGroup();
        if(key != null) map.merge(key, data, MAX);
    

    if(map.size() <= n) 
        return new ArrayList<>(map.values());
    
    else 
        PriorityQueue<Data> top = new PriorityQueue<>(n, BY_SCORE);
        for(Data d: map.values()) 
            top.add(d);
            if(top.size() > n) top.remove();
        
        return new ArrayList<>(top);
    
    

请注意,BinaryOperator.maxBy(…) 使用升序作为基础,并且优先级队列现在需要升序,因为我们正在删除最小的元素,以便顶部 n 保留在排队等待结果。因此,reversed() 已从此处的Comparator 中删除。

如果 n 很小,则使用优先级队列会带来好处,尤其是与地图的大小相比。如果 n 相当大或预计接近地图的大小,则使用它可能更有效

List<Data> top = new ArrayList<>(map.values());
top.sort(BY_SCORE.reversed());
top.subList(n, top.size()).clear();
return top;

按降序对地图的所有值进行排序并删除多余的元素。这可以与处理map.size() &lt;= n 场景的代码结合使用:

public List<Data> getTopN(List<Data> dataList, int n) 
    Map<String, Data> map = new HashMap<>();

    for(Data data: dataList) 
        String key = data.getGroup();
        if(key != null) map.merge(key, data, MAX);
    

    List<Data> top = new ArrayList<>(map.values());
    if(top.size() > n) 
        top.sort(BY_SCORE.reversed());
        top.subList(n, top.size()).clear();
    
    return top;

【讨论】:

谢谢你的详细解释。对大 o 或其他性能指标有任何想法吗? 有三个因素,源列表大小(我们称之为l),限制n,以及组的数量,即地图的大小,(我们称之为@987654338 @)。第一个有O(l + m × log n),第二个有O(l + m × log m)(¹)。当您对这些值一无所知时,mn 两者的上限是 l,因此在最坏的情况下,您必须认为它们都是相同的,最终为O(n × log n),这是两种方法和正式正确答案都不可避免的最坏情况。在实践中,第二个具有更好的平均案例和低水平因素。 (¹) 现在您应该明白为什么当n 已知相当小时第一个更可取,而当n 可能很大时第二个更可取。跨度> 它返回的是优先队列解决方案的升序 优先队列解决方案专注于只提供 max n 个元素,而不是以任何特定顺序提供它们。看到升序只是巧合,表达式new ArrayList&lt;&gt;(top) 不保证任何顺序。请注意,在map.size() &lt;= n 的情况下,它甚至会以未指定的哈希映射顺序返回元素。如果要对结果进行排序,请使用第二种方法。但请注意,答案中发布的版本在不需要时也不会排序。如果您希望始终对结果进行排序,则必须将 sort 调用移到 if 语句之前。

以上是关于如何根据条件从优先级队列中轮询值的主要内容,如果未能解决你的问题,请参考以下文章

意外行为 java 优先级队列。对象添加了一次但轮询了两次。怎么可能?

优先级队列

优先队列

消息轮询实例 ID

优先级队列的总结

修改队列内容后如何从优先级队列中获取最小元素[重复]