3 基本数据结构:哈希表

哈希表

什么是哈希表?

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希函数
index = hashtable(key) 

这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。

哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。)

而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

数据结构特点
  • 数组:寻址容易,插入和删除困难;
  • 链表:寻址困难,插入和删除容易;
  • 哈希表:结合数组和链表的优点,寻址、插入、删除效率非常高,但缺点是扩容困难。
  • ...

哈希表基本思想

1.在元素关键字k和元素存储位置p之间建立对应关系f,使得p=f(k),f称为哈希函数。

2.在创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元。

3.当查找关键字为k的元素时,利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。

如果关键字集合很大,则关键字值中不同的元素可能会映像到和哈希表相同的地址上,即k1≠k2。但是H(k1)=H(k2),上述现象被称为冲突。在这种情况下,通常称k1和k2是同义词。在实际应用中,不能避免上述冲突的情形,只能通过改进哈希函数的性能来减少冲突。

所以构建哈希表的基本思路就两个:

  • 如何构造哈希函数?
  • 如何处理冲突?
如何构造哈希函数?

在构造哈希函数时需要遵循如下原则:

  • 函数本身便于计算。
  • 计算出来的地址分布均匀,即对任一关键字k,f(k)对应不同地址的概率相等,目的是尽可能减少冲突。

构造哈希函数的方法有多种,其中最为常用的有如下5种:

1.数字分析法

如果预先知道关键字集合,则当每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位来构成哈希地址。假设有80个记录,关键字是一个8位的十进制整数:m1 m2 m3 …m7 m8 ,如哈希表长度取值100,则哈希表的地址空间为:00~99。如果经过分析之后,各关键字中m4 和m7 的取值分布比较均匀,则哈希函数为:h(key)=h(m1 m2 m3 …m7 m8 )=m4 m7 。反之,如果经过分析之后,各关键字中m1 和m8 的取值分布很不均匀,例如m1 都等于5,m8 都等于2,则哈希函数为:h(key)=h(m1 m2 m3 …m7 m8 )=m1 m8 ,这种算法的误差很大,所以不可取。

2.平方取中法

如果无法确定关键字中哪几位分布比较均匀,则可以先求出关键字的平方值,然后按照需要取平方值的中间几位作为哈希地址。因为平方后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的哈希地址。

假设把英文字母在字母表中的位置序号作为该英文字母的内部编码,例如K的内部编码为11,E的内部编码为05,Y的内部编码为25,A的内部编码为01,B的内部编码为02,由此可以得出关键字“KEYA”的内部代码为11052501。同理,也可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。对关键字进行平方运算之后,取出第7位到第9位作为该关键字的哈希地址,如下表:

关键字 内部编码 内部编码的平方值 h(key)关键字的哈希地址
KEYA 11050201 122157778355001 778
KYAB 11250102 126564795010404 795
AKEY 01110525 001233265775625 265
BKEY 02110525 004454315775625 315
3.分段叠加法

分段叠加法是指,按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。分段叠加有折叠法与移位法两种。移位法是指将分割后的每部分低位对齐相加;折叠法是指从一端向另一端沿分割边界来回折叠,用奇数段表示正序,用偶数段表示倒序,然后将各段相加。

4.除留余数法

为了更加直观地了解除留余数法,在此举一个例子。假设哈希表长为n,p为小于或等于n的最大素数,则哈希函数为:
h(k)=k % p,
其中%为模p的取余运算。

假设待散列元素为(18,75,60,43,54,90,46),表长n=10,p=7,则有:

  • h(18)=18 % 7=4
  • h(75)=75 % 7=5
  • h(60)=60 % 7=4
  • h(43)=43 % 7=1
  • h(54)=54 % 7=5
  • h(90)=90 % 7=6
  • h(46)=46 % 7=4

此时冲突较多,为减少冲突,可以取较大的n值和p值,例如n=p=13,此时结果如下:

  • h(18)=18 % 13=5
  • h(75)=75 % 13=10
  • h(60)=60 % 13=8
  • h(43)=43 % 13=4
  • h(54)=54 % 13=2
  • h(90)=90 % 13=12
  • h(46)=46 % 13=7
6.伪随机数法

伪随机数法是指采用一个伪随机函数当作哈希函数,即h(key)=random(key)。

哈希函数的衡量标准

在实际应用中,读者应根据具体情况而灵活地采用不同的方法,并使用实际数据来测试它的性能,以便做出正确判定。在判断时通常需要考虑如下5个因素:

  • 计算哈希函数所需的时间(简单)。
  • 关键字的长度。
  • 哈希表大小。
  • 关键字分布情况。
  • 记录查找频率。”
如何处理冲突?

使用性能良好的哈希函数可以减少冲突,但是通常不可能完全避免冲突,所以解决冲突是哈希法的另一个关键问题。无论是在创建哈希表时,还是在查找哈希表时都会遇到冲突,在这两种情况下解决冲突的方法是一致的。

以创建哈希表为例,有以下4种常用的解决冲突的方法:

1.开放地址法

当关键字key的哈希地址m=H(key)出现冲突时,以m为基础产生另一个哈希地址m1 ,如果m2 还是冲突,再以m为基础产生另一个哈希地址m2 ,…,如此继续,一直到找出一个不冲突的哈希地址mi 为止,此时将相应元素存入其中。

