数据结构-哈希

哈希

哈希(散列)的基本概念

哈希也叫散列表,是根据关键码值(Key Value)而直接进行访问的数据结构,它通过把关键码值映射到哈希表中的一个位置来访问记录,以加快查找的速度。通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系 ,那么在查找时通过该函数可以很快找到该元素,这个映射函数就做散列函数,存放记录的数组叫做散列表。哈希表的存储是以数据中每个元素的关键字K为自变量,通过散列函数H(k)计算出函数值,以该函数值作为一块连续存储空间的单元地址,将该元素存储到函数值对应的单元中。哈希表查找的时间复杂度为O(1)因为他只与键值有关。

哈希表的存储数据冲突

哈希冲突(碰撞)是指不同关键字对应相同的存储地址,这些导致冲突的关键字成为”同义词“。引起哈希冲突的一个原因可能是: 哈希函数设计不够合理,散列值尽可能均匀分布减少冲突。

哈希表的装填因子:

装填因子 = (哈希表中的记录数)/(哈希表的长度)。装填因子是哈希表装满程度的标记因子。值越大,填入表中的数据元素越多,产生冲突的可能性越大。不同值通过哈希函数产生的键值相同概率就越大。那么为了解决冲突,一方面尽量构造出“完美”的哈希函数使得哈希函数产生的值尽量分布散列,另一方面无法避免的冲突就用相应的算法解决冲突。

哈希函数

构造方法有:直接定址法、数字分析法、折叠法 、平方取中法、减去法、基数转换法 、除留余数法、随机乘数法、字符串数值哈希法、旋转法 、伪随机数法。常用的有:

1)直接定址法

取关键字或者关键字的某个线性函数值作为哈希地址,即H(Key)=Key或者H(Key)=a*Key+b(a,b为整数),这种散列函数也叫做自身函数。例如:有一个从1到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。

2) 数字分析法

分析一组数据,比如一组员工的出生年月,这时我们发现出生年月的前几位数字一般都相同,因此出现冲突的概率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果利用后面的几位数字来构造散列地址,则冲突的几率则会明显降低,分析数据构造合适的函数。

3)平方取中法:

取关键字平方后的中间几位作为散列地址,一个数的平方值的中间几位和数的每一位都有关。因此,有平方取中法得到的哈希地址同关键字的每一位都有关,是的哈希地址具有较好的分散性。该方法适用于关键字中的每一位取值都不够分散或者较分散的位数小于哈希地址所需要的位数的情况。

4) 折叠法:

将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。例如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不到10,000时,可采用此法构造一个四位数的哈希函数。

5)随机数法:

选择一个随机数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

6)除留余数法:

取关键字被某个不大于哈希表长m的数p除后所得余数为哈希地址,H(key)=key MOD p (p<=m)。

哈希函数冲突处理方法有

开放定址法(线性探测法、线性补偿探测法、随机探测法)、拉链法(建立公共溢出区,再散列法)。

线性探测是线性再散列法最简单的处理冲突的方法。插入元素时,如果发生冲突,算法会简单的从该位置向后循环遍历hash表,直到找到表中的下一个空位,并将该元素放入该位置中(会导致相同hash值的元素挨在一起和其他hash值对应的位置被占用)。查找元素时,首先散列值所指向的位置,如果没有找到匹配,则继续遍历hash表,直到:1)找到相应的元素;2)找到一个空位,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)

用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:

1)处理溢出需另编程序。

2)算法建立起来的哈希表,删除工作非常困难。如果将此元素删除,查找的时会发现空位,则会认为要找的元素不存在。只能标上已被删除的标记,否则,将会影响以后的查找。

3)很容易产生堆聚现象。所谓堆聚现象就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。

2)线性补偿探测法:

线性补偿探测法的基本思想是:将线性探测的步长从 1 改为 Q ,即将上述算法中的hash = (hash + 1) % m 改为:hash = (hash + Q) % m = hash % m + Q % m,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。

3)伪随机探测:

随机探测的基本思想是:将线性探测的步长从常数改为随机数,即令: hash = (hash + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。

拉链法:

将所有具有相同哈希地址的而不同关键字的数据元素连接到同一个单链表中。如果选定的哈希表长度为m,则可将哈希表定义为一个有m个头指针组成的指针数组T[0..m-1],凡是哈希地址为i的数据元素,均以节点的形式插入到T[i]为头指针的单链表中。并且新的元素插入到链表的前端,这不仅因为方便,还因为经常发生这样的事实:新近插入的元素最优可能不久又被访问。以下是拉链法优缺点:1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

再散列:

当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。

常见算法的可视化http://www.comp.nus.edu.sg/~stevenha/visualization/index.html

#include <iostream>
#include <iomanip>
#include <functional>
#include <string>
#include <unordered_set>
 
struct S {
    std::string first_name;
    std::string last_name;
};
bool operator==(const S& lhs, const S& rhs) {
    return lhs.first_name == rhs.first_name && lhs.last_name == rhs.last_name;
}
 
// 自定义散列函数能是独立函数对象:
struct MyHash
{
    std::size_t operator()(S const& s) const 
    {
        std::size_t h1 = std::hash<std::string>{}(s.first_name);
        std::size_t h2 = std::hash<std::string>{}(s.last_name);
        return h1 ^ (h2 << 1); // 或使用 boost::hash_combine (见讨论)
    }
};
 
// std::hash 的自定义特化能注入 namespace std
namespace std
{
    template<> struct hash<S>
    {
        typedef S argument_type;
        typedef std::size_t result_type;
        result_type operator()(argument_type const& s) const
        {
            result_type const h1 ( std::hash<std::string>{}(s.first_name) );
            result_type const h2 ( std::hash<std::string>{}(s.last_name) );
            return h1 ^ (h2 << 1); // 或使用 boost::hash_combine (见讨论)
        }
    };
}
 
int main()
{
 
    std::string str = "Meet the new boss...";
    std::size_t str_hash = std::hash<std::string>{}(str);
    std::cout << "hash(" << std::quoted(str) << ") = " << str_hash << '\n';
 
    S obj = { "Hubert", "Farnsworth"};
    // 使用独立的函数对象
    std::cout << "hash(" << std::quoted(obj.first_name) << ',' 
               << std::quoted(obj.last_name) << ") = "
               << MyHash{}(obj) << " (using MyHash)\n                           or "
               << std::hash<S>{}(obj) << " (using std::hash) " << '\n';
 
    // 自定义散列函数令在无序容器中使用自定义类型可行
    // 此示例将使用注入的 std::hash 特化,
    // 若要使用 MyHash 替代,则将其作为第二模板参数传递
    std::unordered_set<S> names = {obj, {"Bender", "Rodriguez"}, {"Leela", "Turanga"} };
    for(auto& s: names)
        std::cout << std::quoted(s.first_name) << ' ' << std::quoted(s.last_name) << '\n';
}

可能的输出:

hash("Meet the new boss...") = 1861821886482076440
hash("Hubert","Farnsworth") = 17622465712001802105 (using MyHash)
                           or 17622465712001802105 (using std::hash) 
"Leela" "Turanga"
"Bender" "Rodriguez"
"Hubert" "Farnsworth"
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值