【问题标题】:Does Java have a HashMap with reverse lookup?Java 有反向查找的 HashMap 吗?
【发布时间】:2010-12-12 19:20:02
【问题描述】:

我有以“键-键”格式而非“键-值”形式组织的数据。它就像一个 HashMap,但我需要在两个方向上进行 O(1) 查找。这种类型的数据结构是否有名称,Java 的标准库中是否包含类似的内容? (或者可能是 Apache Commons?)

我可以编写自己的类,它基本上使用两个镜像地图,但我宁愿不重新发明轮子(如果这已经存在,但我只是没有寻找正确的术语)。

【问题讨论】:

    标签: java map hashmap bidirectional reverse-lookup


    【解决方案1】:

    Java API 中没有这样的类。您想要的 Apache Commons 类将成为 BidiMap 的实现之一。

    作为一名数学家,我将这种结构称为双射。

    【讨论】:

    • 作为非数学家,我将这种结构称为“一张地图,可让您通过键或其他方式查找值”
    • 太糟糕了,它不支持泛型,Guava 似乎支持。
    • github.com/megamattron/collections-generic 具有支持泛型的 BidiMap
    • @Don "Bidi" -> "双向"
    • @Dónal 是的,但整个 IT 都是基于数学的
    【解决方案2】:

    除了 Apache Commons,Guava 还有一个BiMap

    【讨论】:

    • 感谢您的信息!我暂时坚持使用 apache(除非有充分的理由不这样做?)
    • 我无法与 apache 集合进行很好的比较,但 google 集合确实有很多我认为值得一看的好东西。
    • Google Collections 的一个优点是它具有泛型,而 Commons Collections 则没有。
    • 对于两个库的比较,请参阅此答案中的引号:stackoverflow.com/questions/787446/…(以及原始采访)。出于显而易见的原因,这对 Google 有偏见,但即便如此,我认为现在可以肯定地说,您现在使用 Google Collections 会更好。
    • BiMap 链接已损坏。请使用this one
    【解决方案3】:

    这是我用来完成此任务的一个简单类(我不想再有另一个第三方依赖项)。它不提供地图中可用的所有功能,但它是一个好的开始。

        public class BidirectionalMap<KeyType, ValueType>{
            private Map<KeyType, ValueType> keyToValueMap = new ConcurrentHashMap<KeyType, ValueType>();
            private Map<ValueType, KeyType> valueToKeyMap = new ConcurrentHashMap<ValueType, KeyType>();
    
            synchronized public void put(KeyType key, ValueType value){
                keyToValueMap.put(key, value);
                valueToKeyMap.put(value, key);
            }
    
            synchronized public ValueType removeByKey(KeyType key){
                ValueType removedValue = keyToValueMap.remove(key);
                valueToKeyMap.remove(removedValue);
                return removedValue;
            }
    
            synchronized public KeyType removeByValue(ValueType value){
                KeyType removedKey = valueToKeyMap.remove(value);
                keyToValueMap.remove(removedKey);
                return removedKey;
            }
    
            public boolean containsKey(KeyType key){
                return keyToValueMap.containsKey(key);
            }
    
            public boolean containsValue(ValueType value){
                return keyToValueMap.containsValue(value);
            }
    
            public KeyType getKey(ValueType value){
                return valueToKeyMap.get(value);
            }
    
            public ValueType get(KeyType key){
                return keyToValueMap.get(key);
            }
        }
    

    【讨论】:

    • 您将通过将 containsValue() 更改为 return valueToKeyMap.containsKey(value) 来显着提高其性能
    • 我不会像目前那样使用此映射,因为如果使用不同的值(或键)重新添加键(或值),则双向性会中断,这将是有效的用法更新 IMO 密钥。
    【解决方案4】:

    如果没有发生冲突,您总是可以将两个方向添加到同一个 HashMap :-)

    【讨论】:

    • @Kip:为什么?在某些情况下,这是一个完全合法的解决方案。所以会有两个哈希映射。
    • 不,这是一个丑陋而脆弱的黑客。它需要维护每个 get() 和 put() 上的双向属性,并且可以将其传递给修改映射的其他方法,甚至不知道双向属性。也许它可以作为一个方法中的局部变量,它不会在任何地方传递,或者如果它在创建后立即变得不可修改。但即便如此,它也很脆弱(有人出现并调整了该功能并以一种不会立即表明自己成为问题的方式破坏双向性)
    • @Kip,我同意这种用法应该保留在使用该映射的类内部,但你的最后一句话只有在相应的 JUnit 测试马虎时才成立 :-)
    • 如果我可以非常有效地使用这样的实现,想象需要一个映射来解码/编码汇编语言指令的操作码,你也永远不会改变映射的状态,在一个方向上的键是指令字符串其他二进制值。所以你不应该有冲突。
    • 对于小规模查找目的,该 hack 解决了我的问题。
    【解决方案5】:

    这是我的 2 美分。

    或者您可以使用带有泛型的简单方法。小菜一碟。

    public static <K,V> Map<V, K> invertMap(Map<K, V> toInvert) {
        Map<V, K> result = new HashMap<V, K>();
        for(K k: toInvert.keySet()){
            result.put(toInvert.get(k), k);
        }
        return result;
    }
    

    当然,您必须拥有具有唯一值的地图。否则,将替换其中一个。

    【讨论】:

      【解决方案6】:

      受到GETah's answer的启发,我决定自己写一些类似的东西,并做了一些改进:

      • 该类正在实现Map&lt;K,V&gt;-接口
      • 通过put 更改值时,确实可以保证双向性(至少我希望在此保证)

      用法就像法线贴图,在映射调用getReverseView() 上获得反向视图。不复制内容,只返回一个视图。

      我不确定这是否完全万无一失(实际上,可能不是),所以如果您发现任何缺陷,请随时发表评论,我会更新答案。

      public class BidirectionalMap<Key, Value> implements Map<Key, Value> {
      
          private final Map<Key, Value> map;
          private final Map<Value, Key> revMap;
      
          public BidirectionalMap() {
              this(16, 0.75f);
          }
      
          public BidirectionalMap(int initialCapacity) {
              this(initialCapacity, 0.75f);
          }
      
          public BidirectionalMap(int initialCapacity, float loadFactor) {
              this.map = new HashMap<>(initialCapacity, loadFactor);
              this.revMap = new HashMap<>(initialCapacity, loadFactor);
          }
      
          private BidirectionalMap(Map<Key, Value> map, Map<Value, Key> reverseMap) {
              this.map = map;
              this.revMap = reverseMap;
          }
      
          @Override
          public void clear() {
              map.clear();
              revMap.clear();
          }
      
          @Override
          public boolean containsKey(Object key) {
              return map.containsKey(key);
          }
      
          @Override
          public boolean containsValue(Object value) {
              return revMap.containsKey(value);
          }
      
          @Override
          public Set<java.util.Map.Entry<Key, Value>> entrySet() {
              return Collections.unmodifiableSet(map.entrySet());
          }
      
          @Override
          public boolean isEmpty() {
              return map.isEmpty();
          }
      
          @Override
          public Set<Key> keySet() {
              return Collections.unmodifiableSet(map.keySet());
          }
      
          @Override
          public void putAll(Map<? extends Key, ? extends Value> m) {
              m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
          }
      
          @Override
          public int size() {
              return map.size();
          }
      
          @Override
          public Collection<Value> values() {
              return Collections.unmodifiableCollection(map.values());
          }
      
          @Override
          public Value get(Object key) {
              return map.get(key);
          }
      
          @Override
          public Value put(Key key, Value value) {
              Value v = remove(key);
              getReverseView().remove(value);
              map.put(key, value);
              revMap.put(value, key);
              return v;
          }
      
          public Map<Value, Key> getReverseView() {
              return new BidirectionalMap<>(revMap, map);
          }
      
          @Override
          public Value remove(Object key) {
              if (containsKey(key)) {
                  Value v = map.remove(key);
                  revMap.remove(v);
                  return v;
              } else {
                  return null;
              }
          }
      
      }
      

      【讨论】:

      • 请注意,就像 BiMap 和 BidiMap 一样,这是一个双射,不允许有多个具有相同值的键。 (getReverseView().get(v) 将始终只返回一个键)。
      • 没错,但 OTOH 这正是 OP 要求的
      • 我不确定他是否表示它的数据符合这个约束,但无论如何,它可以帮助其他人更好地理解!
      【解决方案7】:

      这是一个很老的问题,但如果其他人像我一样有脑阻塞并且偶然发现这个问题,希望这会有所帮助。

      我也在寻找双向 HashMap,有时它是最有用的最简单的答案。

      如果您不想重新发明轮子并且不想将其他库或项目添加到您的项目中,那么简单实现并行数组(或 ArrayLists,如果您的设计需要它)怎么样。

      SomeType[] keys1 = new SomeType[NUM_PAIRS];
      OtherType[] keys2 = new OtherType[NUM_PAIRS];
      

      一旦您知道两个键之一的索引,您就可以轻松地请求另一个。因此,您的查找方法可能类似于:

      SomeType getKey1(OtherType ot);
      SomeType getKey1ByIndex(int key2Idx);
      OtherType getKey2(SomeType st); 
      OtherType getKey2ByIndex(int key2Idx);
      

      这是假设您正在使用正确的面向对象结构,其中只有方法正在修改这些数组/ArrayLists,保持它们并行非常简单。 ArrayList 更容易,因为如果数组的大小发生变化,您不必重新构建,只要您一前一后地添加/删除。

      【讨论】:

      • 你正在失去 HashMaps 的一个重要特性,即 O(1) 查找。像这样的实现需要扫描其中一个数组,直到找到要查找的项目的索引,即 O(n)
      • 是的,这是非常正确的,这是一个相当大的缺点。然而,在我个人的情况下,我实际上需要一个三向密钥列表,而且我总是会提前知道至少 1 个密钥,所以对我个人而言,这不是问题。不过,感谢您指出这一点,我似乎在原始帖子中跳过了这个重要的事实。
      猜你喜欢
      • 2016-11-11
      • 1970-01-01
      • 2013-12-23
      • 1970-01-01
      • 2019-04-01
      • 2016-04-10
      • 2017-01-25
      • 1970-01-01
      相关资源
      最近更新 更多