【问题标题】:C++ to Java: searching a collection efficientlyC++ 到 Java:有效地搜索集合
【发布时间】:2012-07-30 16:35:37
【问题描述】:

来自主要是 C++ 背景的我现在正在愤怒地编写一些 Java。我发现使用 STL 在 C++ 中基本的东西在 Java 中似乎比我认为的更麻烦。我的结论是,可能有一个更好的 Java 习惯用法我还没有掌握。这是一个使用伪代码的示例。

我有一个基于某些成员变量的自然排序关系的集合,这些成员变量恰好是字符串。

class Thing
{
   String key1;
   String key2;
}

在 C++ 中,我可能会定义一个排序运算符

///
/// @brief
/// provide a total order for 'Things' using key1 and key2
///
bool operator<(const Thing& a, const Thing& b)
{
  if (a.key1 < b.key1) return true; 
  else if (a.key1 > b.key1) return false; 
  else return a.key2 < b.key2;
} 

然后,我可以使用 set::find 在 O(log N) 时间内找到元素,以应对拥有事物的情况。使用 operator

struct Ordering
{
   /// A strict weak ordering not a total ordering
   bool operator()(const Thing& A,const std::string& key1) const;
}

const_iterator iter = std::lower_bound(someThings.begin(),
                                       someThings.end(),
                                       key1,
                                       Ordering());

为了不那么抽象,假设 key1 是名称,key2 是版本。我可以问一下我们是否有任何名为 Foobar 的软件,或者更具体地说,我们是否有 Foobar v1.0。

从表面上看,Java 中 std::set 最直接的等价物似乎是 TreeSet 排序可以通过继承 Comparator 接口来实现。 然而,对于我所说的,看起来需要多个地图才能在 Java 中执行此操作。 在 C++ 中,如果我想更改值,只会费心使用像 std::map 这样的关联容器。 在 C++ std::set 中,就像在 Java TreeSet 中一样,值是它自己的键。但是,在 C++ 中,我可以编写比较器来使用 key1 或 key2 将“Thing”与“std::string”进行比较,并在它们的 std::set 中找到特定的东西。 在我看来,您必须使用 Map 在 Java 中执行此操作。 否则(因为 Comparator 只有一个类型参数) 你最终会遇到这样的混乱:

public static class Order implements Comparator<Object>
{
  @Override
  @Constant
  public int compare(Object a, Object b)
  {
     String aString;
     String bString;         
     if (a instanceof String)
     {
        aString = (String)a;
     }
     else if (a instanceof Thing)
     {
        aString = ((Field)a).getKey1();
     }
     else
     {
        throw new ClassCastException("String or Field object expected.");
     }
     if (b instanceof String)
     {
        bString = (String)b;
     }
     else if (b instanceof Thing)
     {
        bString = ((Field)b).getKey1();
     }
     else
     {
        throw new ClassCastException("String or Field object expected.");
     }
     return aString.compareTo(bString);
  }
};

但是,如果您这样做,您可以(在 Thing 类中)写:

Set<Thing> things = new TreeSet<Thing>(new Order());

boolean hasFieldWithKey1(final String key1) 
{
   return this.fields.contains(key1);
}

使用 Java Set,您只能测试是否存在,但不能检索您正在搜索的对象。例如你做不到

Field getFieldWithKey1(final String key1) 
{
   return this.fields.floor(key1);
}

因为像 floor() 这样的方法只接受值类型的对象(即 Thing)

显而易见的解决方案是为每个键使用一个 Map。

Map<String,Thing> thingsByKey1 = new TreeMap<Thing>(new Order());

来自 C++ 背景,这似乎不必要地臃肿。 当东西已经包含它时,为什么我要再次存储它? 如果我有两把钥匙,那就更糟了。我需要两张地图。

Map<String,Thing> thingsByKey1 = new TreeMap<Thing>(new OrderByKey1());
Map<String,Thing> thingsByKey2 = new TreeMap<Thing>(new OrderByKey2());

我现在不仅要复制键,还要创建额外的不必要的树数据结构(或具有更好运行时性能的 HashMap)。对于上面的排序实现,这也可能是“完全错误的”,因为每个键本身仅形成部分顺序,而不是一组事物的总顺序。

我已经看到这里使用线性搜索回答了有关搜索的问题,这几乎总是最糟糕的选择。 例如

