『数据结构』散列表(hash table)

哈希表 (hash table) , 可以实现 O(1) 的 read, write, update
相对应 python 中的 dict, c语言中的 map

其实数组也能实现, 只是数组用来索引的关键字是下标, 是整数.
而哈希表就是将各种关键字映射到数组下标的一种"数组"

<a id="markdown-1-关键字" name="1-关键字"></a>

1. 关键字

由于关键字是用来索引数据的, 所以要求它不能变动(如果变动,实际上就是一个新的关键字插入了), 在python 中表现为 imutable. 常为字符串.

<a id="markdown-2-映射" name="2-映射"></a>

2. 映射

<a id="markdown-21-散列函数hash" name="21-散列函数hash"></a>

2.1. 散列函数(hash)

将关键字 k 进行映射, 映射函数 h, 映射后的数组地址 h(k).

<a id="markdown-211-简单一致散列" name="211-简单一致散列"></a>

2.1.1. 简单一致散列

  • 简单一致假设:元素散列到每个链表的可能性是相同的, 且与其他已被散列的元素独立无关.
  • 简单一致散列(simple uniform hashing): 满足简单一致假设的散列

好的散列函数应 满足简单一致假设
例如
\begin{aligned} &(1)\quad h(k) = k \ mod\ m \\ &(2)\quad h(k) = \lfloor {m(kA \ mod\ 1)\rfloor} = kA-\lfloor kA \rfloor \text{,\ x(0< A< 1)}\\ &\quad\text{任何 A 都使用,最佳的选择与散列的数据特征有关.}\\ &\quad\text{ Knuth 认为,最理想的是黄金分割数}\frac{\sqrt{5} -1}{2} \approx 0.618 \end{aligned}

<a id="markdown-212-碰撞collision" name="212-碰撞collision"></a>

2.1.2. 碰撞(collision)

由于关键字值域大于映射后的地址值域, 所以可能出现两个关键字有相同的映射地址

<a id="markdown-213-str2int-的方法" name="213-str2int-的方法"></a>

2.1.3. str2int 的方法

可以先用 ascii 值,然后

  • 各位相加
  • 两位叠加
  • 循环移位
  • ...

<a id="markdown-22-直接寻址法" name="22-直接寻址法"></a>

2.2. 直接寻址法

将关键字直接对应到数组地址, 即 h(k)=k

缺点: 如果关键字值域范围大, 但是数量小, 就会浪费空间, 有可能还不能储存这么大的值域范围.

<a id="markdown-23-链接法" name="23-链接法"></a>

2.3. 链接法

通过链接法来解决碰撞

记有 m 个链表, n 个元素 \alpha = \frac{n}{m} 为每个链表的期望元素个数(长度)

则查找成功,或者不成功的时间复杂度为 \Theta(1+\alpha)
如果 n=O(m), namely \quad \alpha=\frac{O(m)}{m}=O(1), 则上面的链接法满足 O(1)的速度

<a id="markdown-231-全域散列universal-hashing" name="231-全域散列universal-hashing"></a>

2.3.1. 全域散列(universal hashing)

随机地选择散列函数, 使之独立于要存储的关键字
<a id="markdown-2311-定义" name="2311-定义"></a>

2.3.1.1. 定义

设一组散列函数 H=\{h_1,h_2,\ldots,h_i\}, 将 关键字域 U 映射到 \{0,1,\ldots,m-1\} , 全域的函数组, 满足
for \ k \neq l \ \in U, h(k) = h(l), \text{这样的 h 的个数不超过}\frac{|H|}{m}
即从 H 中任选一个散列函数, 当关键字不相等时, 发生碰撞的概率不超过 \frac{1}{m}

<a id="markdown-2312-性质" name="2312-性质"></a>

2.3.1.2. 性质

对于 m 个槽位的表, 只需 \Theta(n)的期望时间来处理 n 个元素的 insert, search, delete,其中 有O(m)个insert 操作
<a id="markdown-2313-实现" name="2313-实现"></a>

2.3.1.3. 实现

选择足够大的 prime p, 记Z_p=\{0,1,\ldots,p-1\}, Z_p^{*}=\{1,\ldots,p-1\},
h_{a,b}(k) = ((ak+b)mod\ p) mod\ m
H_{p,m}=\{h_{a,b}|a\in Z_p^{*},b\in Z_p\}
<a id="markdown-24-开放寻址法" name="24-开放寻址法"></a>

2.4. 开放寻址法

所有表项都在散列表中, 没有链表.
且散列表装载因子\alpha=\frac{n}{m}\leqslant1
这里散列函数再接受一个参数, 作为探测序号
逐一试探 h(k,0),h(k,1),\ldots,h(k,m-1),这要有满足的,就插入, 不再计算后面的 hash值

探测序列一般分有三种

  • 线性\ 0,1,\ldots,m-1
    存在一次聚集问题
  • 二次\ 0,1,\ldots,(m-1)^2
    存在二次聚集问题
  • 双重探查
    h(k,i) = (h_1(k)+i*h_2(k))mod\ m
    为了能查找整个表, 即要为模 m 的完系, 则 h_2(k)要与 m 互质.
    如可以取 h_1(k) = k\ mod \ m,h_2(k) = 1+(k\ mod\ {m-1})

注意删除时, 不能直接删除掉(如果有元素插入在其后插入时探测过此地址,删除后就不能访问到那个元素了), 应该 只是做个标记为删除

