哈希表详解

哈希表

原理介绍

  • 哈希表(Hash table,也叫 散列表), 是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录, 以加快查找的速度。这个映射函数叫做 散列函数,存放记录的数组叫做 散列表。
  • 哈希表hash table(key,value) 的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对 数组长度 进行 取余取余结果就当作数组的下标 ,将value 存储 在以该数字为下标的数组空间里。
  • 而当使用哈希表进行查询的时候,就是再次使用哈希函数将 key转换为对应的数组下标 ,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
  • 哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用 空间换时间 的做法是值得的。另外,编码比较容易也是它的特点之一。 哈希表又叫做散列表,分为“开散列” 和“闭散列 ”。
  • 用法:我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方。
  • 但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“ 冲突 ”,换句话说,就是把不同的元素分在了相同的“类”之中。后面我们将看到一种解决“冲突”的简便做法。 总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。

哈希函数构造

就是映射函数构造,看某个元素具体属于哪一个类别。
  • 除余法: 选择一个适当的正整数 p ,令 h(k ) = k mod p ,这里, p 如果选取的是 比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。
         公式:index=value%素数(一般为hashTable数组的长度)
         学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
  • 平方散列法
         求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
         index = (value * value) >> 28 ( 右移,除以2^28。记法:左移变大,是乘。右移变小,是除)
  • 数字选择法: 如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。
  • 斐波那契(Fibonacci)散列法:平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是  拿value本身当作乘数呢?答案是肯定的。
        1,对于16位整数而言,这个乘数是40503
        2,对于32位整数而言,这个乘数是2654435769
        3,对于64位整数而言,这个乘数是11400714819323198485
        这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610,987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。
        对我们常见的32位整数而言,公式:
        index = (value * 2654435769) >> 28

冲突处理

  • 线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。又叫作“开发地址法”,此方法并不常用,因为过度浪费了内存空间。