2. 再哈希法

再哈希法能够同时构造多个不同的哈希函数,具体格式如下所示:

Hi=RHi(key) i=1, 2, …, k

当哈希地址Hi =RH1 (key)发生冲突时才计算Hi ==RH2 (key)……如此继续,一直到冲突不再产生为止。这种方法不易产生聚集,但增加了计算时间。

3.拉链法(链地址法)

链地址法的基本思想是:将所有哈希地址为i的元素构成一个同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中。链地址法适用于经常进行插入和删除的情况,其中的查找、插入和删除操作主要在同义词链中进行。

假设有如下一组关键字:

32,40,36,53,16,46,71,27,42,24,49,64

哈希表长度为13,哈希函数为:H(key)=key%13,则用链地址法处理冲突的结果如下图所示。


拉链法.png

上组关键字的平均查找长度ASL=(1×7+2×4+3×1)/12=1.5。

4. 建立公共溢出区

建立公共溢出区的基本思想是将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

哈希表的查找过程

哈希表的查找过程与哈希表的创建过程一样。当想查找关键字为K的元素时,首先计算p0=hash(K),然后根据计算结果进行处理:

1.如果单元p0为空,则不存在所查的元素。

2.如果单元p0中元素的关键字为K,则找到所查元素。

否则重复下述操作来解决冲突过程:按解决冲突的方法,找出下一个哈希地址pi,如果单元pi为空,则不存在所查的元素;如果单元pi中元素的关键字为K,则找到所查元素。

分析哈希法的性能

因为冲突的存在,哈希法仍然需要比较关键字,然后用平均查找长度来评价哈希法的查找性能。在哈希法中,影响关键字比较次数的因素有3个,分别是哈希函数、处理冲突的方法以及哈希表的装填因子。定义哈希表的装填因子α的格式如下:

α=哈希表中的元素个数/哈希表的长度

α能够描述哈希表的装满程度。如果α越小,发生冲突的可能性就越小;如果α越大,则发生冲突的可能性就越大。假设哈希函数是均匀的,则只有两个影响平均查找长度的因素,分别是处理冲突的方法和α。

Go 模拟实现一个哈希表

go自带map结构,这里模拟哈希表的实现原理
常见实现方式:
拉链法:数组+链表+哈希算法+素数生成函数



// 一个数组链表模拟哈希表
type HashMap [16]*node

// 哈希表方法
// 往表里存key值
func (hmap *HashMap) SetKey(k string, v interface{}) {
    // 存k先判断存哪里  这里才使用了散列算法计算
    num := hashFunc(k)
    hmap[num].add(k, v)
}

// 取key值
func (hmap *HashMap) GetKey(k string) interface{} {
    num := hashFunc(k)
    return hmap[num].getKey(k)
}

type kv struct {
    key   string
    value interface{}
}
type node struct {
    data kv
    next *node
}

// 新增数据
func (n *node) add(k string, v interface{}) {
    if n.getKey(k) != nil {
        return
    }

    linkNode := n
    for linkNode.next != nil {
        linkNode = linkNode.next
    }

    // 创建新的尾节点
    newNode := new(node)
    newNode.setKv(k,v)
    linkNode.next = newNode
}

// key value 存放方法
func (n *node) setKv(k string, v interface{}) *node {
    n.data.key = k
    n.data.value = v
    return n
}

func (n *node) getKey(k string) interface{} {
    if n.next == nil {
        return nil
    }
    linkNode := n
    for linkNode.next != nil {
        if linkNode.next.data.key == k {
            return n.next.data.value
        } else {
            linkNode = linkNode.next
        }
    }
    return nil
}

// 初始化头节点
func newNodeHead() *node {
    node := new(node)
    node.next = nil
    return node
}

// 创建初始化HashMap
func NewHashMap() *HashMap {
    hashMaps := new(HashMap)
    for i := 0; i < 16; i++ {
        hashMaps[i] = newNodeHead()
    }
    return hashMaps
}

// 简单的哈希函数计算
func hashFunc(key string) int {
    var index int
    index = int(key[0])
    // 询价累积新的数值
    for k := 0; k < len(key); k++ {
        index *= 1103515245 + int(key[k])
    }
    // 右位移2^27次方  这里可以位移任何数  只要别太大导致直接清空
    index >>= 27
    // 按位&  这里想要求32以内就 32-1即可   也就是说  index跟 1111 进行&  得到15以内的数  跟11111取余则得31以内的数
    index &= 16 - 1

    return index
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 一.概念 哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可...
    lfp901020阅读 2,880评论 0 2
  • 哈希表:即散列存储结构。散列法存储的基本思想:建立记录关键码字与其存储位置的对应关系,或者说,由关键码的值决定数据...
    linbj阅读 6,085评论 1 5
  • 一、概述 根据设定的哈希函数H(key)和处理冲突的方法将一组关键字影像到一个有限的连续的地址集(区间)上,并以关...
    12313凯皇阅读 16,858评论 0 3
  • 哈希表定义 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结...
    n油炸小朋友阅读 4,785评论 0 22
  • 虚拟 PK 真实 “对方正在输入中……”,这几个字,你是否很常见呢?微信聊天里,你是否经常看到这几个字呢?当时想法...
    寒梓昀阅读 368评论 3 1