<a id="markdown-241-不成功查找的探查数的期望" name="241-不成功查找的探查数的期望"></a>

2.4.1. 不成功查找的探查数的期望

对于开放寻址散列表,且 \alpha<1,一次不成功的查找,是这样的: 已经装填了 n 个, 总共有m 个,则空槽有 m-n 个.
不成功的探查是这样的: 一直探查到已经装填的元素(但是不是要找的元素), 直到遇到没有装填的空槽. 所以这服从几何分布, 即
p(\text{不成功探查})=p(\text{第一次找到空槽})=\frac{m-n}{m}

E(\text{探查数})=\frac{1}{p}\leqslant \frac{1}{1-\alpha}

<a id="markdown-2411-插入探查数的期望" name="2411-插入探查数的期望"></a>

2.4.1.1. 插入探查数的期望

所以, 插入一个关键字, 也最多需要 \frac{1}{1-\alpha}次, 因为插入过程就是前面都是被占用了的槽, 最后遇到一个空槽.与探查不成功是一样的过程
<a id="markdown-2412-成功查找的探查数的期望" name="2412-成功查找的探查数的期望"></a>

2.4.1.2. 成功查找的探查数的期望

成功查找的探查过程与插入是一样的. 所以查找关键字 k 相当于 插入它, 设为第 i+1 个插入的(前面插入了i个,装载因子\alpha=\frac{i}{m}. 那么期望探查数就是
\frac{1}{1-\alpha}=\frac{1}{1-\frac{i}{m}}=\frac{m}{m-i}

则成功查找的期望探查数为
\begin{aligned} \frac{1}{n}\sum_{i=0}^{n-1}\frac{m}{m-i}=\frac{m}{n}\sum_{i=0}^{n-1}\frac{1}{m-i} &= \frac{m}{n}\sum_{i=m-n+1}^{m}\frac{1}{i}\\ &\leqslant \frac{1}{\alpha} \int_{m-n}^m\frac{1}{x}dx\\ &=\frac{1}{\alpha}ln\frac{1}{1-\alpha} \end{aligned}

代码

github地址

class item:
    def __init__(self,key,val,nextItem=None):
        self.key = key
        self.val = val
        self.next = nextItem
    def to(self,it):
        self.next = it
    def __eq__(self,it):
        '''using  keyword <in> '''
        return self.key == it.key
    def __bool__(self):
        return self.key is not None
    def __str__(self):
        li = []
        nd = self
        while nd:
            li.append(f'({nd.key}:{nd.val})')
            nd = nd.next
        return ' -> '.join(li)
    def __repr__(self):
        return f'item({self.key},{self.val})'
class hashTable:
    def __init__(self,size=100):
        self.size = size
        self.slots=[item(None,None) for i in range(self.size)]
    def __setitem__(self,key,val):
        nd = self.slots[self.myhash(key)]
        while nd.next:
            if nd.key ==key:
                if nd.val!=val: nd.val=val
                return
            nd  = nd.next
        nd.next = item(key,val)

    def myhash(self,key):
        if isinstance(key,str):
            key = sum(ord(i) for i in key)
        if not isinstance(key,int):
            key = hash(key)
        return key % self.size
    def __iter__(self):
        '''when using keyword <in>, such as ' if key in dic',
            the dic's  __iter__ method will be called,(if hasn't, calls __getitem__
            then  ~iterate~  dic's keys to compare whether one equls to the key
        '''
        for nd in self.slots:
            nd = nd.next
            while nd :
                yield nd.key
                nd = nd.next
    def __getitem__(self,key):
        nd =self.slots[ self.myhash(key)].next
        while nd:
            if nd.key==key:
                return nd.val
            nd = nd.next
        raise Exception(f'[KeyError]: {self.__class__.__name__} has no key {key}')

    def __delitem__(self,key):
        '''note that None item and item(None,None) differ with each other,
            which means you should take care of them and correctly cop with None item
            especially when deleting items
        '''
        n = self.myhash(key)
        nd = self.slots[n].next
        if nd.key == key:
            if nd.next is None:
                self.slots[n] =  item(None,None) # be careful
            else:self.slots[n] = nd.next
            return
        while nd:
            if nd.next is None: break  # necessary
            if nd.next.key ==key:
                nd.next = nd.next.next
            nd = nd.next
    def __str__(self):
        li = ['\n\n'+'-'*5+'hashTable'+'-'*5]
        for i,nd in enumerate(self.slots):
            li.append(f'{i}: '+str(nd.next))
        return '\n'.join(li)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 散列表是支持 INSERT 、DELETE 和 SEARCH 的字典操作,其是对普通数组概念的推广,因为可以对数组...
    点融黑帮阅读 852评论 0 11
  • "use strict";function _classCallCheck(e,t){if(!(e instanc...
    久些阅读 2,027评论 0 2
  • 课程介绍 先修课:概率统计,程序设计实习,集合论与图论 后续课:算法分析与设计,编译原理,操作系统,数据库概论,人...
    ShellyWhen阅读 2,252评论 0 3
  • 今天 我们拿好篮球准备去上体育课的时候,老师就进门说我我们只想选30个同学跟我去练啦啦操,那剩下的同学...
    祥颐阅读 231评论 0 0
  • 繁华大都市里的树 一棵棵金灿灿的 它们也熟了 一片一片地 连着熟 公孙树还绿着 它们不急着熟 它们不再急着熟 20...
    淑文许阅读 119评论 3 2