举例
哈希表支持的运算主要有:初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素(member)。 设插入的元素的关键字为 x ,A 为存储的数组。
当数据规模接近 哈希表上界或者下界的时候,哈希表完全不能够体现高效的特点,甚至还不如一般算法。但是 如果规模在中央,它高效的特点可以充分体现。试验表明当元素充满哈希表的 90% 的时候,效率就已经开始明显下降。这就给了我们提示:如果确定使用哈希表,应该 尽量使数组开大,但对太大的数组进行操作也比较费时间,需要找到一个平衡点。通常使它的容量至少是题目最大需求的 120% ,效果比较好(这个仅仅是经验,没有严格证明)。
什么时候适合应用哈希表呢?如果发现解决这个问题时经常要询问:“某个元素是否在已知集合中?”,也就是需要高效的数据存储和查找,则使用哈希表是最好不过的了!那么,在应用哈希表的过程中,值得注意的是什么呢?
哈希函数的设计很重要。一个不好的哈希函数,就是指造成很多冲突的情况,从前面的例子已经可以看出来,解决冲突会浪费掉大量时间,因此我们的目标 就是尽力避免冲突。前面提到,在使用“除余法”的时候,h(k)=k mod p ,p 最好是一个大素数。这就是为了尽力避免冲突。为什么呢?假设 p=1000 ,则哈希函数分类的标准实际上就变成了按照末三位数分类,这样最多1000类,冲突会很多 。一般地说,如果 p 的约数越多,那么冲突的几率就越大
简单的证明:假设 p 是一个有较多约数的数,同时在数据中存在 q 满足 gcd(p,q)=d >1 ,即有 p=a*d , q=b*d, 则有 q mod p= q – p* [q div p] =q – p*[b div a] . ① 其中 [b div a ] 的取值范围是不会超过 [0,b] 的正整数。也就是说,[b div a] 的值只有 b+1 种可能,而 p 是一个预先确定的数。因此 ① 式的值就只有 b+1 种可能了。这样,虽然mod运算之后的余数仍然在 [0,p-1] 内,但是它的取值仅限于 ① 可能取到的那些值。也就是说余数的分布变得不均匀了。容易看出, p 的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率越高。而素数的约数是最少的,因此我们选用大素数。记住“素数是我们的得力助手”。
另一方面,一味的追求低冲突率也不好。理论上,是可以设计出一个几乎完美,几乎没有冲突的函数的。然而,这样做显然不值得,因为这样的函数设计 很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还不如用一个虽然冲突多一些但是编码简单的函数。因此,函数还需要易于编码,即易于实现。综上所述,设计一个好的哈希函数是很关键的。而“好”的标准,就是较低的冲突率和易于实现。另外,使用哈希表并不是记住了前面的基本操作就能以不变应万变的。有的时候,需要按照题目的要求对哈希表的结构作一些改进。往往一些简单的改进就可以带来巨大的方便。
这些只是一般原则,真正遇到试题的时候实际情况千变万化,需要具体问题具体分析才行。
当然,以上讲解的都是闭散列,如果使用链表,做开散列的话就可以更方便存储和删除了
拉链法
上面的方法使用数组实现的,其实很多时候需要使用数组链表来做。 开一个数组,数组每个元素都是一个链表。(hash函数选择,针对字符串,整数,排列,具体相应的hash方法。 碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。)
使用除法散列:
使用斐波那契散列:
使用扩展法:
d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。
hash索引跟B树索引的区别。
Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
(2)Hash 索引无法被用来避免数据的排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
(3)Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
(4)Hash 索引在任何时候都不能避免表扫描。
前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define swap (a, b) { \
    __typeof(a) __c = a; \
    a = b, b = __c; \
}
typedef struct Node {
    //存储字符串
    char *s;
    struct Node *next;
} Node ;
typedef struct HashTable {
    //虚拟头节点数组
    Node *data;
    //cnt - 元素个数, size - 链表长度 
    int cnt, size;
} HashTable ;
void expand( HashTable * h );
void clearHashTable( HashTable * h );
Node *getNewNode( const char * s ) {
    Node *p = ( Node *)malloc( sizeof ( Node ));
    //拷贝s的副本到p->s
    p->s = strdup( s );
    p->next = NULL ;
    return p;
}
HashTable * getNewHashTable( int n ) {
    HashTable *h = ( HashTable *)malloc( sizeof ( HashTable ));
    h->data = ( Node *)malloc( sizeof ( Node ) * n );
    h->size = n ;
    h->cnt = 0;
    return h;
}
//哈希函数
int hash_func( const char * s ) {
    int seed = 131, h = 0;
    for ( int i = 0; s [i]; i++) {
        h = h * seed + s [i];
    }
    //保证哈希值是正数;
    return h & 0x7fffffff;
}
bool find( HashTable * h , const char * s ) {
    int hcode = hash_func( s ), ind = hcode % h ->size;
    Node *p = h ->data[ind].next;
    while (p) {
        if (strcmp(p->s, s ) == 0) return true ;
        p = p->next;
    }
    return false ;
}
bool insert( HashTable * h , const char * s ) {
    if ( h ->cnt >= h ->size * 2) {
        expand( h );
    }
    int hcode = hash_func( s ), ind = hcode % h ->size;
    Node *p = getNewNode( s );
    p->next = h ->data[ind].next;
    h ->data[ind].next = p;
    h ->cnt += 1;
    return true ;
}
//交换哈希表的数据区
void swapHashTable( HashTable * h1 , HashTable * h2 ) {
    swap ( h1 ->data, h2 ->data);
    swap ( h1 ->cnt,  h2 ->cnt);
    swap ( h1 ->size, h2 ->size);
    return ;
}
void expand( HashTable * h ) {
    printf( "expand Hash Table %d -> %d\n" , h ->size, h ->size * 2);
    HashTable *new_h = getNewHashTable( h ->size * 2);
    for ( int i = 0; i < h ->size; i++) {
        Node *p = h ->data[i].next;
        while (p) {
            insert(new_h, p->s);
            p = p->next;
        }
    }
    swapHashTable( h , new_h);
    clearHashTable(new_h);
    return ;
}
void clearNode( Node * p ) {
    if ( p == NULL ) return ;
    if ( p ->s) free( p ->s);
    free( p );
    return ;
}
void clearHashTable( HashTable * h ) {
    if ( h == NULL ) return ;
    for ( int i = 0; i < h ->size; i++) {
        Node *p = h ->data[i].next, *q;
        while (p) {
            q = p->next;
            clearNode(p);
            p = q;
        }
    }
    free( h ->data);
    free( h );
    return ;
}
void output( HashTable * h ) {
    printf( "\n\nHash Table(%d / %d) : \n" , h ->cnt, h ->size);
    for ( int i = 0; i < h ->size; i++) {
        printf( "%d : " , i);
        Node *p = h ->data[i].next;
        while (p) {
            printf( "%s -> " , p->s);
            p = p->next;
        }
        printf( "\n" );
    }
    return ;
}
int main() {
    srand(time(0));
    char s[100];
    #define MAX_N 2
    HashTable *h = getNewHashTable( MAX_N );
    while (~scanf( "%s" , s)) {
        if (strcmp(s, "end" ) == 0) break ;
        insert(h, s);
    }
    output(h);
    while (~scanf( "%s" , s)) {
        printf( "find(%s) = %d\n" , s, find(h, s));
    }
    #undef MAX_N
    return 0;
}

C++语言标准库的哈希函数:

std::hash 是一个通用的哈希函数模板类,可以用于哈希任意类型的数据。它定义在 <functional> 头文件中。使用 std::hash 时,需要提供一个特化的哈希函数来处理具体的数据类型。 C++ 标准库已经为内置类型(如整数、浮点数、指针等)和一些常见的标准库类型(如字符串、容器等)提供了默认的特化版本。
#include <iostream>
#include <functional>
int main() {
    std::hash<int> intHash;  // 创建一个哈希函数对象,用于哈希整数
    int num = 42;
    std::size_t hashValue = intHash(num);  // 计算哈希值
    std::cout << "Hash value of " << num << ": " << hashValue << std::endl;
    std::hash<std::string> stringHash;  // 创建一个哈希函数对象,用于哈希字符串
    std::string str = "Hello";
    hashValue = stringHash(str);  // 计算哈希值
    std::cout << "Hash value of \"" << str << "\": " << hashValue << std::endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值