`
风子柒
  • 浏览: 55552 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
社区版块
存档分类
最新评论

从散列到HashMap的简单实现

阅读更多
         在程序中,我们常常用数组和链表来保存一些数据。作为两种最基本也最常用的保存数据的结构,数组和链表也是各有千秋:数组可以通过下标提供十分高效的查找,而链表可以利用本身在内存中的离散分布特性进行灵活的增删操作。两者可以说是各有优劣,但是为什么不能将两者的优点结合起来呢?这样不就可以提供一种既方便插入又方便查找的数据结构吗?是的,从你开始在Google上输入关键字搜索的时候你就已经知道答案了,它就是HashMap。

         在介绍HashMap之间,首先要介绍一下散列技术。散列就是hash的中文名字,虽然我觉得叫做哈希更加的fashion。散列技术是为了解决符号表的增删查问题应运而生的。我们知道,符号表是一种抽象数据类型(ADT),它基于无序集合,支持增删查三种基本操作。在Hash技术之前,我们有两种方法实现符号表的基本功能:位向量和集合的链表,但它们都有一个共同的弊病:时间复杂度高。而散列技术可以将符号表的操作的平均时间保持在一个常数水平。这也是散列技术受到欢迎的重要原因。
         散列可以分为两种:开散列(外部散列)和闭散列(内部散列)。开散列,顾名思义,是一种open的散列方式,它支持容量的动态分配,也就是说,它存储数据的空间可以说是无穷的(前提是你的计算机可以提供无穷的空间),而站在对立面的闭散列则是将数据保存在一个密闭的空间内,是一个闭关锁国政策的骨灰级粉丝。我们可以将它们想象成一堆保存数据的水桶,这些桶按照一定的规律摆放,开散列可以在一个桶里存放多个集合中元素,但是这些元素都有共同的特性;而闭散列的宪法规定,一个桶里只能有一个集合元素,其他的要想进来:没门!
         这里主要介绍开散列。前面提到开散列的桶里的元素有共同的特性,这个特性就是由hash函数决定的。Hash函数会为每个想要进来的元素进行检查和分组,然后发给他们一个号码牌,对“桶”入座。这点有点像垃圾分类,可回收垃圾请投入绿色垃圾桶,不可回收垃圾请自觉投入旁边的黄色桶(爱护地球,从我做起!)。散列函数也是有多种多样的。下面取几种常见的进行介绍:
         取余法:这个最常用也最没有技术含量。直接对key值的hashcode(什么是hashcode请关注www.google.com或者我未来的blog,或者,you let me know!)进行取余运算,然后得到一个哈希值h(x),作为保存的位置;这种方法简单易懂而且效果不错;
         随机数法:这个相信大家可以明白,h(x)=random(k),这种方法也是很简单,但是效果一般不如第一种;
         叠加法:一般我们可以把要查找的值的hashcode各位数相加得到一个新的整数,一般的hashcode是8、9位,因此得到的新的整数一般不会超过81,当然这样不符合散列表的动态扩容的特性,因此我们可以采取另一种叠加的方式,例如hashcode为123456789,我们可以将其分为三组,然后相加:123+456+789=1368,对于这个散列函数,散列表最大的大小是2997。如果还是数字大于2997,可以再将其分组:29+97。当然,这里只是一种示例,相信聪明如你,一定会有办法解决和完善。

         聪明如你,肯定会发现我们得到的h(x)会出现重复,专业一点叫做冲突,这时就需要一个救世主出来拯救世界了。还好,电视上说了:如果冲突在所难免,重新散列在你身边。重新散列(rehash)就是散列冲突的终结者。常见的rehash一般有两种(参考《Data Abstraction and Problem Solving With JAVA-walls and mirrors》Frank M.Carrano & Janet J.Prichard):
        1.开放寻址:即如果一个待插入的位置已经被其他的元素占据,则探测其他的空位置来放置新项。当然,这种方案必须找到其有效查找的方法。开放寻址一般有三种方案:
         ①线性探测:这是一种相对简单的检测方案,当a位置已经被占据了,则探测a+1、a+2位置,以此类推,直到找到可用位置。但是这种方案增加了删除的复杂度,而且还会出现主要聚集的现象(表中包含多组连续占用的位置),这会降低散列的总体效率;
         ②二次探测:这种方案是不从原始位置开始探测,而是有规律地检查a+1^2、a+2^2等等,以此类推。当然这种方法也会导致聚集的发生;
         ③双散列:这种方案可以极大的减少聚集。双散列比较麻烦,它定义了两个hash函数h1、h2,h1以查找的key为自变量,产生一个0到m-1的整数x,h2同样以key作为自变量,得到一个1到m-1之间且和x互为素数的整数y,然后将y作为增量,直到找到空位为止。
        2.重新构建散列表:这种方案是改变了表的结构。一般有以下两种方法:
         ①:这是将数组里存储的元素也定义为数组,但是这个元素级别的数组需要有一个合适的大小,若太小则不能很好的解决冲突,若太大又可能造成空间的浪费;
         ②分离链表:可以把数组当成元素进行保存,当然也就可以把链表放进去,而且链表具有灵活增删的优点。这样,表中的每一个项保存的都是一个链表的引用,而所要保存的元素都在链表中。

         下面介绍一个我自己实现的MyHashMap。
         好的,在这里我可以很负责的告诉你,下面的HashMap简单实现真的不是吹的,那真的不是一般的简单,它只是用来介绍HashMap的实现的思想的,谁当真谁就输了。其实,如果你理解了HashMap的禅理,那么相信你一定可以写一个更好的出来。但是,如果你想在被砖头拍一下再得到璞玉的话,那么我就抛一个砖出来吧!客官,你可接好喽!
         首先,MyHashMap的实现了put、get、keyset、values、remove、isempty、containskey、clear和size这几个功能。它使用了分离链表法。它首先创建了一个保存链表的数组,默认的初始容量是10,默认的装载因子是0.85(Sun提供的HashMap的默认装载因子是0.75,这里因为为了使内存的浪费程度降低,我将其提升至0.85)。另一方面,我把要保存的内容包装成了一个MyHashElem对象,它拥有Key和Value两个属性。
         HashMap的核心当然是Hash函数。这里用了一个很简单的方法得到要保存的key值的散列位置。



         看到了吧,这种hash值的求解和sun提供的比起来,看上去的专业水平确实不是一两个层次的(不过取余是最常用的hash函数)。但是,前面说过,这个MyHashMap是用来了解HashMap的实现原理与机制的。因此大家就不要追究了,嘿嘿。
         其实,与其说自己的方法很简单,不如说我的方法很fashion。这里可以从我的rehash方法看出来:



         我知道,又有人会惊呼:Daddy Trap!(翻译为:坑爹啊!!)这个也太浪费内存了吧。是的,我当初也考虑到了,但是想想我做这件事的初衷只是为了了解HashMap的机制,而且这个可以节省rehash的时间,不是吗?而且,这里是很有优化的空间的,这里的思想是减少数组复制的时间,我们可以利用数学方法或其它操作使得其旧数组元素在新的数组的下标呈现出一定的规律性。我的简单实现是以大量内存的浪费为惨痛代价的。有细心的哥们姐们会觉察到:抛开内存浪费不说,这样固然节省了rehash的时间,但是查找呢?假如我插入的是345,当容量还够的时候,345%10=5,所以插在第六个位置,但是在一次扩充后,再查找345,位置就变成了345%100=45,也就是第46个位置,怎么会查到呢?别急,先看我查找的方法:



         这样就可以保证能够遍历到我们要找的元素了。这里先得到一个链表,即先查找46号位置上的链表是否包含有我们要找的元素,如果没有再继续往回找6号。这样从平均时间水平上讲,可以在一定程度上降低查找的时间复杂度,当然还有优化的空间,读者不妨一试。
         相信解决了hash和rehash之后,其余的功能对熟悉编程的人来说,肯定是手到擒来了。所以也没必要去展示和讲解其它的代码了,毕竟都是散列以外的知识了。不过有一点需要注意,就是HashMap的特性:key值不能重复!因此在put的时候,需要特别注意key值的检测。
         MyHashMap的实现其实是毫无技术含量可言,至少我是这么认为的,但是如果可以在一定程度上帮助你理解HashMap的实现,也算是功德一桩了。最后还是那句话:能力有限,欢迎拍砖!
  • 大小: 64.7 KB
  • 大小: 61.2 KB
  • 大小: 76.1 KB
2
3
分享到:
评论
5 楼 青春的、脚步 2012-12-31  
楼主书法练过
4 楼 guokwei 2012-09-29  
这书法,眼前一亮啊
3 楼 sghcel 2011-10-30  
字体太非主流了
2 楼 pywepe 2011-10-30  
你的eclipse的字体 真个性  行业少有的个性
1 楼 javafound 2011-10-29  
引用
如果冲突在所难免,重新散列在你身边

相关推荐

    Go-Go的hashmap使用加密随机种子散列提示开放寻址和罗宾汉哈希

    下面将详细解释这些概念以及它们如何在Go的HashMap实现中发挥作用。 首先,**加密随机种子**(Cryptographically Secure Pseudo-Random Number Generator, CSPRNG)被用于初始化哈希函数的种子。Go的HashMap不再...

    hashmap 集合

    在深入理解HashMap之前,我们先简单回顾一下Java集合的基本概念。 Java集合框架包括Set、List和Map三个主要接口。其中,Map接口不同于Set和List,因为它不存储重复元素,而是通过键来唯一标识每个值。HashMap就是...

    Go-rhh一个简单而高效的Go的hashmap包

    本文将深入探讨标题提及的"Go-rhh",这是一个专为Go设计的简单而高效的HashMap实现。 "Go-rhh"这个包采用了开放寻址和罗宾·胡德(Robin Hood)散列策略,这两个概念都是解决哈希冲突的方法。开放寻址是指当哈希...

    散列表(HashMap)

    这种方法称为开放寻址法,但这里使用了二重散列来确定冲突时的下一个位置,而非简单地线性探测下一个位置。 综上所述,散列表通过高效的散列函数提供快速的数据访问,而二重散列是解决冲突的一种有效策略。在实际...

    Java 散列存储详解及简单示例

    散列存储的数据结构如HashMap利用散列码将对象映射到数组索引,通过处理冲突来保证数据的有效访问。理解并正确实现这些概念对于优化Java程序性能至关重要。在实际编程中,应根据具体需求选择合适的数据结构,合理...

    JAVA 实现通讯录

    考虑到通讯录可能需要频繁地添加、删除和查找联系人,散列(HashMap)数据结构是一个不错的选择,因为它提供了高效的插入、删除和查找操作。我们可以创建一个`ContactBook`类,包含一个`HashMap, Contact>`,其中键...

    ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

    HashTable 和 HashMap 的实现原理几乎一样,差别无非是 HashTable 不允许 key 和 value 为 null,HashTable 是线程安全的,但是 HashTable 线程安全的策略实现代价却太大了,简单粗暴,get/put 所有相关操作都是 ...

    哈希表java实现及简单演示

    哈希表,又称为散列表,是一种数据结构,它通过使用散列函数将键(Key)映射到数组的特定位置来实现快速访问。在Java中,哈希表的实现主要依赖于`java.util.HashMap`类,它是基于哈希表的Map接口实现。在这个Java版...

    哈希表实现简单说明-附代码

    哈希表是一种高效的数据结构,它通过特定的哈希函数将键(key)映射到一个固定大小的数组中,以此实现快速查找、插入和删除操作。哈希表的理论基础在于散列技术,它的核心目标是使得数据的访问时间复杂度接近O(1)。 ...

    CrudHashMapJava:Java Crud utilizando HashMap

    HashMap是一种散列映射容器,它存储元素以键值对的形式,其中键(Key)是唯一的,并用于查找对应的值(Value)。HashMap的内部实现基于哈希表,允许快速的插入、查找和删除操作,平均时间复杂度为O(1)。 **创建...

    java实现简单通讯录

    在本文中,我们将深入探讨如何使用Java来实现一个简单的通讯录系统。首先,我们要理解通讯录的基本需求,它通常包括用户认证、联系人信息的存储、检索、添加、删除和修改等功能。Java作为一门强大的面向对象编程语言...

    数据结构 实现 源代码

    对于初学者,可以从简单的数据结构如数组和链表开始,逐渐挑战更复杂的结构,逐步提升自己的编程水平。而对于有一定经验的开发者,深入研究这些源代码,可以深化对数据结构优化和性能调优的理解,提高软件的运行效率...

    数据结构中哈希表的实现代码

    哈希表,也被称为散列表,是数据结构中一种高效的数据存储方式,它通过特定的哈希函数将关键字(key)映射到一个固定大小的数组中,从而实现了快速的查找、插入和删除操作。在计算机科学中,哈希表是解决“查找问题...

    java课程详细讲解(针对初学者)

    - **散列映射HashMap和树映射TreeMap**:提供键值对存储,HashMap以非排序的方式快速查找,TreeMap则保持键的排序。 5. **多线程** - **线程创建**:通过实现Runnable接口或继承Thread类来创建线程。 - **同步...

    Java面试题之HashSet的实现原理

    在HashSet的源代码中,我们可以看到HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。 HashSet的源代码中,我们可以看到...

    2019阿里内推面经1

    在Java的集合框架中,哈希表是一种常用的数据结构,主要体现在`HashMap`和`HashTable`等实现类中。这两个类都是用来存储键值对的数据结构,但是它们之间存在一些重要的区别。 首先,`HashTable`是线程安全的,这...

    Java面试宝典V8.0(1).pdf

    16. **HashMap底层实现**:HashMap基于哈希表(数组+链表/红黑树),通过key的哈希值定位到数组位置,链表解决哈希冲突,当链表长度达到一定阈值时会转换为红黑树,以优化查找性能。 17. **HashMap扩容与resize**:...

    Advance-Data-structures-implementation:使用不同的数据结构并在Java中实现算法

    总的来说,这个项目不仅涵盖了基础的数据结构知识,还深入到特定的散列算法实现,这对于提升Java程序员在处理大数据和性能优化方面的能力大有裨益。通过实践这些代码,我们可以加深对数据结构和算法的理解,为未来的...

    java版数据结构与算法

    哈希表(HashMap)通过散列函数快速定位元素,提供O(1)的平均查找时间。它在Java中由HashMap类实现,适用于大量数据的快速存取。 **六、高级排序** 高级排序算法如快速排序、归并排序、堆排序等,它们在处理大量...

    线性开型寻址 单步演示软件

    这种方法相对于开放寻址法中的其他策略(如二次探测和双哈希)来说,实现简单,但可能会形成聚集现象,即某些区域的槽位被频繁使用,而其他区域则相对空闲。 Java是广泛使用的编程语言,其丰富的库函数和面向对象...

Global site tag (gtag.js) - Google Analytics