哈希表 (桶)

之前博文提到的平衡二叉搜索树 (AVL 树 和 红黑树)可以在 O(logN) 的时间复杂度内进行增删等操作,下面要介绍的哈希表 (hashtable) 在增删等操作表现更为出色,时间复杂度为 O(1)。

概述

不同于之前的树形数据结构,此处的哈希表是一种表 (或者字典) 结构,这种结构的好处是提供常数时间复杂度的基本操作,就像数组那样,想要访问某个元素,通过下标一步就可以找到该元素。

事实上,哈希表就是借助数组下标定位的这种方法来达到常数时间复杂度的操作的。

看个例子:
假如此处有一组数据,其类型皆为 unsigned char (即范围为 0 ~ 255),那么通过一个大小为 256 的数组A 储存 这组元素,数组的 256 个位置上初始值皆为 0, 当插入 i 时, 就执行 A[i]++, 当删除 j 时, 就执行 a[j]--,要查找 k ,则判断 0 == a[k],以上的操作时间复杂度都为 O(1)。
这里写图片描述

很显然,这种方法存在几个缺陷。

  • 如果此处只有 20 个 unsigned char 类型的数据, 那么我们还是需要一个大小为 256 的数组,空间浪费严重
  • 如果数据类型为 32bit 位的 int,那么这个储存数据的数组就需要 4GB,这显然大的不切实际
  • 如果数据是字符串或者其他复杂类型,就无法拿来当数组索引。

我们可以通过某种映射函数来解决上述问题,将大数据转换为我们可以接受的小数据,这个映射函数我们称之为 哈希函数
常见的哈希函数的构造方法有以下几种:数字分析法,平方取中法,分段叠加法,伪随机数法,除留余数法。通常采用除留余数法。
使用哈希函数存在一个问题:不同的值会被映射到相同位置。这种现象叫做哈希碰撞(或者哈希冲突)。我们可以通过下面的方法解决哈希碰撞。

哈希碰撞
  • 负载因子:元素个数除以表格大小。对于线性探测和二次探测的负载因子一般为 0~0.7 ,不让数据过于饱满而造成严重的哈希碰撞。对于开链法负载因子则可以设置为 0 ~ 1。在每次插入元素时,都会检查负载因子,当负载因子超过限定值时,就重新分配空间,并且重新哈希(将数据搬移到新表中)。

下面介绍解决哈希碰撞的方法(线性探测、二次探测、开链法)。

  • 线性探测
    线性探测是指:当计算出的某个位置已经被占用,我们就依序往后寻找,到达尾部时就跳到头部继续,这样总能找到一个安身立命的位置。对与元素删除,只能采用“惰性删除”—— 只用符号标记删除,而不真正抹除。元素的查找就很简单了,在哈希函数计算出来的位置上找,若没有则依序往后寻找,直到碰到一个“空”位置(指的是没有插入过数据的位置,如果数据插入后又删除, 则还得往后找)。

下面是依次插入五个数据时数组的变化。

这里写图片描述

上述方法存在很大的一个缺陷是:当接下来插入8,9,0,1,2中的任意一个数据时,它们都会产生哈希碰撞而不会落在相应的位置,都需要向后查找,这样就与我们之前 O(1) 的时间复杂度背道而驰了。

  • 二次探测

二次探测是为了解决一次探测中连续的哈希碰撞的问题。

如果计算出来的位置已被占用,则就一次找这些位置 H+1^2, H+2^2, H+3^2 … H+i^2,而不是挨个找。
下面是依次插入五个数时数组的变化。
这里写图片描述

可以看到,这种方法处理使得每次探测的都是一个不同的位置。我们如果设置表格大小为质数, 而且保持负载系数在 0.5 之下(当超过 0.5 就重新分配空间并搬移元素),那么可以保证每次插入探测的次数不大于2。

