Hash算法系列-应用(查找)
2011-09-11 20:56
281 查看
每一种算法的提出都是为了解决某类问题,Hash算法也不例外。Hash算法最常见的几种应用为:Hash表,快速查找用的;一致性Hash算法,缓存系统;SHA之类,加密用的。
快速查找的应用到处都是。HashMap应该说是Java中最经典的一个应用范例。HashMap本身是用一个Hash表和一个链表来实现的。通过对key进行hash,再进行二次hash,将得到的结果同Hash表长度-1进行位与(&)运算确定该key在Hash表中的位置(index)。当然hashmap实现过程中,对于二次hash有深层次的考量。对于HashMap实现有一篇质量很高的文章,摘抄到这里供大家学习。加深对hash算法在查找应用中的理解。
快速查找的应用到处都是。HashMap应该说是Java中最经典的一个应用范例。HashMap本身是用一个Hash表和一个链表来实现的。通过对key进行hash,再进行二次hash,将得到的结果同Hash表长度-1进行位与(&)运算确定该key在Hash表中的位置(index)。当然hashmap实现过程中,对于二次hash有深层次的考量。对于HashMap实现有一篇质量很高的文章,摘抄到这里供大家学习。加深对hash算法在查找应用中的理解。
存取之美——HashMap原理与实践 HashMap是一种十分常用的数据结构,作为一个应用开发人员,对其原理、实现的加深理解有助于更高效地进行数据存取。本文所用的jdk版本为1.5。 使用HashMap 《Effective JAVA》中认为,99%的情况下,当你覆盖了equals方法后,请务必覆盖hashCode方法。默认情况下,这两者会采用Object的“原生”实现方式,即: view plaincopy to clipboardprint? protected native int hashCode(); public boolean equals(Object obj) { return (this == obj); } hashCode方法的定义用到了native关键字,表示它是由C或C++采用较为底层的方式来实现的,你可以认为它返回了该对象的内存地址;而缺省equals则认为,只有当两者引用同一个对象时,才认为它们是相等的。如果你只是覆盖了equals()而没有重新定义hashCode(),在读取HashMap的时候,除非你使用一个与你保存时引用完全相同的对象作为key值,否则你将得不到该key所对应的值。 另一方面,你应该尽量避免使用“可变”的类作为HashMap的键。如果你将一个对象作为键值并保存在HashMap中,之后又改变了其状态,那么HashMap就会产生混乱,你所保存的值可能丢失(尽管遍历集合可能可以找到)。 HashMap存取机制 Hashmap实际上是一个数组和链表的结合体,利用数组来模拟一个个桶(类似于Bucket Sort)以快速存取不同hashCode的key,对于相同hashCode的不同key,再调用其equals方法从List中提取出和key所相对应的value。 Java中hashMap的初始化主要是为initialCapacity和loadFactor这两个属性赋值。前者表示hashMap中用来区分不同hash值的key空间长度,后者是指定了当hashMap中的元素超过多少的时候,开始自动扩容,。默认情况下initialCapacity为16,loadFactor为0.75,它表示一开始hashMap可以存放16个不同的hashCode,当填充到第12个的时候,hashMap会自动将其key空间的长度扩容到32,以此类推;这点可以从源码中看出来: view plaincopy to clipboardprint? void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); } 而每当hashMap扩容后,内部的每个元素存放的位置都会发生变化(因为元素的最终位置是其hashCode对key空间长度取模而得),因此resize方法中又会调用transfer函数,用来重新分配内部的元素;这个过程成为rehash,是十分消耗性能的,因此在可预知元素的个数的情况下,一般应该避免使用缺省的initialCapacity,而是通过构造函数为其指定一个值。例如我们可能会想要将数据库查询所得1000条记录以某个特定字段(比如ID)为key缓存在hashMap中,为了提高效率、避免rehash,可以直接指定initialCapacity为2048。 另一个值得注意的地方是,hashMap其key空间的长度一定为2的N次方,这一点可以从一下源码中看出来: view plaincopy to clipboardprint? int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; 即使我们在构造函数中指定的initialCapacity不是2的平方数,capacity还是会被赋值为2的N次方。 为什么Sun Microsystem的工程师要将hashMap key空间的长度设为2的N次方呢?这里参考R.W.Floyed给出的衡量散列思想的三个标准: 一个好的hash算法的计算应该是非常快的, 一个好的hash算法应该是冲突极小化, 如果存在冲突,应该是冲突均匀化。 为了将各元素的hashCode保存至长度为Length的key数组中,一般采用取模的方式,即index = hashCode % Length。不可避免的,存在多个不同对象的hashCode被安排在同一位置,这就是我们平时所谓的“冲突”。如果仅仅是考虑元素均匀化与冲突极小化,似乎应该将Length取为素数(尽管没有明显的理论来支持这一点,但数学家们通过大量的实践得出结论,对素数取模的产生结果的无关性要大于其它数字)。为此,Craig Larman and Rhett Guthrie《Java Performence》中对此也大加抨击。为了弄清楚这个问题,Bruce Eckel(Thinking in JAVA的作者)专程采访了java.util.hashMap的作者Joshua Bloch,并将他采用这种设计的原因放到了网上(http://www.roseindia.net/javatutorials/javahashmap.shtml) 。 上述设计的原因在于,取模运算在包括Java在内的大多数语言中的效率都十分低下,而当除数为2的N次方时,取模运算将退化为最简单的位运算,其效率明显提升(按照Bruce Eckel给出的数据,大约可以提升5~8倍) 。看看JDK中是如何实现的: view plaincopy to clipboardprint? static int indexFor(int h, int length) { return h & (length-1); } 当key空间长度为2的N次方时,计算hashCode为h的元素的索引可以用简单的与操作来代替笨拙的取模操作!假设某个对象的hashCode为35(二进制为100011),而hashMap采用默认的initialCapacity(16),那么indexFor计算所得结果将会是100011 & 1111 = 11,即十进制的3,是不是恰好是35 Mod 16。 上面的方法有一个问题,就是它的计算结果仅有对象hashCode的低位决定,而高位被统统屏蔽了;以上面为例,19(10011)、35(100011)、67(1000011)等就具有相同的结果。针对这个问题, Joshua Bloch采用了“防御性编程”的解决方法,在使用各对象的hashCode之前对其进行二次Hash,参看JDK中的源码: view plaincopy to clipboardprint? static int hash(Object x) { int h = x.hashCode(); h += ~(h << 9); h ^= (h >>> 14); h += (h << 4); h ^= (h >>> 10); return h; } 采用这种旋转Hash函数的主要目的是让原有hashCode的高位信息也能被充分利用,且兼顾计算效率以及数据统计的特性,其具体的原理已超出了本文的领域。 加快Hash效率的另一个有效途径是编写良好的自定义对象的HashCode,String的实现采用了如下的计算方法: view plaincopy to clipboardprint? for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; 这种方法HashCode的计算方法可能最早出现在Brian W. Kernighan和Dennis M. Ritchie的《The C Programming Language》中,被认为是性价比最高的算法(又被称为times33算法,因为C中乘数常量为33,JAVA中改为31),实际上,包括List在内的大多数的对象都是用这种方法计算Hash值。 另一种比较特殊的hash算法称为布隆过滤器,它以牺牲细微精度为代价,换来存储空间的大量节俭,常用于诸如判断用户名重复、是否在黑名单上等等。
相关文章推荐
- Hash算法系列-应用(负载均衡)
- 算法细节系列(5):二分查找应用
- Hash算法系列-应用(加密)
- hash算法在查找、比较中的应用
- PHP应用系列之二:查找、诊断和加速运行缓慢的代码
- [转]为 PHP 应用提速、提速、再提速!,第 2 部分: 分析 PHP 应用程序以查找、诊断和加速运行缓慢的代码
- vbox系列教程-高级应用(二)
- 【软件测试自动化-QTP系列讲座 45】== JScript在QTP中的应用探究(一) ==
- Android应用开发提高系列(3)——《Effective Java 中文版》读书笔记
- WPF基础到企业应用系列7——深入剖析依赖属性(五)
- 火星人谚语系列之六:一次真实应用
- 【Xamarin开发 Android 系列 9】 创建一个Json读取数据应用-列表页(中)
- 《Entity Framework 6 Recipes》中文翻译系列 (35) ------ 第六章 继承与建模高级应用之TPH继承映射中使用复合条件
- EF之POCO应用系列2——示例入门(转)
- 1900页Python系列PPT分享五:函数设计与应用(134页)
- 大话西游之Office应用实例系列! <6>
- 大型网站架构系列:缓存在分布式系统中的应用(二)
- 进程间通信系列 之 消息队列应用实例
- Win10系列:C#应用控件基础4
- 机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