Finding all objects that have a given property inside a collection

我注意到有一个 BinarySearch 版本接受 Comparator 对象作为参数,但返回元素的索引而不是元素本身。 这意味着在使用 get() 之后会有不必要的调用(假设集合支持它)。

那么,Java 在时间和空间上有效地做到这一点的方法是什么?

【问题讨论】:

  • 这个问题还不错,但我很困惑你如何使用std::set 在 O(log N) 时间内搜索不同的键。 std::set 仅使用 1 个比较器类,并且仅比较集合中的 value_type,因此在使用 find()lower_bound()upper_bound() 时,额外的重载将无济于事。如果你使用std::find(),你就会陷入线性搜索。
  • 在集合中使用std::find 会浪费时间。 std::setO(log N),而std::find在一个集合上会是O( N )(在操作总数中,即使比较次数还是O(log N)
  • 如果a.key1 &gt; b.key1,您的operator&lt; 实现会导致未定义的行为,因为它不会针对这种情况返回值。
  • 我的措辞选择不当(实际上完全错误)。实际上,我正在使用带有多种比较方法的 std::lower_bound,如果我完全使用 find,它将被设置为::find 或 map::find 而不是 std::find。在此示例中,集合只有一个总订单,但您可能希望通过多种方式与之匹配。我将进行编辑以更好地解释和更正运算符

标签: java c++ collections map set


【解决方案1】:

Java 方法是使用Map

来自 C++ 背景,这似乎不必要地臃肿。当东西已经包含它时,我为什么要再次存储它?

这并没有您想象的那么多开销。您正在存储一个对String 的额外引用,总成本为...4 个字节。 (实际上,成本为零:TreeSet 实现占用的内存与TreeMap 完全相同。)

如果要使用两个键进行搜索,可以使用比较两个键的Comparator&lt;Thing&gt;,或使Thing 实现Comparable&lt;Thing&gt;,然后维护TreeSet&lt;Thing&gt;。这比您在上面写的...不愉快的Comparator 紧凑得多。如果您想一键搜索,只需使用Map&lt;String, Thing&gt;。如果您真的非常想同时搜索两者,请同时维护它们。 (实际上,我几乎从来不需要这样做……JDK Collections 框架的作者也不认为你需要经常这样做。)

【讨论】:

  • 我想我对这个问题的两件事感到好奇。当 Java 社区真正需要编写高效的代码时,他们倾向于如何思考以及他们会做什么。例如,我可以在 C++ 中使用排序向量而不是 std::set 并以插入更昂贵为代价完全丢失树。我可以使用 ArrayList 和 Collections.sort 在 Java 中做同样的事情。一个 Java 人可能会在 Apache Commons 中找到更合适的东西,而不是自己动手。
  • Collections 框架本身倾向于推动您使用一致的抽象。例如,“排序的List”将无法满足add(int, E) 的约定,该约定指定在特定位置添加元素。也就是说,我从未发现这会显着损害性能,而且 JDK 中的实现通常比你或我写的任何东西都要好。
  • 四处搜索我怀疑你在 Guava 中的可迭代接口更适合我的编程风格。所以看起来,在这种情况下,JDK 中的实现并不比 you 写的任何东西更好(即在谷歌) - stackoverflow.com/questions/587404/… 我想我需要问自己一个不同的问题,哪些库我应该使用,为什么。我也发现自己在问为什么还有一个 apache commons 集合库。
  • 老实说,链接问题的答案(即使是使用番石榴的答案)都没有比简单的 for 循环表现得更好,老实说,我认为它们中的大多数都不是作为可读。 (参见 Guava wiki 中的 this 页面。) Wrt Apache,Guava 出现在 Apache 之后,解决了 Commons 库的感知问题:a)Apache 不提供泛型,b)Apache 库倾向于违反 Collection合同。
  • 我知道为什么要创建番石榴。不太清楚为什么首先需要 Apache 公共集合。从 C++ 背景来看,Guava 的重要贡献是函数式算法和不可变集合,它们是在 C++ 中随 STL 开箱即用提供的。
猜你喜欢
  • 2014-04-28
  • 1970-01-01
  • 2016-11-17
  • 2014-07-28
  • 1970-01-01
  • 2011-12-15
  • 2014-12-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多