【问题标题】:How to filter Map values using a Set of Strings?如何使用一组字符串过滤地图值?
【发布时间】:2020-06-19 12:21:36
【问题描述】:

我必须使用 Set 字符串过滤 Map 值,我可以让它工作(一位合作伙伴建议使用 anyMatch() 代替我在这里所做的,但我看不出如何) 我想知道你对这个算法有什么看法,如果可以改进它,也许使用另一个Stream 函数甚至contains() 方法,我也不确定我是否可以避免直接迭代Set(对于每个循环)。

ProductsResponse serviceResponse = (obtained from backend) ...;
Set<String> productIds = (some code to collect the strings from another API) ...;
//Here starts the filtering process 
serviceResponse.setProducts(serviceResponse.getProducts().entrySet().stream()
          .filter(product -> {
             for (String productId: productIds) {
                if (product.getKey().startsWith(productId)) {
                    return true;
                }
             }
             return false;
           }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));

重要提示:我使用Set过滤Map中的产品,Set中的字符串格式与Map,因此使用startsWith() 比较它们是安全的

更新 1:添加涉及的类定义。

class ProductsResponse() {

  private Map<String, ProductResource> products;
}

class ProductResource () {

   private String productId;
   private String name;
   private Double price;
}

【问题讨论】:

  • 你能把周围的代码贴出来吗?值是什么类型?如此有限的信息很难提供帮助。
  • 是的,当然,我错了,我认为这很清楚,我会在几分钟内上传更多代码,谢谢。

标签: java dictionary for-loop lambda set


【解决方案1】:

如果productIds 集合中没有两个值ab where a.startsWith(b),那么您可以通过将集合设为TreeSet 来大大提高性能。

TreeSet<String> productIds = (some code to collect the strings from another API) ...;
.filter(product -> {
   String id = productIds.floor(product.getKey());
   return (id != null && product.getKey().startsWith(id));
})

或者:

.filter(product -> Optional.ofNullable(productIds.floor(product.getKey()))
                   .map(product.getKey()::startsWith).orElse(false))

这会将性能从 O(n*m) 更改为 O(n*log(m)),其中 mproductIds 的大小。


更新

如果在productIds 集合中a.startsWith(b) 中有ab,那么您需要一些额外的逻辑。

例如如果集合包含GGED,并且您正在检查它是否包含GET 的前缀,那么floor() 将返回GED,因此您必须去掉最后一个字符并使用@ 重复查找987654342@,现在返回G,将其作为有效前缀。

所以我们需要添加一个循环来重新检查:

.filter(product -> {
   String candidate = product.getKey();
   while ((candidate = productIds.floor(candidate)) != null) {
      if (product.getKey().startsWith(candidate))
         return true;
      candidate = candidate.substring(0, candidate.length() - 1);
   }
   return false;
})

它会稍微减慢查找速度,但仍然比完整的顺序搜索要好得多。

【讨论】:

  • 我不知道floor() 操作的存在,听起来不错,我会尝试并做一些测试来分析您的解决方案和@Andronicus 之间的响应时间,谢谢大家。
  • @MarcoMarchetti TreeSetTreeMap 的“查找附近”方法被大多数人完全忽视。他们只是将 Tree 变体视为 Hash 变体的排序版本。但是,已排序 (SortedSet/SortedMap) 和可导航 (NavigableSet/NavigableMap) 允许使用 firstlower (floor (ceiling ( >=)、higher (>) 和 last(命名为 xxxKeyxxxEntry 用于 Map)。
  • 但这真的只适用于natural ordering。如果您按降序对Integers 中的set 进行排序,则这些方法的行为与其名称所暗示的相反。它们仍然可以使用,但可能会令人困惑。
【解决方案2】:

您可以通过流式传输集合并使用 anyMatch 来避免内部 for 循环:

serviceResponse.setProducts(serviceResponse.getProducts()
    .entrySet().stream()
    .filter(product -> productIds.stream().anyMatch(productId -> product.getKey().startsWith(productId)))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));

为简洁起见,您也可以使用方法参考:

serviceResponse.setProducts(serviceResponse.getProducts()
    .entrySet().stream()
    .filter(product -> productIds.stream().anyMatch(product.getKey()::startsWith))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));

【讨论】:

  • 请注意 - 这仍然是 O(m*n) 解决方案,但至少在没有 for 循环的情况下看起来会更好。
  • 当然算法没变,谢谢@VLAZ
  • 是的,只是想澄清一下。我见过一些人出于某种原因认为迭代 Set 是O(1)
  • 我不相信它会编译。当我这样做时,我不得不使用anyMatch(id-&gt;product.getKey().startsWith(id))
  • @WJS 是的,我没有测试它,这也是我的想法。我按照你以前的方式写了它,然后编辑了,但我可能会恢复,谢谢!
猜你喜欢
  • 1970-01-01
  • 2021-06-06
  • 1970-01-01
  • 2016-01-11
  • 2019-04-04
  • 2021-06-05
  • 2015-10-30
  • 1970-01-01
  • 2018-06-17
相关资源
最近更新 更多