数据结构与算法----->数据结构----->哈希表

1.哈希表概述:

  • 数据结构:哈希表
  • 插入时间复杂度:若不发生“冲突”,可以达到O(1)时间级,只需要哈希函数运行时间加一次直接插入所需时间;若是发生冲突,并且程序中是使用开放地址法解决的冲突问题,就需要考虑探测步长来估算整个插入过程所需要的时间。平均探测长度取决于装填因子(表中已有数据项数与表容量的比值),随着装填因子越大,探测长度就会越长。
  • 删除时间复杂度:O(1),同上
  • 查找时间复杂度:
  • 优点:
    • 哈希表的操作速度比较,如插入、删除的时间复杂度都是常量O(1),可以在一秒内查找上千条记录
    • 哈希表的编程实现相对容易
  • 缺点:  
    • 哈希表不能被填满。哈希表被基本填满的时候,性能会急剧下降,所以为了保证性能,你一定要确保你哈希表的容量的大小是足够的
    • 哈希表到其他哈希表的数据迁移过程是一个费时的过程(如定期将数据迁移到更大的哈希表中去会很费时)。如果一开始没有预估好你的数据量的大小,初始时创建的用于存放相应数据的哈希表太小,实际使用过程中就需要创建更大的哈希表,并且定期地将数据转移到更大的哈希表中去,这是一个非常费时的过程。
    • 使用哈希表之前最好能提前预测数据量大小。哈希表是基于数组的,一旦创建,后面想要再进行扩展是非常难的,所以使用哈希表的前提是你可以提前预测你的数据量的大小,否则如果没能准确预估数据量大小,导致实际运行过程中你的哈希表被基本填满,那么你的哈希表的性能会急剧下降。更糟糕的是,如果你没能准确预估数据量大小,后期会极有可能需要将你的数据从当前哈希表迁移到更大的哈希表中,这个过程会很慢。
    • 哈希表中的数据不支持顺序遍历。没有简便的方法来以任何一种顺序(如从小到大)遍历哈希表中的数据,如果需要顺序遍历相应的数据,就不能选用哈希表,只能选用其他的数据结构。
  • 哈希表的适用场景:
    • 当  可以准确预估数据量的大小
    • 并且   不需要顺序遍历(如从小到大)哈希表中数据 时
    • 并且   最好没有重复的关键字(当关键字的值重复时,想要查找到重复关键值对应的所有数据项就得遍历整个哈希表,非常耗时)
    • 哈希表在速度和易用性方面是其他数据结构无法比拟的