上面的两种方法都是基于:将数据直接储存在数组中,即数组的每个位置只储存一个元素,下面介绍一种方法——开链法。数组的每个位置“挂”着一个桶,桶里可以有多个元素。

开链法与哈希桶

在 STL 中采用开链法处理哈希冲突。

这里写图片描述

数组的每个位置储存的都是一个“桶”(链表),冲突的元素被链接到同一个链表后面,这种方法称为开链法。

下面是 STL 中哈希桶的结构。

//桶中链接的节点
template <class Value>
struct __hashtable_node
{
  __hashtable_node* next;//指向下一个冲突节点的指针
  Value val;//值
};

template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey,
          class Alloc>
class hashtable {

  //...

private:
  //...

  vector<node*,Alloc> buckets;//以拥有动态扩容的vector管理桶子

  //...
};

模板参数

  • Value:节点的实值类型;
  • Key:节点关键在类型;
  • HashFcn:仿函数,哈希函数;
  • ExtractKey: 仿函数,从节点中取出关键字的方法;
  • EqualKey: 仿函数,关键字的比较方法;
  • Alloc: 空间配置器。
哈希函数

对于简单的类型(int、char、 long、size_t等),采用除留余数法就可以求出哈希值,而对于复杂类型,比如 string, 采用除留余数法就难以下手。

问题:

  • 采用除留余数法的话应该除以多少?
  • 复杂类型应该如何处理?

对于第一个问题:通过数学方法研究发现,当模数为质数时,产生哈希碰撞的几率会大大下降,故我们将表的大小设置为质数,每次增容时也将其增加到一个质数大小的值。这些质数由下表给出:

static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,         193,       389,       769,
  1543,       3079,       6151,      12289,     24593,
  49157,      98317,      196613,    393241,    786433,
  1572869,    3145739,    6291469,   12582917,  25165843,
  50331653,   100663319,  201326611, 402653189, 805306457, 
  1610612741, 3221225473, 4294967291
};

inline unsigned long __stl_next_prime(unsigned long n)
{
  const unsigned long* first = __stl_prime_list;
  const unsigned long* last = __stl_prime_list + __stl_num_primes;
  const unsigned long* pos = lower_bound(first, last, n);
  return pos == last ? *(last - 1) : *pos;
}

第一次表的大小设置为 53, 当后面每次增容时就去表中找比当前表大的一个质数。表中的质数从小到大大约呈2倍关系。

对于复杂类型,我们不能采用除留余数法。比如,对于 字符串,我们就需要采用字符串哈希函数。下面列出了一些哈希函数以及它们性能的比较:

unsigned int SDBMHash(char *str)
{
    unsigned int hash = 0;

    while (*str)
    {
        // equivalent to:
        hash = 65599*hash + (*str++);
        hash = (*str++) + (hash << 6) + (hash << 16) - hash;
    }

    return (hash & 0x7FFFFFFF);
}

// RS Hash Function
unsigned int RSHash(char *str)
{
    unsigned int b = 378551;
    unsigned int a = 63689;
    unsigned int hash = 0;

    while (*str)
    {
        hash = hash * a + (*str++);
        a *= b;
    }

    return (hash & 0x7FFFFFFF);
}

// JS Hash Function
unsigned int JSHash(char *str)
{
    unsigned int hash = 1315423911;

    while (*str)
    {
        hash ^= ((hash << 5) + (*str++) + (hash >> 2));
    }

    return (hash & 0x7FFFFFFF);
}

// P. J. Weinberger Hash Function
unsigned int PJWHash(char *str)
{
    unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
    unsigned int ThreeQuarters    = (unsigned int)((BitsInUnignedInt  * 3) / 4);
    unsigned int OneEighth        = (unsigned int)(BitsInUnignedInt / 8);
    unsigned int HighBits         = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
    unsigned int hash             = 0;
    unsigned int test             = 0;

    while (*str)
    {
        hash = (hash << OneEighth) + (*str++);
        if ((test = hash & HighBits) != 0)
        {
            hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
        }
    }

    return (hash & 0x7FFFFFFF);
}

