zoukankan      html  css  js  c++  java
  • 用于查找的HASH算法

    1.Hash介绍

         Hash这个在实现某些功能的经常会用到的数据结构,在java和c++ 里面都有相应的封装好的数据结构:C++ STL Map  java 有HashMap TreeMap。

    计算理论中,没有Hash函数的说法,只有单向函数的说法。所谓的单向函数,是一个复杂的定义,大家可以去看计算理论或者密码学方面的数据。用“人 类”的语言描述单向函数就是:如果某个函数在给定输入的时候,很容易计算出其结果来;而当给定结果的时候,很难计算出输入来,这就是单项函数。各种加密函 数都可以被认为是单向函数的逼近。Hash函数(或者成为散列函数)也可以看成是单向函数的一个逼近。即它接近于满足单向函数的定义。

    Hash函数还有另外的含义。实际中的Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。除此以外,Hash函数往往应用于查找上。所以,在考虑使用Hash函数之前,需要明白它的几个限制:

    1. Hash的主要原理就是把大范围映射到小范围;所以,你输入的实际值的个数必须和小范围相当或者比它更小。不然冲突就会很多。
    2. 由于Hash逼近单向函数;所以,你可以用它来对数据进行加密。
    3. 不同的应用对Hash函数有着不同的要求;比如,用于加密的Hash函数主要考虑它和单项函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率。

    应用于加密的Hash函数已经探讨过太多了,在作者的博客里面有更详细的介绍。所以,本文只探讨用于查找的Hash函数。
    Hash函数应用的主要对象是数组(比如,字符串),而其目标一般是一个int类型。以下我们都按照这种方式来说明。

    2. 常用的Hash 算法

    有时候hash函数是一个压缩映像,因此不可避免会发生冲突,因此在建造hash’函数的时候不仅要设定一个好的hash函数,还要设定一种处理冲突的方法,哈希造表,散列表。

    1. 直接定址法 :地址集合 和 关键字集合大小相同
    2. 数字分析法 :根据需要hash的 关键字的特点选择合适hash算法,尽量寻找每个关键字的 不同点
    3. 平方取中法:取关键字平方之后的中间极为作为哈希地址,一个数平方之后中间几位数字与数的每一位都相关,取得位数由表长决定。比如:表长为512,=2^9,可以取平方之后中间9位二进制数作为哈希地址。
    4. 折叠法:关键字位数很多,而且关键字中每一位上的数字分布大致均匀的时候,可以采用折叠法得到哈希地址,
    5. 除留取余法除P取余,可以选P为质数,或者不含有小于20的质因子的合数
    6. 随机数法:通常关键字不等的时候采用此法构造哈希函数较恰当。

    实际工作中需要视不同的情况采用不同的hash函数:

    1. 考虑因素:计算哈希函数所需要的时间,硬件指令等因素。
    2. 关键字长度
    3. 哈希表大小
    4. 关键字分布情况
    5. 记录查找的频率。(huffeman树)

    处理冲突的方法:

    1. 开放地址法:现行探测再散列 只要哈希表为填满,总能找到一个不冲突的地址,二次探测再散列 表长为素数时才可能保证总能找到一个不冲突的地址,随机探测再散列取决于伪随机数列
    2. 再哈希法:不易发生聚集,但是增加了计算的时间
    3. 链地址法;Chord协议中,一致性hash有应用。

     对不同hash函数以及处理冲突的方法,计算了查找成功的平均长度以及不成功的平均长度。这些结果主要取决于装填因子,装填因子,可以将平均查找长度限定在一个范围内。

    一般的说,Hash函数可以简单的划分为如下几类:

    1. 加法Hash;
    2. 位运算Hash;
    3. 乘法Hash;
    4. 除法Hash;
    5. 查表Hash;
    6. 混合Hash;
    下面详细的介绍以上各种方式在实际中的运用。

    一 加法Hash

    所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果。标准的加法Hash的构造如下:

    1. static int additiveHash(String key, int prime)
    2. {
    3.    int hash, i;
    4.    for (hash = key.length(), i = 0; i < key.length(); i++)
    5.     hash += key.charAt(i);
    6.    return (hash % prime);
    7. }

    这里的prime是任意的质数,看得出,结果的值域为[0,prime-1]。

    二 位运算Hash

    这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素。比如,标准的旋转Hash的构造如下:

    1. static int rotatingHash(String key, int prime)
    2. {
    3.    int hash, i;
    4.    for (hash=key.length(), i=0; i
    5.      hash = (hash<<4>>28)^key.charAt(i);
    6.    return (hash % prime);
    7. }

    先移位,然后再进行各种位运算是这种类型Hash函数的主要特点。比如,以上的那段计算hash的代码还可以有如下几种变形:

    1. hash = (hash<<5>>27)^key.charAt(i);
    2. hash += key.charAt(i);
    3. hash += (hash << 10);
    4. hash ^= (hash >> 6);
    5. if((i&1) == 0)
    6. {
    7. hash ^= (hash<<7>>3);
    8.   }
    9. else
    10.   {
    11.    hash ^= ~((hash<<11>>5));
    12.   }
    13. hash += (hash<<5>
    14. hash = key.charAt(i) + (hash<<6>>16) ? hash;
    15. hash ^= ((hash<<5>>2));

    三 乘法Hash

    这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)。比如,

    1. static int bernstein(String key)
    2. {
    3.    int hash = 0;
    4.    int i;
    5.    for (i=0; i
    6.    return hash;
    7. }

    jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。

    使用这种方式的著名Hash函数还有:

    1. // 32位FNV算法
    2. int M_SHIFT = 0;
    3.     public int FNVHash(byte[] data)
    4.     {
    5.         int hash = (int)2166136261L;
    6.         for(byte b : data)
    7.             hash = (hash * 16777619) ^ b;
    8.         if (M_SHIFT == 0)
    9.             return hash;
    10.         return (hash ^ (hash >> M_SHIFT)) & M_MASK;
    11. }

    以及改进的FNV算法:

    1. public static int FNVHash1(String data)
    2. {
    3.         final int p = 16777619;
    4.         int hash = (int)2166136261L;
    5.         for(int i=0;i
    6.             hash = (hash ^ data.charAt(i)) * p;
    7.         hash += hash << 13;
    8.         hash ^= hash >> 7;
    9.         hash += hash << 3;
    10.         hash ^= hash >> 17;
    11.         hash += hash << 5;
    12.         return hash;
    13. }

    除了乘以一个固定的数,常见的还有乘以一个不断改变的数,比如:

    1. static int RSHash(String str)
    2. {
    3.         int b    = 378551;
    4.         int a    = 63689;
    5.         int hash = 0;
    6.  
    7.        for(int i = 0; i < str.length(); i++)
    8.        {
    9.           hash = hash * a + str.charAt(i);
    10.           a    = a * b;
    11.        }
    12.        return (hash & 0x7FFFFFFF);
    13. }

    虽然Adler32算法的应用没有CRC32广泛,不过,它可能是乘法Hash里面最有名的一个了。关于它的介绍,大家可以去看RFC 1950规范。

    四 除法Hash

    除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用。需要注意的是,我们在前面看到的hash的 结果除以一个prime的目的只是为了保证结果的范围。如果你不需要它限制一个范围的话,可以使用如下的代码替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。
    五 查表Hash

    查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快的实现方式。下面是CRC32的实现:

    1. static int crctab[256] = {
    2. 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
    3. 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
    4. };
    5. int crc32(String key, int hash)
    6. {
    7. int i;
    8. for (hash=key.length(), i=0; i
    9.     hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ k.charAt(i)];
    10. return hash;
    11. }

    复制代码

    查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。

    六 混合Hash

    混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少在面向查找的Hash函数里面使用。

    七 对Hash算法的评价

    http://www.burtleburtle.net/bob/hash/doobs.html 这个页面提供了对几种流行Hash算法的评价。我们对Hash函数的建议如下:

    1. 字符串的Hash。最简单可以使用基本的乘法Hash,当乘数为33时,对于英文单词有很好的散列效果(小于6个的小写形式可以保证没有冲突)。复杂一点可以使用FNV算法(及其改进形式),它对于比较长的字符串,在速度和效果上都不错。

    2. 长数组的Hash。可以使用http://burtleburtle.net/bob/c/lookup3.c这种算法,它一次运算多个字节,速度还算不错。

    3. 标准库中提供的hash

    3.1 C++

          标准的STL序列容器

    vector、string、deque和list。

    Ø        标准的STL关联容器

    set、multiset、map和multimap。

    Ø        非标准序列容器

    slist和rope。

    Ø        非标准的关联容器

    hash_set、hash_multiset、hash_map和hash_multimap。

    Ø        几种标准的非STL容器

    数组、bitset、valarray、stack、queue和priority_queue。

    关于容器类型的选择可以参考:http://blog.csdn.net/alais/article/details/1180942

    Map(multimap)底层基于红黑树,可以通过下标访问

    Set(multiset)底层基于平衡二叉树,必须通过迭代器进行元素的间接存取

    3.2 JAVA

    在Java 2的Collections框架中,主要包括两个接口及其扩展和实现类:Collection接口和Map接口。两者的区别在于前者存储一组对象,后者则存储一些关键字/值对。

    有些比较复杂的累需要在容器中实现,必须要实现comparable的接口才可以。

    这样collections的sort函数才可以调用。SortedMap的实现类是TreeMap。

    如果在多线程中使用collections可以考虑调用java.concurrent.util的包中的类。

    java.util.concurrent 中的高级实用程序类 -- 线程安全集合、线程池、信号和同步工具

    出处,可能是:http://www.cnblogs.com/qianxun/archive/2011/07/03/2096773.html

  • 相关阅读:
    POJ 1953 World Cup Noise
    POJ 1995 Raising Modulo Numbers (快速幂取余)
    poj 1256 Anagram
    POJ 1218 THE DRUNK JAILER
    POJ 1316 Self Numbers
    POJ 1663 Number Steps
    POJ 1664 放苹果
    如何查看DIV被设置什么CSS样式
    独行DIV自适应宽度布局CSS实例与扩大应用范围
    python 从入门到精通教程一:[1]Hello,world!
  • 原文地址:https://www.cnblogs.com/zhaoyl/p/2708038.html
Copyright © 2011-2022 走看看