2.哈希表相关基础知识点

  概述:要想使用哈希表这种数据结构,首先要了解以下基础知识,只有具备了这些基础知识,才能知道该如何更好地使用哈希表这种数据结构进行编程。

  2.1哈希化过程与哈希函数

    • 概述:哈希化过程就是将新来数据项的关键字的值转化成数组下标(也即新来数据项应该存放在哈希表中的具体位置)的过程
        • 哈希表是一种使用数组实现的数据结构,将数据存放至哈希表的过程其实也是将数据项存放至一个数组中的过程
        • 哈希表和普通数组之间的差别是:普通数组直接存放新来的数据项(如普通无序数组中是直接将新来的数据项存放至现有数组的末尾位置);但是,哈希表存放新来的数据项不是直接将其放在现有表格的末尾,而是先计算新来的数据项的关键字的值对应的哈希值(使用哈希函数来计算),然后根据关键值对应的哈希值来确定新来的数据项应该被放在现有哈希表的什么位置(哈希表:新来的数据项-->该数据项的关键字的值-->关键值对应的哈希值-->数组下标)
        • 有些数据元素的关键值恰好可以直接作为哈希表数组下标,但是一般情况下不可以,一般情况下需要使用哈希函数将数据项的关键值转化成数组下标
    • 哈希函数:哈希化过程涉及到哈希函数,使用哈希函数才能将数据项的关键字的值映射成哈希数组下标。但是,不同应用场景下哈希函数也是不一样的,实际编程过程中,需要根据实际应用背景寻找合适的哈希函数。寻找满足要求的哈希函数的过程是一个复杂的过程,并且最终可能找到多个符合要求的哈希函数,那么这种情况下应该如何选取哪个哈希函数作为你最终使用的哈希函数?另外,你的哈希函数是否还有优化的可能,该朝着什么方向去优化你的哈希函数?
        • 什么样的哈希函数是好的哈希函数?(有效哈希函数应满足的要求) 
          • 首先,好的哈希函数的运算速度应该很快,否则如果你的哈希化过程很慢的话最终你的整个哈希表中的数据操作也会很慢。所以一个好的哈希函数中不应该有很多乘法与除法(遇到乘2的倍数或除2的倍数时,尽量转换成位移运算(<<左移,>>右移),可以使用这种手段优化你的哈希函数,提升哈希函数的运算速度)
          • 其次,你的哈希化结果最好能使计算出的数组下标值随机的分布在整个哈希表中,否则如果出现“聚集”现象,那么后期哈希表的速度会大幅降低。 当然,能否使哈希化结果中得到的数组下标值随机分布在整个哈希表中,除了依靠好的哈希函数之外,还受制于源数据集中各个数据项的关键值的随机分布状况,如果你的源数据集中各个关键值是随机分布的,那么你的哈希化结果也很容易随机分布在整个哈希表中,但是很多情况下源数据集的关键字的值并不是随机分布的,源数据集的关键值的分布本身就有聚集现象(如关键值取值范围为00000-99999,但是根据关键字的实际意义,有可能很多值根本就不可能被取到,如13000-50000是保留字段,那么源数据集的关键值其实只分布在00000-12999和50001-99999之间,这种情况下源数据集的关键值就是有聚集现象) 
          • 首先,你的哈希函数产生的数组下标的个数不要太少,尤其更不应该小于数据量大小。最好不要有多个关键字的值对应于一个数组下标
          • 其次,你的哈希函数产生的数组下标的个数不要太多,不要让它远远大于数据量大小。最好不要使你的哈希表数组有太多空缺的位置,也就是说你的哈希函数定得不合理的情况下,你的哈希函数产生的数组下标数目太多,远远超过你的数据量的情况下,虽然可以避免多个关键值对应于一个数组下标的情况,但是又会产生另外一个问题那就是,使用这种哈希函数去计算数组下标并在哈希表中存储数据项,会使得哈希表数组的很多位置空着,根本不会有数据项的关键值对应于该位置,也就不会有数据项被存储到该位置,造成空间的大大浪费
        • 如何寻找满足要求的哈希函数?(Tips)    
          • 遇到乘或者除以2的倍数这样的运算时,转化成位移运算(左移或右移相应位数),这样可以提高哈希函数计算速度,使得你的哈希函数更易满足速度要求
          • 对数据项的关键值进行预处理,去掉关键值中的无用字段,但是要小心不要去掉任何有用字段
          • 有时候关键字的取值范围太大,超出int、long型变量所能表示的范围,可以使用如下方法解决“溢出”问题 
          • 尽量保证哈希表数组容量是质数,这样可以更大程度地避免“聚集”现象的发生
        • 哈希函数实例一:哈希化字符串(这个例子讲述了程序员寻找满足要求的哈希化字符串的哈希函数的过程)
          • 详情参见《数据结构与算法》一书的P416/595-P420/595,   P449/595-P451/595    

  2.2哈希化过程中的“冲突”问题

    概述:在使用哈希函数将数据项的关键值映射到一个数组下标的过程中,有可能会将多个数据项的关键值映射成一个数组下标,这种问题被称作“冲突”,当冲突发生时,怎么办?譬如,新来了一个数据项,我将该数据项的关键值通过哈希函数映射成一个数组下标,想要去该数据项的时候发现数组那个位置已经存在了一个数据项,那么我的新的数据项应该被存放在哪里呢?

    解决办法:解决上述“冲突”问题有多种办法,可以使用开放地址法或者链地址法来解决上述问题。开放地址法的思路是:当冲突发生时,也就是使用哈希函数计算出的数组下标处已经有其他数据项存在时,从当前下标处开始向下查找,找到第一个空白位置或者标记为“deleted”的位置,将新来的数据项插入。根据查找空白位置的方法的不同,又将开放地址法分成线性探测、二次探测、再哈希法。除了开放地址法可以解决冲突问题之外,还可以使用链地址法来解决可能发生的冲突问题,链地址法的思路是在哈希表每个单元中设置链表,这样一来当冲突发生时,将新来的数据项封装成链节点存放至哈希化所得下标对应的链表的表头即可解决冲突问题。

  2.3开放地址法解决“冲突”问题

    概述:在使用哈希函数将数据项的关键值映射到一个数组下标的过程中,有可能会将多个数据项的关键值映射成一个数组下标,这种问题被称作“冲突”,当冲突发生时,怎么办?譬如,新来了一个数据项,我将该数据项的关键值通过哈希函数映射成一个数组下标,想要去该数据项的时候发现数组那个位置已经存在了一个数据项,那么我的新的数据项应该被存放在哪里呢?前面我们已经知道,哈希表存放数据的时候需要你事先估算数据量的大小,并且哈希表的容量应该要大于数据量大小,这样才能保证性能。一般情况下我们取哈希表容量为数据量大小的二倍。这样一来其实哈希表中就有一半的位置是空的,这样一来当“冲突”发生时,也就是说新来的数据项根据哈希函数计算出的数组下标位置处已经被占用的情况下,可以放弃哈希函数得到的数组下标,转而从该下标位置开始向下寻找空的位置,并将新的数据项存放在该下标后面第一个空位置。(例如:新数据项经哈希函数计算得到下标值为345,但是发现该位置已经有数据项存在,也就是“冲突”发生了,这时使用“开放地址法”解决冲突问题,从下标346开始查找哈希表数组中空位置,譬如发现346处就是空的,那么把新数据项放在哈希表数组346下标位置即可。这只是简单地举了一个例子,实际编程过程中,使用开放地址法解决“冲突”问题时,冲突发生时寻找该冲突数组下标后面第一个空白数据单元时又有三种方法,分别是线性探测法、二次探测法和再哈希法。)

    开放地址法中寻找下一个空白数据单元时又有三种方法:

      • 方法一,线性探测法解决冲突问题。(编程思路)
        • 插入过程: 
          •    
          • 插入过程中会在第一个空白单元或者标记为“Deleted”的位置插入新的待插入数据项.如果一直查找到哈希表的尾部都没有查找到符合要求的单元,就将指针指向哈希表数组的起始位置,即数组头部,再从哈希表头部开始向下查找。但是为了防止无限循环,还应该防止哈希表已满的情况发生。
        • 查找过程:上述过程描述的是新来的数据项插入的过程,下面描述查找过程,

          • 查找按钮将标记为“deleted”的数据项认作存在的数据项而非空白单元,find程序可以跨过标记为“deleted”的数据单元继续向下查找冲突的数据项,直到真的找到冲突的数据项或者遇到空白单元(总结:find函数只有在遇到空白数据单元或者真的找到冲突数据项时才会停止查找,遇到标记为“deleted”的数据单元时并不会停止查找)。
          • 如果一直查找到哈希表的尾部都没有查找到符合要求的单元,就将指针指向哈希表数组的起始位置,即数组头部,再从哈希表头部开始向下查找。但是为了防止无限循环,还应该防止哈希表已满的情况发生。
        • 删除过程: 

      • 方法二,二次探测法解决冲突问题。(编程思路)
          • 线性探测法会产生聚集问题,聚集现象越严重,哈希表的速度就会越慢。二次探测可以在一定程度上控制聚集现象:其思想是探测相隔较远的单元而不是相邻的单元。
          •  

                
      • 方法三,再哈希法解决冲突问题。(编程思路)
          •  

                

  2.4链地址法解决“冲突”问题

    概述:在使用哈希函数将数据项的关键值映射到一个数组下标的过程中,有可能会将多个数据项的关键值映射成一个数组下标,这种问题被称作“冲突”,当冲突发生时,怎么办?譬如,新来了一个数据项,我将该数据项的关键值通过哈希函数映射成一个数组下标,想要去该数据项的时候发现数组那个位置已经存在了一个数据项,那么我的新的数据项应该被存放在哪里呢?前面一个小节(2.3节)中我们已经介绍了“开放地址法”来解决“冲突”问题,其实还有另外一种方法来解决冲突问题,那就是“链地址法”:使用这种方法的哈希表数组中不直接存放数据项,而是存放数据项链表的地址,当冲突发生时,也就是说根据哈希函数计算出的数组下标位置已经有其他数据项存在时,可以将该数据项封装成链表节点,并将新数据项对应的链表节点作为该位置整个链表的表头节点。

