【问题标题】:How to implement efficient hash cons with java HashSet如何使用 java HashSet 实现高效的哈希 cons
【发布时间】:2014-06-06 11:20:33
【问题描述】:

我正在尝试在 java 中实现 hash cons,与 String.intern 对字符串所做的类似。即,我希望一个类将数据类型 T 的所有不同值存储在一个集合中,并提供一个 T intern(T t) 方法来检查 t 是否已经在集合中。如果是,则返回集合中的实例,否则将t 添加到集合中并返回。原因是可以使用引用相等来比较结果值,因为从 intern 返回的两个相等值肯定也是同一个实例。

当然,哈希 cons 最明显的候选数据结构是java.util.HashSet<T>。但是,它的接口似乎有缺陷并且不允许有效插入,因为没有方法可以检索已经在集合中的元素,或者如果它不在集合中则插入一个。

使用HashSet 的算法如下所示:

class HashCons<T>{
    HashSet<T> set = new HashSet<>();

    public T intern(T t){
        if(set.contains(t)) {
           return ???;  // <----- PROBLEM
        } else {
           set.add(t); // <--- Inefficient, second hash lookup
           return t;
    }
}

如您所见,问题是双重的:

  1. 此解决方案效率低下,因为我将访问哈希表两次,一次用于contains,一次用于add。但是好吧,这可能不会对性能造成太大影响,因为正确的存储桶将在 contains 之后位于缓存中,因此 add 不会触发缓存未命中,因此速度非常快。
  2. 我无法检索集合中已有的元素(参见标记为PROBLEM 的行)。只是没有方法可以检索集合中的元素。所以这是不可能实现的。

我在这里遗漏了什么吗?还是真的不可能用java.util.HashSet 构建一个通常的哈希缺点?

【问题讨论】:

    标签: java hashtable hashset


    【解决方案1】:

    我认为使用HashSet 是不可能的。您可以改用某种Map,并将您的值用作键作为值。 java.util.concurrent.ConcurrentMap也正好有相当方便的方法

    putIfAbsent(K key, V value)
    

    如果该值已经存在,则返回该值。但是,我不知道这种方法的性能(与“手动”检查Map 的非并发实现相比)。

    下面是使用HashMap 的方法:

    class HashCons<T>{
        Map<T,T> map = new HashMap<T,T>();
    
        public T intern(T t){
            if (!map.containsKey(t))
                map.put(t,t);
            return map.get(t);
        }
    }
    

    我认为HashSet 不可能的原因很简单:对于集合,如果满足contains(t),则意味着给定的t等于其中之一集合中的 t'。没有理由退回它(因为您已经拥有它)。

    【讨论】:

    • 是的,我也想过这个问题。但这有点 hacky,因为这个东西不是地图,它是一个集合。当然,我可以滥用没有有意义的值类型的映射(就像 HashSet 实现在内部本身所做的那样),但是如果通常的集合实现缺少这样的功能,那就太可惜了……此外,我仍然必须访问HashMap 两次,这是一种耻辱:(
    • 看这里stackoverflow.com/questions/23877776/… 你需要一个 Map 来用它第一个创建的相同实例替换任何候选人。还有stackoverflow.com/questions/23744201/…
    • 这里稍微说明一下:HashSet其实是用一个HashMap来存储item的,所以如果你把它改成HashMap也不会有什么负面影响。最终,您可以像 HashSet 一样做:只使用映射的键。
    • 一个map就是这么浪费,每个T被存储两次,一次作为key,一次作为value。如果我存储数百万个值,那将是相当大的空间浪费。搞笑的是java.util中没有空间效率设置...
    • @cy3er:是的,是的。那我会自己写哈希表,因为东西太大了,我不能容忍这么多的空间浪费。
    【解决方案2】:

    HashSet 在 OpenJDK 中实现为 HashMap 包装器,因此与 aRestless 建议的解决方案相比,您不会在内存使用方面获胜。

    10 分钟草图

    class HashCons<T> {
        T[] table;
        int size;
        int sizeLimit;
        HashCons(int expectedSize) {
            init(Math.max(Integer.highestOneBit(expectedSize * 2) * 2, 16));
        }
    
        private void init(int capacity) {
            table = (T[]) new Object[capacity];
            size = 0;
            sizeLimit = (int) (capacity * 2L / 3);
        }
    
        T cons(@Nonnull T key) {
            int mask = table.length - 1;
            int i = key.hashCode() & mask;
            do {
                if (table[i] == null) break;
                if (key.equals(table[i])) return table[i];
                i = (i + 1) & mask;
            } while (true);
            table[i] = key;
            if (++size > sizeLimit) rehash();
            return key;
        }
    
        private void rehash() {
            T[] table = this.table;
            if (table.length == (1 << 30))
                throw new IllegalStateException("HashCons is full");
            init(table.length << 1);
            for (T key : table) {
                if (key != null) cons(key);
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-03-20
      • 2015-07-06
      • 2014-11-21
      • 1970-01-01
      • 2010-11-16
      • 1970-01-01
      • 2021-09-14
      相关资源
      最近更新 更多