我有以下情况:我有很多BST s,我想合并同构子树来节省空间。
我将二进制搜索树节点散列为“唯一表” - 基本上是BST节点的散列。
具有相同左右子节点和相同键的节点具有相同的哈希码,并且我已适当地重写了节点类的等号。
一切正常,除了计算哈希是昂贵的 - 它涉及计算子节点的哈希值。
我想缓存节点的散列值。我遇到的问题是这样做的自然方式,从节点到整数的HashMap本身会调用节点上的哈希函数。
我通过在节点中声明一个新字段来解决这个问题,我用它来存储哈希码。但是,我觉得这不是正确的解决方案。
我真正想要的是使用使用节点地址的哈希将节点映射到其哈希码。我认为我可以通过制作HashMap并将节点转换为对象来实现这一点,然后对象将调用对象上的hashCode方法,但这不起作用(插入到散列中仍然会调用节点散列和相等函数。
我希望深入了解将节点实现到哈希代码缓存的最佳方法。我在下面附上了代码,说明了下面发生了什么。
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
class Bst {
int key;
String name;
Bst left;
Bst right;
public Bst( int k, String name, Bst l, Bst r ) {
this.key = k;
this.name = name;
this.left = l;
this.right = r;
}
public String toString() {
String l = "";
String r = "";
if ( left != null ) {
l = left.toString();
}
if ( right != null ) {
r = right.toString();
}
return key + ":" + name + ":" + l + ":" + r;
}
@Override
public boolean equals( Object o ) {
System.out.println("calling Bst's equals");
if ( o == null ) {
return false;
}
if ( !(o instanceof Bst) ) {
return false;
}
Bst n = (Bst) o;
if ( n == null || n.key != key ) {
return false;
} else if ( n.left != null && left == null || n.right != null && right == null ||
n.left == null & left != null || n.right == null && right != null ) {
return false;
} else if ( n.left != null && n.right == null ) {
return n.left.equals( left );
} else if ( n.left != null && n.right != null ) {
return n.left.equals( left ) && n.right.equals( right );
} else if ( n.left == null && n.right != null ) {
return n.right.equals( right );
} else {
return true;
}
}
@Override
public int hashCode() {
// the real hash function is more complex, entails
// calling hashCode on children if they are not null
System.out.println("calling Bst's hashCode");
return key;
}
}
public class Hashing {
static void p(String s) { System.out.println(s); }
public static void main( String [] args ) {
Set<Bst> aSet = new HashSet<Bst>();
Bst a = new Bst(1, "a", null, null );
Bst b = new Bst(2, "b", null, null );
Bst c = new Bst(3, "c", null, null );
Bst d = new Bst(1, "d", null, null );
a.left = b;
a.right = c;
d.left = b;
d.right = c;
aSet.add( a );
if ( aSet.contains( d ) ) {
p("d is a member of aSet");
} else {
p("d is a not member of aSet");
}
if ( a.equals( d ) ) {
p("a and d are equal");
} else {
p("a and d are not equal");
}
// now try casts to objects to avoid calling Bst's HashCode and equals
Set<Object> bSet = new HashSet<Object>();
Object foo = new Bst( a.key, a.name, a.left, a.right );
Object bar = new Bst( a.key, a.name, a.left, a.right );
bSet.add( foo );
p("added foo");
if ( bSet.contains( bar ) ) {
p("bar is a member of bSet");
} else {
p("bar is a not member of bSet");
}
}
}
答案 0 :(得分:2)
将哈希存储在节点中的字段中对我来说感觉恰到好处。这也是java.lang.String
用于自己的哈希码的内容。除了其他任何东西,它意味着你不可能最终得到可以以其他方式收集的对象的缓存条目等。
如果确实希望hashCode
中的实现返回Object
的值,则可以使用System.identityHashCode
。您不应该依赖于此 - 或任何其他哈希代码 - 是唯一的。
另一点:由于字段是包访问,您的树目前是可变的。如果您在第一次调用哈希码时缓存哈希码,则不会“注意”它是否因字段更改而发生更改。基本上,在使用其哈希码后,不应更改节点。
答案 1 :(得分:2)
将哈希值存储在字段中实际上可以等同于“缓存”该值,以便不必过于频繁地重新计算它。
这不一定是一种不好的做法,但是你必须确保在发生变化时正确地清除/重新计算它,如果你必须通知复杂的图形或树的变化,这可能是令人生畏的。
如果你想使用由JVM计算的哈希码(大致基于对象的“RAM地址”,即使它的值是特定于实现的),你也可以使用System.identityHashCode(x)那,以及Object.hashCode究竟做了什么。
答案 2 :(得分:2)
Java的内置IdentityHashMap完成了您所描述的内容。
那就是说,Jon Skeet的回答听起来更像是正确的方式。
答案 3 :(得分:1)
我真正想要的是使用使用节点地址的哈希将节点映射到其哈希码。
节点的地址是什么意思?在Java中没有这样的概念,并且对于我所知道的对象没有唯一的标识符,例如非基于VM的语言中的物理地址,例如C ++。 Java中的引用不是内存地址,GC可以随时将对象重新定位到内存中。
我认为我可以通过制作HashMap并将节点转换为对象来实现这一点,然后对象将调用对象上的hashCode方法,但这不起作用
实际上,由于hashCode
是虚拟的,并且在您的节点类中被覆盖,因此无论您使用的引用的静态类型如何,都将始终调用子类实现。
我担心任何使用地图缓存哈希值的尝试都会陷入同样的鸡和蛋问题,如你所说 - 地图首先需要哈希值。
我没有看到比在节点中缓存哈希值更好的方法。 你需要确保每当子节点发生变化时缓存的值都会失效。错误 - 正如Jon的回答所指出的那样,在对象存储到地图后更改对象的哈希码会破坏地图的内部完整性,所以一定不会发生。