我有两张地图Map<String, Long>
。我想合并两个映射,按降序排序,并获得前5个。如果合并中有重复键,我需要对值求和。我有以下代码可用:
Map<String, Long> topFive = (Stream.concat(map1.entrySet().stream(),
map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
Long::sum)))
.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(5)
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1,
LinkedHashMap::new));
但我想知道是否有更好的解决方案。
答案 0 :(得分:3)
如果你的意思是更好 性能,并且你有大量的集合,并且只需要很少的顶级元素,你可以避免对整个地图进行排序,给定{{ 1}}复杂性。
如果您已经拥有Guava,则可以使用MinMaxPriorityQueue仅存储最佳的 N 结果。然后只需对这几个常量 N 元素进行排序。
n*log(n)
如果您没有/想要使用Guava,可以使用自定义Comparator<Entry<String, Long>> comparator = Entry.comparingByValue(reverseOrder());
Map<String, Long> merged = Stream.of(map1, map2)
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
Long::sum));
MinMaxPriorityQueue<Entry<String, Long>> tops = MinMaxPriorityQueue.orderedBy(comparator)
.maximumSize(5)
.create(merged.entrySet());
Map<String, Long> sorted = tops.stream()
.sorted(comparator)
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
(m1, m2) -> m1,
LinkedHashMap::new));
来模拟MinMaxPriorityQueue
(如果您还可以创建在构造函数中接收最大大小的类,不想使用匿名类[这是为了显示功能]。
TreeMap
并将所有元素添加到顶部。
Set<Entry<String, Long>> sorted = new TreeSet<Entry<String, Long>>(comparator) {
@Override
public boolean add(Entry<String, Long> entry) {
if (size() < 5) { // 5 can be constructor arg in custom class
return super.add(entry);
} else if (comparator().compare(last(), entry) > 0) {
remove(last());
return super.add(entry);
} else {
return false;
}
}
};
您还可以更改合并功能,使用类似于Federico提到的合并的内容。
sorted.addAll(merged);
这比使用流更快,在此之后,一旦你有了合并的地图,你可以用Map<String, Long> merged = new HashMap<>(map1);
map2.forEach((k, v) -> merged.merge(k, v, Long::sum));
或MinMaxPriorityQueue
选择前N个元素,再次避免不必要的排序整个系列。
答案 1 :(得分:0)
更好的解决方案可能是使用保持前5个的累加器,而不是对整个流进行排序。现在你正在进行估计的n * log(n)比较,而不是n和n * log(5)之间的某种比较。
答案 2 :(得分:0)
我会专注于让代码更容易阅读:
// Merge
Map<String, Long> merged = new HashMap<>(map1);
map2.forEach((k, v) -> merged.merge(k, v, Long::sum));
// Sort descending
List<Map.Entry<String, Long>> list = new ArrayList<>(merged.entrySet());
list.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
// Select top entries
Map<String, Long> top5 = new LinkedHashMap<>();
list.subList(0, Math.min(5, list.size()))
.forEach(e -> e.put(e.getKey(), e.getValue()));
此外,通过不使用流,此解决方案肯定会有更好的性能。
答案 3 :(得分:0)
使用Collector
添加其他解决方案。它使用TreeSet
作为中间累积类型,使用整理器将集合转换为地图。
private <K, V, E extends Map.Entry<K,V>> Collector<E, TreeSet<E>, Map<K,V>>
toMap(BinaryOperator<V> mergeFunction, Comparator<E> comparator, int limit) {
Objects.requireNonNull(mergeFunction);
Objects.requireNonNull(comparator);
Supplier<TreeSet<E>> supplier = () -> new TreeSet<>(comparator);
BiConsumer<TreeSet<E>, E> accumulator = (set, entry) -> accumulate(set, entry, mergeFunction);
BinaryOperator<TreeSet<E>> combiner = (destination, source) -> {
source.forEach(e -> accumulator.accept(destination, e)); return destination; };
Function<TreeSet<E>, Map<K,V>> finisher = s -> s.stream()
.limit(limit)
.collect(Collectors.toMap(E::getKey, E::getValue, (v1, v2) -> v1, LinkedHashMap::new));
return Collector.of(supplier, accumulator, combiner, finisher);
}
private <K, V, E extends Map.Entry<K,V>> void accumulate(
TreeSet<E> set, E newEntry, BinaryOperator<V> mergeFunction) {
Optional<E> entryFound = set.stream()
.filter(e -> Objects.equals(e.getKey(), newEntry.getKey()))
.findFirst();
if (entryFound.isPresent()) {
E existingEntry = entryFound.get();
set.remove(existingEntry);
existingEntry.setValue(mergeFunction.apply(existingEntry.getValue(), newEntry.getValue()));
set.add(existingEntry);
}
else {
set.add(newEntry);
}
}
这是你如何使用它,比较条目的值(反过来)和使用Long::sum
合并函数进行条目冲突。
Comparator<Map.Entry<String,Long>> comparator = Map.Entry.comparingByValue(Comparator.reverseOrder());
Map<String, Long> topFive = Stream.of(map1, map2)
.map(Map::entrySet)
.flatMap(Collection::stream)
.collect(toMap(Long::sum, comparator, 5));