C++转Java:高效地搜索集合

8

我之前主要使用C++,现在正在使用Java进行编程。我发现,在C++中使用STL的一些基本操作,在Java中似乎比我预想的更加繁琐。我的结论是,可能有一种更好的Java语法,我还没有完全掌握。以下是一个用伪代码表示的示例。

我有一组物品,它们具有基于某些成员变量的自然排序关系,这些成员变量恰好是字符串。

class Thing
{
   String key1;
   String key2;
}

在C++中,我可以定义一个排序操作符<(Thing, Thing),并将它们放入std::set中。例如:
///
/// @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 在拥有 Thing 的情况下,以 O(log N) 时间找到元素。使用 operator<() 的额外重载,我可以使用 std::lower_bound 或 std::equal_range 进行只有 key1 或同时拥有 key1 和 key2 的搜索。 例如:
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接口来实现排序。 然而,对于我所说的内容,似乎需要使用多个Maps在Java中实现此操作。 在C ++中,如果我想改变值,才会使用诸如std :: map之类的关联容器。 在C++中,像在Java TreeSet中一样,值是其自身的键。但是,我可以编写比较器,使用key1或key2适当地将“Thing”与“std :: string”进行比较,并在它们的std :: set中找到特定的thing。 在Java中,我觉得你必须使用Map才能实现这一点。 否则(因为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)。对于上述排序实现,这可能也是“完全错误”的,因为每个关键字本身只形成了部分顺序而不是一组物品的完全顺序。
我看到这里有关搜索的问题被回答为使用线性搜索,这几乎总是最糟糕的选择。例如: 在集合中查找具有给定属性的所有对象 我注意到有一个BinarySearch版本接受一个比较器对象作为参数,但返回元素的索引而不是元素本身。这意味着在使用它之后需要不必要地调用get()(假设集合支持它)。
那么Java的高效时间和空间方式是什么?

这个问题并不坏,但我很困惑你如何使用std::set在O(log N)时间内搜索不同的键。std::set只使用1个比较器类,并且仅比较集合的value_type,因此在使用find()lower_bound()upper_bound()时,额外的重载是无用的。如果你要求助于std::find(),那么你就会陷入线性搜索。 - Dave S
在集合中使用 std::find 是浪费时间的行为。std::set 的时间复杂度是 O(log N),而在集合上使用 std::find 的时间复杂度将会是 O(N)(即使比较次数仍然是 O(log N))。 - David Rodríguez - dribeas
如果a.key1 > b.key1,则您的operator<实现会导致未定义行为,因为它没有为该情况返回值。 - fredoverflow
我的措辞选择很糟糕(实际上是完全错误的)。事实上,我正在使用带有多个比较方法的std::lower_bound,如果我使用find,它将是set::find或map::find而不是std::find。 在这个例子中,集合有一个单一的总序,但有几种匹配方式。我会进行编辑以更好地解释并纠正operator<定义。 - Bruce Adams
1个回答

4
Java的方法是使用Map。
对于那些从C++转换过来的人,这似乎有些冗余。为什么我需要再次存储键,而事物已经包含它了?
实际上并不像你想的那样浪费资源。你只需要多存储一个指向String的引用,总共成本只有...4个字节。(实际上,成本为零:TreeSet实现的内存占用与TreeMap相同)。
如果您想使用两个密钥进行搜索,则可以使用一个比较器,该比较器比较两个键,或使Thing实现Comparable,然后维护一个TreeSet。这比您编写的令人不适的比较器紧凑得多。如果您只想使用一个键进行搜索,只需使用Map即可。如果您确实想要同时搜索,请将它们同时维护。 (实际上,我几乎从未必须这样做......JDK Collections框架的作者认为您也很少需要这样做。)

我猜这个问题有两个方面值得探究。Java社区的想法是什么,当他们需要编写高效代码时会做些什么。例如,我可以在C++中使用排序向量而不是std::set,从而完全省去树结构,但插入操作的代价更高。我可以通过使用ArrayList和Collections.sort在Java中实现相同的功能。Java开发者可能会选择在Apache Commons中找到更合适的解决方案,而不是自己动手实现。 - Bruce Adams
集合框架本身倾向于推动您使用一致的抽象。例如,“已排序的 List”将无法满足 add(int, E) 的合同,该合同指定在特定位置添加元素。尽管如此,我从未发现这会显著损害性能,并且JDK中的实现通常比您或我编写的任何内容都要好。 - Louis Wasserman
搜索一番后,我怀疑Guava中的可迭代接口更适合我的编程风格。因此,在这种情况下,JDK中的实现并不比你(即在Google)编写的任何东西更好- https://dev59.com/aHRB5IYBdhLWcg3wj36c 我认为我需要问自己一个不同的问题,那就是我应该使用哪些库以及为什么。我也在问为什么还有一个apache commons collections库。 - Bruce Adams
老实说,即使是使用Guava的答案,也没有比直接使用for循环表现得更好,而且说实话,我认为大多数答案都不够可读。关于这一点,可以参考Guava维基页面中的此处。就Apache而言,Guava是在Apache之后出现的,解决了Commons库的问题,其中a)Apache没有提供泛型,b)Apache库倾向于违反集合契约。 - Louis Wasserman
我知道Guava为什么被创建。但Apache Commons Collections一开始为什么需要并不清楚。从C++背景来看,Guava的重要贡献是功能风格算法和不可变集合,而这些在C++的STL中已经预置了。 - Bruce Adams
因为一个未解释的负评(仍然没有解释),我被提示重新访问这个问题。现在已经过去了几年,没有其他答案。我的工作让我回到了C++方向,这段时间我没有太多使用Java。我认为现在是接受这个答案的时候了。如果下次我再转向Java方向时发现更好的答案,我会发布它的。 - Bruce Adams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接