// ELF Hash Function
unsigned int ELFHash(char *str)
{
    unsigned int hash = 0;
    unsigned int x    = 0;

    while (*str)
    {
        hash = (hash << 4) + (*str++);
        if ((x = hash & 0xF0000000L) != 0)
        {
            hash ^= (x >> 24);
            hash &= ~x;
        }
    }

    return (hash & 0x7FFFFFFF);
}

// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
    unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;

    while (*str)
    {
        hash = hash * seed + (*str++);
    }

    return (hash & 0x7FFFFFFF);
}

// DJB Hash Function
unsigned int DJBHash(char *str)
{
    unsigned int hash = 5381;

    while (*str)
    {
        hash += (hash << 5) + (*str++);
    }

    return (hash & 0x7FFFFFFF);
}

// AP Hash Function
unsigned int APHash(char *str)
{
    unsigned int hash = 0;
    int i;

    for (i=0; *str; i++)
    {
        if ((i & 1) == 0)
        {
            hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
        }
        else
        {
            hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
        }
    }

    return (hash & 0x7FFFFFFF);
}
//编程珠玑中的一个哈希函数
//用跟元素个数最接近的质数作为散列表的大小
#define NHASH 29989
#define MULT 31

unsigned in hash(char *p)
{
    unsigned int h = 0;
    for (; *p; p++)
        h = MULT *h + *p;
    return h % NHASH;
}

这里写图片描述

下面是 STL 中的哈希函数(定义于 stl_hash_fun.h 中),可以处理多种类型:


template <class Key> struct hash { };

//处理字符串
inline size_t __stl_hash_string(const char* s)
{
  unsigned long h = 0; 
  for ( ; *s; ++s)
    h = 5*h + *s;

  return size_t(h);
}

//特化出不同版本以处理不同类型
__STL_TEMPLATE_NULL struct hash<char*>
{
  size_t operator()(const char* s) const { return __stl_hash_string(s); }
};

__STL_TEMPLATE_NULL struct hash<const char*>
{
  size_t operator()(const char* s) const { return __stl_hash_string(s); }
};

//处理简单内置类型,直接返回它们本身
__STL_TEMPLATE_NULL struct hash<char> {
  size_t operator()(char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned char> {
  size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<signed char> {
  size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<short> {
  size_t operator()(short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
  size_t operator()(unsigned short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<int> {
  size_t operator()(int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
  size_t operator()(unsigned int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<long> {
  size_t operator()(long x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned long> {
  size_t operator()(unsigned long x) const { return x; }
};
哈希桶的迭代器

采用开链法的哈希桶的迭代器必须维持整个桶结构之间的链接关系,并且记录当前所指节点。其 ++ 跳到下一个节点,如果下一个节点是当前桶(链表)的尾端, 那么久跳到下一个不为空的桶的第一个节点。

下面是 STL 中的迭代器代码:


template <class Value, class Key, class HashFcn,
          class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
  //...
  typedef __hashtable_node<Value> node;
  //...
  node* cur;//当前所指节点
  hashtable* ht;//维持整个表结构,得以到达当前桶尾端时可以跳到下一个桶中
  //...
  //++操作细节
__hashtable_const_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
  const node* old = cur;
  cur = cur->next;//跳到下个节点
  if (!cur) {//若到达尾端,则寻找下一个不为空的桶
    size_type bucket = ht->bkt_num(old->val);
    while (!cur && ++bucket < ht->buckets.size())
      cur = ht->buckets[bucket];
  }
  return *this;
}

哈希桶无 operator-- 操作。 operator++ 操作为了方便遍历整个表,而 operator-- 没有意义。

———谢谢!

参考资料

【作者:果冻 http://blog.csdn.net/jelly_9

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值