3.如何使用哈希表进行数据存储(编程思路)

  step1,了解项目需求,预估数据量大小,创建哈希表使得哈希表容量大于数据量大小(一般取为其2倍)

      尽量保证哈希表数组容量是质数,这样可以更大程度地避免聚集现象的发生  

  step2,寻找满足要求的哈希函数

      哈希函数的作用:使用哈希函数计算出每一个数据项的关键字的值所对应的哈希表数组下标,要哈希化的关键值类型不同,取值范围不同,实际满足要求的哈希函数也是不同的,可能要经过一个复杂的过程找到合适的哈希函数

       哈希函数应满足的要求:详情参见“本文第2.1节”内容

  step3,使用step2中所得哈希函数计算数据项关键字的值对应的数组下标

  step4,根据实际应用场景选择工程想要使用的解决“冲突”问题的办法

      • 使用哈希函数计算数据项关键值对应的数组下标的时候,可能出现冲突问题,即有可能多个数据项的关键值经哈希函数计算所得的数组下标是一样的,所以编程实现数据项的插入、查找、删除函数时要考虑到有可能会出现“冲突”问题,编写出能够解决冲突问题的代码。而解决冲突问题的办法又有很多种,如开放地址法(开放地址法又分成线性探测、二次探测、再哈希法),链表法都可以解决冲突问题,实际编程时,需要权衡利弊最终选定一种方法进行编程,如选定开放地址法中的线性探测法来编写实现在哈希表中插入、查找、删除数据项的成员函数。

  step5,明确step4中所选定的解决“冲突”问题的方法下的    编程思路

  step6,根据step5中所得编程思路编写成员函数insert()、find()、delete()...

 本部分内容小结:由上面的步骤可以看出,使用哈希表存储数据集并且操作数据集,其相关程序中最重要的部分有两个:第一个便是哈希函数,针对具体应用需求找到合适的哈希函数是一个很复杂的过程;第二个便是解决“冲突”问题的办法,确定了哈希函数也只是确定了关键值和哈希数组下标的对应关系而已,前面已经讲过,哈希化结果极有可能出现“冲突”,而解决冲突的办法又有许多种,选择不同的解决冲突的办法,其insert()、find()等成员函数的编程思路是不一样的,只有确定了解决冲突问题的方法,才能确定编程思路,最终才可以编出满足要求的程序。一句话总结:编写哈希表操作数据集的相关程序,首先要确定哈希函数的具体形式,其次要确定解决冲突问题的具体方法(也就是insert()等函数的编程思路),最后才去编程。

