如何比较两个映射并检索两个映射中出现的值的键?

时间:2019-07-01 12:53:48

标签: java java-8 java-stream

newPropertiesFile.keySet().parallelStream()
    .filter(value -> oldPropertiesFile.keySet().parallelStream()
            .filter(entry -> oldPropertiesFile.get(entry).toString().equals(newPropertiesFile.get(value).toString()))
            .filter(values -> !values.equals(value)).count() > 0)
    .collect(Collectors.toMap(entryKey -> (String) entryKey, entryKey -> newPropertiesFile.get(entryKey).toString()));

例如, 我有mapA = {(1,'a'),(2,'b'),(3,'c')}mapB = {(5,'a'),(6,'d'),(7,'c')} 比较两个映射的valueList,'a'中的值'c'mapA出现在mapB中,它们的键分别是{​​{1}}和5

因此是我需要的o / p:
75

我已完成上述操作,并获得了所需的输出。但是O(n ^ 2)的复杂度太高了。有优化的方法吗?

更简化的示例:

7

2 个答案:

答案 0 :(得分:4)

让我们总结一下这方面的一些变化,因为最好的解决方案只有在阅读了其他答案和评论后才会出现。

这个问题的简化问题是这样的。给定两个地图:

Map<Integer, String> mapA = Map.of(1, "a", 2, "b", 3, "c")
Map<Integer, String> mapB = Map.of(5, "a", 6, "d", 7, "c")

找到mapB的键,它们对应于两个映射中出现的值。这个问题首先是一个解决方案,它是这样的(为清晰起见进行了编辑):

Set<Integer> result = mapB.keySet().stream()
    .filter(keyB -> mapA.keySet().stream()
                        .filter(keyA -> mapA.get(keyA).equals(mapB.get(keyB)))
                        .count() > 0)
    .collect(toSet());

从本质上讲,这就像两个嵌套的循环,在每个映射的键上循环。内部循环获取每个键的对应值并计算匹配次数。如果至少有一个匹配项,则密钥将通过过滤器传递到结果。

OP对此并不满意,因此要求进行改进,尤其是在算法复杂性方面。如评论中所述,实际问题可能有15,000个地图条目。该算法为O(n ^ 2),并且在此数量的映射的情况下确实开始明显退化。有一些较小的方法可以改进,例如使用anyMatch代替filtercount > 0,但是鉴于answer from Eritrean中提出的替代方法,这些方法不是必需的:

Set<Integer> result = mapB.entrySet().stream()
    .filter(entry -> mapA.values().contains(entry.getValue()))
    .map(Map.Entry::getKey)
    .collect(toSet());

这更好,因为它在mapA的contains视图上使用了values()操作,代替了先前解决方案的内部流。但是,不会为地图的值建立索引,因此contains()对地图的值起作用的唯一方法是(可能)搜索每个条目。这比以前要好一些,因为如果找到匹配项,contains()可以立即返回;但是如果找不到匹配项,则必须搜索地图的所有值。因此,平均而言,这种变化仍然需要O(n ^ 2)的时间。

减轻这种情况的一种方法是将mapA的值提取到HashSet中。这会将contains()检查从线性时间减少到恒定时间,从而将总体复杂度从O(n ^ 2)减少到O(n)。看起来像这样:

Set<String> aValues = new HashSet<>(mapA.values());
Set<Integer> result = mapB.entrySet().stream()
    .filter(entry -> aValues.contains(entry.getValue()))
    .map(Map.Entry::getKey)
    .collect(toSet());

这是一个很大的改进,但是事实证明,根本不需要使用流。回到问题陈述,它具有子句“ ...在两个映射中都出现的值”。实质上,这是在值集合上进行设置的交集。在Java中进行交叉的方法是使用retainAll方法。也就是说,给定两个集合xy,做x.retainAll(y)只会在x中保留那些也出现在y中的元素,而它将删除其他元素。这本质上是一个设定的交集。为此,retainAll通常必须在contains上反复调用y,因此,最好确保操作快速-就像使用HashSet一样。

好吧,如果我们与值集合相交,那就给了我们值-但是我们想要键。特别是,我们需要mapB的键。我们该怎么做?

事实证明,地图的values()视图支持删除(retainAll可以这样做),并且如果从其中删除了值,则会从基础地图中删除相应的条目。在这种情况下,我们可以从mapB(或副本)开始,获取其values()视图,使用先前已加载到retainAll中的mapA的值调用HashSet。这样在mapB中仅保留具有与mapA相同的值的条目。由于我们对键感兴趣,而不是对条目感兴趣,因此我们只获得keySet()视图。该代码如下所示:

Set<String> aValues = new HashSet<>(mapA.values());
Set<Integer> mapBcopy = new HashMap<>(mapB);
mapBcopy.values().retainAll(aVals);
Set<Integer> result = mapBcopy.keySet();

这说明了与在使用流的情况下相比,如何更可能在集合视图上使用集合批量操作来完成某些任务。

答案 1 :(得分:0)

如果我说对了:

  

比较两个地图的valueList,mapA中的值'a'和'c'出现在mapB中,其键分别为5和7。因此,我需要的o / p:5、7

仅用list#contains过滤第二张地图还不够:

    Map<Integer,String> mapA = new HashMap<>();
    mapA.put(1, "a");
    mapA.put(2, "b");
    mapA.put(3, "c");
    Map<Integer,String> mapB = new HashMap<>();
    mapB.put(5, "a");
    mapB.put(6, "d");
    mapB.put(7, "c");

    List<Integer> list = mapB.entrySet().stream()
                             .filter(e->mapA.containsValue(e.getValue()))
                             .map(e -> e.getKey())
                             .collect(Collectors.toList());
    System.out.println(list);