[英]How should I map string keys to values in Java in a memory-efficient way?
我正在寻找一种存储字符串 - > int映射的方法。 当然,HashMap是一个最明显的解决方案,但由于我受内存限制,需要存储200万对,7个字符长的密钥,我需要一些内存有效的东西,检索速度是次要参数。
目前我正沿着以下方向前进:
List<Tuple<String, int>> list = new ArrayList<Tuple<String, int>>();
list.add(...); // load from file
Collections.sort(list);
然后进行检索:
Collections.binarySearch(list, key); // log(n), acceptable
我是否可以选择自定义树(每个节点都是一个字符,每个叶子都有结果),或者是否有适合这种情况的现有集合? 这些字符串实际上是顺序的(英国邮政编码,它们没有多大区别),所以我期待在这里节省大量内存。
编辑 :我刚刚看到你提到字符串是英国邮政编码,所以我相当自信你使用Trove TLongIntHashMap不会出错:顺便说一下, Trove是一个小型库,它非常容易使用。
编辑2 :很多人似乎觉得这个答案很有趣,所以我正在添加一些信息。
这里的目标是以一种以内存效率的方式使用包含键/值的映射,因此我们将首先查找内存有效的集合。
以下SO问题是相关的(但与此相同)。
Jon Skeet提到Trove “只是一个来自原始类型的集合库” [原文如此],实际上,它并没有增加太多功能。 我们还可以看到一些关于Trove的内存和速度与默认集合相比的基准(由.duckman提供 )。 这是一个片段:
100000 put operations 100000 contains operations
java collections 1938 ms 203 ms
trove 234 ms 125 ms
pcj 516 ms 94 ms
还有一个示例显示使用Trove而不是常规Java HashMap可以节省多少内存:
java collections oscillates between 6644536 and 7168840 bytes
trove 1853296 bytes
pcj 1866112 bytes
因此,尽管基准测试总是需要花费一些时间,但很明显, Trove不仅会节省内存,而且会更快。
因此,我们现在的目标是使用Trove(通过在常规HashMap中投入数百万条条目,您的应用开始感到反应迟钝)。
你提到了200万对,7个字符的长键和一个String / int映射。
2000000是真的不那么多,但你还是会觉得“对象”开销和原语的常数(UN)拳击整数在一个普通的HashMap {字符串,整数}这就是为什么特罗韦使得有很大的意义在这里。
但是,我要指出,如果你可以控制“7个字符”,你可以更进一步:如果你只使用ASCII或ISO-8859-1字符,那么你的7个字符就会很长( *)。 在这种情况下,您可以完全躲避对象创建,并在很长时间内代表您的7个角色。 然后,您将使用Trove TLongIntHashMap并完全绕过“Java对象”开销。
你明确指出你的密钥是7个字符长然后评论他们是英国邮政编码:我将每个邮政编码映射到一个长的,并通过使用Trove将数百万个键/值对装入内存来节省大量内存。
Trove的优势基本上在于它不会对对象/原语进行持续的装箱/拆箱:在很多情况下,Trove只能直接使用基元和基元。
将String
键编码为long
的示例方法(假设ASCII字符,每个字符一个字节用于简化 - 7位就足够了):
long encode(final String key) {
final int length = key.length();
if (length > 8) {
throw new IndexOutOfBoundsException(
"key is longer than 8 characters");
}
long result = 0;
for (int i = 0; i < length; i++) {
result += ((long) ((byte) key.charAt(i))) << i * 8;
}
return result;
}
使用Trove库。
Trove库已经为基元优化了HashMap
和HashSet
类。 在这种情况下, TObjectIntHashMap<String>
会将参数化对象( String
)映射到基本int
。
首先,您是否测量到LinkedList
确实比HashMap
更具内存效率,或者您是如何得出这个结论的? 其次, LinkedList
的元素访问时间为O(n)
,因此您无法对其进行有效的二进制搜索。 如果你想做这样的方法,你应该使用一个ArrayList
,它可以让你在性能和空间之间做出妥协。 然而,我再次怀疑HashMap
, HashTable
或者 - 特别是 - TreeMap
将消耗更多的内存,但前两个将提供常量访问和树映射对数,并提供一个比普通列表更好的接口。 我会尝试做一些测量,内存消耗的差异究竟是多少。
更新 :正如Adamski指出的那样, String
本身,而不是它们存储的数据结构,将消耗最多的内存,查看特定于字符串的数据结构可能是个好主意,例如尝试 (特别是patricia尝试 ),这可能会减少字符串所需的存储空间。
你看过尝试了吗? 我没有使用它们,但它们可能适合你正在做的事情。
自定义树将具有与O(log n)
相同的复杂性,请勿打扰。 你的解决方案是合理的,但我会使用ArrayList
而不是LinkedList
因为链表每个存储值分配一个额外的对象,这相当于你的案例中的很多对象。
正如Erick所写,使用Trove库是一个很好的起点,因为你在存储int
原语而不是Integer
s中节省了空间。
但是,您仍然面临存储200万个String实例的问题。 鉴于这些是地图中的关键,实习他们不会提供任何好处,所以接下来我要考虑的是是否有一些可以被利用的字符串的特征。 例如:
String
表示常用单词的句子,那么您可以将String转换为Sentence
类,并实习单个单词。 MyString
。 显然,这里的权衡是执行查找所花费的额外时间。 也许你可以使用RadixTree ?
使用java.util.TreeMap
而不是java.util.HashMap
。 它使用红黑二进制搜索树,并且不使用比保存包含地图中元素的注释所需的更多内存。 没有额外的桶,不像HashMap或Hashtable。
我认为解决方案是在Java之外做一点。 如果您有这么多值,则应使用数据库。 如果您不想安装Oracle,SQLite快速而简单。 这样,您不需要的数据就会存储在磁盘上,所有的缓存/存储都会为您完成。 设置具有一个表和两列的DB不会花费太多时间。
我考虑使用一些缓存,因为它们通常具有溢出到磁盘的能力。
问题是对象的内存开销,但使用一些技巧可以尝试实现自己的hashset。 像这样的东西。 像其他人一样,字符串的开销很大,所以你需要以某种方式“压缩”它。 另外,尽量不要在哈希表中使用太多的数组(列表)(如果你做链接类型哈希表),因为它们也是对象,也有开销。 更好的是开放寻址哈希表。
您可以创建符合您需求的密钥类。 也许是这样的:
public class MyKey implements Comparable<MyKey>
{
char[7] keyValue;
public MyKey(String keyValue)
{
... load this.keyValue from the String keyValue.
}
public int compareTo(MyKey rhs)
{
... blah
}
public boolean equals(Object rhs)
{
... blah
}
public int hashCode()
{
... blah
}
}
试试这个
OptimizedHashMap<String, int[]> myMap = new OptimizedHashMap<String, int[]>();
for(int i = 0; i < 2000000; i++)
{
myMap.put("iiiiii" + i, new int[]{i});
}
System.out.println(myMap.containsValue(new int[]{3}));
System.out.println(myMap.get("iiiiii" + 1));
public class OptimizedHashMap<K,V> extends HashMap<K,V>
{
public boolean containsValue(Object value) {
if(value != null)
{
Class<? extends Object> aClass = value.getClass();
if(aClass.isArray())
{
Collection values = this.values();
for(Object val : values)
{
int[] newval = (int[]) val;
int[] newvalue = (int[]) value;
if(newval[0] == newvalue[0])
{
return true;
}
}
}
}
return false;
}
实际上,HashMap和List对于通过zipcode查找int这样的特定任务来说太笼统了。 您应该利用使用数据的知识。 其中一个选项是使用带有存储int值的叶子的前缀树。 此外,如果(我的猜测)很多具有相同前缀的代码映射到相同的整数,它可以被修剪。
通过zipcode查找int将在这种树中是线性的,并且如果代码数量增加则不会增长,在二进制搜索的情况下与O(log(N))相比。
由于您打算使用散列,因此可以尝试基于ASCII值对字符串进行数值转换。 最简单的想法是
int sum=0;
for(int i=0;i<arr.length;i++){
sum+=(int)arr[i];
}
使用定义良好的散列函数散列“sum”。 您将使用基于预期输入模式的哈希函数。 例如,如果你使用除法
public int hasher(int sum){
return sum%(a prime number);
}
选择一个不接近精确2次幂的素数可以改善性能并提供更好的均匀散列键分配。
另一种方法是根据各自的位置权衡角色。
例如:如果使用上述方法,“abc”和“cab”都将被散列到同一位置。 但如果您需要将它们存储在两个不同的位置,请为我们使用数字系统的位置提供权重。
int sum=0;
int weight=1;
for(int i=0;i<arr.length;i++){
sum+= (int)arr[i]*weight;
weight=weight*2; // using powers of 2 gives better results. (you know why :))
}
由于您的样本非常大,因此您可以通过链接机制避免冲突,而不是使用探测序列。 毕竟,您选择的方法完全取决于您的应用程序的性质。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.