4.使用哈希表进行数据存储的例子

  4.1实例一,数据项的关键字的值直接就可以作为哈希表数组下标(这种情况下哈希表就变成了普通数组

      问题:

                    

           

           

 

 

 

  4.2实例二,哈希化字符串:用哈希表来存储英文单词,实现一个英文词典,将该词典放在内存中,用于快速地单词查找翻译等等

       项目需求:需要将一本英语词典的每一个英文单词都写到计算机内存中,以便于快速读写,那么哈希表是一个不错的选择。在计算机内存中使用哈希表这种数据结构存储一本英语词典:需要先在计算机中分配一块内存空间,将待存储的单词看成一个个的数据项,数据项的关键字其实就是单词本身这个字符串,那么需要先将数据项的关键值(也就是单词本身)先使用哈希函数映射到一个int型的数值,并将该int值作为哈希表数组下标,最终将数据项(单词)存储到上述过程计算出的数组下标处。

         编程思路:

          step1,明确本工程的需求,其实是需要在内存中开辟一块空间存储一本英文词典的所有单词

          step2,确定如何组织单词的存储结构,为了使得查询速度更快,会选择使用哈希表这种数据结构来存储相关单词

          step3,哈希表其实就是一数组,那么你的单词应该被存放在哈希表数组的什么位置呢?

          • 这一阶段需要将单词哈希化,也即需要使用哈希函数将英文单词变成数组下标。那么这个哈希函数又应该是怎样的呢?实际上有很多种哈希函数,但是不同的哈希函数的最终的应用效果不同,实际编程过程中可能会需要一个循序渐进的过程来优化你的这个哈希函数,最终找到一种满足要求的哈希函数。
          • 对于使用哈希表来存储英文词典的所有单词这个案例来说,开发者经过一系列的探索,最终将本例中的哈希函数确定为如下的样子:(关于如何找出的这个哈希函数,详情参见“《数据结构与算法》P416/595-P***/595”)

          step4,确定数据量大小,取哈希表容量为数据量大小的二倍(如词典中共5000个单词,可以取哈希表容量为10000)      

          step5,假设该词典只有小写字母,给a,b,c.......z 以及空格各分配一个数字来表示

                a b c d e ... z 空格

                1 2 3 4 5 ...26 27

          Step6, 假定这个词典一共要存储50000个单词,那么数组大小就该定为2*50000(字典大小的   二倍)

                arraySize=2*50000=100000

          Step7, 给定一个单词,计算这个单词对应的数字

                例,给定单词add

                Add所对应的数字hugeNumber=1*27*27+4*27+4

          Step8,使用哈希函数由hugeNumber获得数组下标(使用哈希函数将step3中对应的数字变换成 一个较小的数字作为该单词add对应的数组下标

                arrayIndex=hugeNumber % arraySize

          Step9:向数组中存入该单词

                Dictionary[arrayIndex]=”add”;

              注意,这一步中可能出现“冲突”,具体信息及解决办法参见本文第2.2-2.4这三个小节的内容。

          Step10:数组就是哈希表(因为该数组使用了哈希函数来存储数组元素)

  4.3哈希化字符串:高级计算机语言(如java、C++)的   编译器

       项目需求:程序编译器在编译程序的过程中需要将程序中声明的变量、函数名等等统统放在内存中,这样后面链接、运行才能快速查找到这些变量、函数并使用其 值或者运行其函数体,那么内存中应该如何存放这些变量、函数名称才能使得后续的查找过程变得便捷,查找速度变得很快呢,答案就是在内存中使用哈希表这种数据结构来存储程序中的相关变量、函数(变量名和函数名作为相关数据项的关键字),就能保证后续查找过程的速度。(内存哈希表中实际是存储一个个数据项,只不过数据项的关键值是变量名称、函数名称...实际上一个数据项要包含很多东西的,如变量数据项就包含变量名、变量在内存中的地址、变量的值等等)

posted on 2016-12-03 11:53  LXRM-JavaWeb、ML  阅读(281)  评论(0编辑  收藏  举报

导航