【问题标题】:Performance of algorithms sorting list entries into a map将列表条目排序到地图中的算法的性能
【发布时间】:2016-12-01 03:32:53
【问题描述】:

给定一个列表,其中每个条目都是一个看起来像的对象

class Entry {
    public String id;
    public Object value;
}

多个条目可以具有相同的id。我需要一个可以访问属于某个 id 的所有值的地图:

Map<String, List<Object>> map;

我实现这个的算法:

for (Entry entry : listOfEntries) {
    List<Object> listOfValues;
    if (map.contains(entry.id)) {
        listOfValues = map.get(entry.id);
    } else {
        listOfValues = new List<Object>();
        map.put(entry.id, listOfValues);
    }
    listOfValues.add(entry.value);
}

简单地说:我转换了一个看起来像这样的列表

ID | VALUE
---+------------
a  | foo
a  | bar
b  | foobar

到一张看起来像的地图

a--+- foo 
   '- bar
b---- foobar

如您所见,contains 为源列表的每个条目调用。这就是为什么我想知道是否可以改进我的算法,如果我预先对源列表进行排序然后这样做:

List<Object> listOfValues = new List<Object>();
String prevId = null;
for (Entry entry : listOfEntries) {
    if (prevId != null && prevId != entry.id) {
        map.put(prevId, listOfValues);
        listOfValues = new List<Object>();
    }
    listOfValues.add(entry.value);
    prevId = entry.id;
}
if (prevId != null) map.put(prevId, listOfValues);

第二种解决方案的优点是我不需要为每个条目调用map.contains(),但缺点是我必须先进行排序。此外,第一种算法更容易实现且不易出错,因为您必须在实际循环之后添加一些代码。

因此我的问题是:哪种方法性能更好?

示例是用 Java 伪代码编写的,但实际问题也适用于其他编程语言。

【问题讨论】:

  • 没有实际回答您的问题,您的数据结构称为多图。您可以在 Guava 的 TreeMultimap 和/或 MultimapBuilder 的帮助下获得所需的内容。
  • @Sorin 的回答在很大程度上是正确的。在性能方面,我自己也遇到过类似的问题。在我的情况下(整数 id;数十亿个条目;许多重复的 id),第二种方法要快得多,因为排序是缓存高效的并且与一个很小的常数相关联。但是,在您的情况下,对字符串进行排序会抵消排序的缓存效率; large Object 也可能会稍微降低排序性能。此外,如果您没有很多重复的 id,则第一种方法可能会更快。不过,我不能肯定地说。

标签: java algorithm performance sorting dictionary


【解决方案1】:

如果您有一个哈希映射和大量的条目,那么逐个插入项目将比按列表排序和插入它们更快(O(n) vs O(N log N))。如果您使用基于树的地图,那么这两种方法的复杂性是相同的。

但是,我真的怀疑您有足够多的条目,因此内存访问模式,以及比较和哈希函数的生效速度有多快。您有 2 个选项:忽略它,因为差异不会很大;或者对这两个选项进行基准测试,看看哪一个在您的系统上运行得更好。如果您没有数百万个条目,我会忽略这个问题并使用更容易理解的内容。

【讨论】:

    【解决方案2】:

    不要预分类。即使是像 quicksort 这样的快速排序算法,对于 n 个项目,平均而言,O(n log n) .之后,您仍然需要 O(n) 来遍历列表。 contains 在(散列)地图上需要固定时间(结帐this question),不用担心。以线性时间遍历列表并使用contains

    【讨论】:

    • “你仍然需要 O(n) 才能遍历列表” 是吗?如果您在添加时进行了预排序,则可以使用二进制搜索,有效地将线性探测减少到 O(log n)。在 Java 8 的 HashMap 中,当 Comparable 值落入同一个哈希桶时,它们就是这样存储的。
    • @Slanec O(n*log(n))+O(log n) 仍然大于 O(n)+O(n)。
    • @Slanec:我指的是“因此我的问题是:哪种方法的性能更好?”由于 OP 在这两种情况下都使用简单的 foreach 循环,因此它是 O(n)。但是,“[…] 在添加时预排序,然后可以使用二进制搜索 […]”是什么意思?当您必须查看每个值时,您将如何使用二进制搜索?
    • @Vesper:这是 O(n log n) + O(log n) = O(n log n) 与 O(n),你不必遍历列表两次。
    【解决方案3】:

    想提供另一种使用流的解决方案

    import static java.util.stream.Collectors.groupingBy;
    import static java.util.stream.Collectors.mapping;
    import static java.util.stream.Collectors.toList;
    
    Map<String, List<Object>> map = listOfValues.stream()
        .collect(groupingBy(entry -> entry.id, mapping(entry -> entry.value, toList())));
    

    此代码更具声明性 - 它仅指定应将 List 转换为 Map。 那么以有效的方式实际执行转换是图书馆的责任。

    【讨论】:

    • 虽然我也更喜欢声明性代码,但我不同意你的观点“那么以有效的方式实际执行转换是库的责任”。当然,给定操作是否执行良好取决于库实现者。但是对于 Java 的 groupingBy“没有对返回的 Map 的类型、可变性、可序列化性或线程安全性的保证”。所以如果谈到性能,我会说这是想要控制的属性,因此,必须照顾好。
    猜你喜欢
    • 1970-01-01
    • 2022-01-04
    • 2020-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-17
    • 1970-01-01
    • 2022-10-24
    相关资源
    最近更新 更多