映射与字典

字典dict是Python中重要的数据结构,在字典中,每一个键都对应一个值,其中键与值的关系就叫做映射,也可以说是每一个键都映射到一个值上。

映射(map)是更具一般性的数据类型,具体到Python中就是字典。

一个简单实现

在使用字典的同时我们一定会有一个疑问,它是怎样通过键去映射到值的呢,它怎么知道这个键的值是谁?

于是我们有了一个这样的想法:

使用列表来存储一项一项的键值对象,寻找的时候就遍历一遍列表,找到当键是你所要找的键时,取出该对象中的值value。

这个想法很简单,我们可以很快的实现一下:

这里先介绍一些相关的抽象基类,Mapping与MutableMapping,它们在collections模块中,供我们实现自定义的map类。Mapping包含dict中的所有不变方法,MutableMapping扩展包含了所有可变方法,但它们两个都不包含那五大核心特殊方法:getitem、setitem、delitem、len、iter。也就是说我们的目标就是实现这五大核心方法使该数据结构能够使用。


from collections import MutableMapping class MyMap(MutableMapping): class item(): def __init__(self,key,value):
self.key = key
self.value = value def __eq__(self, other):
return self.key == other.key def __ne__(self, other):
return self.key != other.key def __init__(self):
self.table = [] def __getitem__(self, item):
for i in self.table:
if i.key == item:
return i.value
raise KeyError('Key Error: '+ repr(item)) def __setitem__(self, key, value):
for i in self.table:
if i.key == key:
i.value = value
return
self.table.append(self.item(key,value)) def __delitem__(self, key):
for n,i in enumerate(self.table):
if i.key == key:
self.pop(n)
return
raise KeyError('Key Error: '+ repr(key)) def __len__(self):
return len(self.table) def __iter__(self):
for i in self.table:
yield i.key

上面这个办法很简单,但是却不是很有效率,我们每次都需要遍历一遍列表才能找到该键的索引,所以时间复杂的为O(n),我们希望这个映射的时间复杂度为O(1)或者是一个常数级别的,于是我们使用叫做哈希表的结构来实现

哈希表

首先先介绍一下哈希表的实现方式

1.对于一个键,我们需要计算出一个值来代表这个键,也就是将键映射到一个整数上,这个整数可以是正数也可以是负数,这一步就是求哈希值

2.这些哈希值有正有负,互相之间没有什么关系,并且位数也可能是好几位,我们想要把这些哈希值再次映射到一个区间[0,N-1]中,使得可以通过列表的整数索引去查找,这一步就是对哈希码的压缩,使用的函数叫做压缩函数

3.在经过压缩函数处理后,就可以得到原先的键对应的列表索引了,但是求哈希值与执行压缩函数的过程中,可能会有冲突发生,也就是得出的值不一定只是属于本键唯一的,可能一个其他的键也会得到同样的值。这时就要在最后把这种冲突处理掉,这一步就叫做冲突处理。

下面具体介绍一下这三个步骤

1.哈希码

求哈希码有很多种方式

将位作为整数处理

举个例子,Python中的哈希码是32位的,如果一个浮点数是64位,我们可以采取取其高32位为哈希码,或者低32位为哈希码,但这样极易出现冲突,所以可以采取高32位与低32位按位相加,或者按位异或

多项式哈希码

对于像是字符串这样的对象,如果按照求和或异或的方式,可能会产生更多的冲突,比如temp10与temp01就会得到相同的哈希码。在字符串中,字符的位置非常重要,所以需要采取一种与位置有关系的哈希码计算方法,如下面这个式子:

x0a^(n-1)+x1a^(n-2)+……+x(n-2)a+x(n-1)

(x0,x1,x2,……,xn-1)是一个32位整数的n元组,是对象x的二进制表示

采用这种计算方式就可以与位置有关联了

循环移位哈希码

利用二进制位循环移位方式,如下面这个字符串循环移位哈希码计算的实现:


def hash_code(s):
mask = (1 << 32) - 1
h = 0
for character in s:
h = (h << 5 & mask) | (h >> 27)
h += ord(character)
return h

<<是左移,>>是右移,&是按位与,|是按位或,ord()函数返回一个字符的ascii码或unicode值

Python中的哈希码

Python中提供了hash()函数,传入对象x,返回一个整型值作为它的哈希码

在Python中只有不可变类型才可以使用hash,如果把我们自定义的对象作为参数传入则会报错。

若想让我们自定义的对象能够使用,可以在类中实现一个叫做hash的特殊方法,在该函数中调用hash函数,并传入该对象的一些不可变属性组合,将值再返回,例如:


def __hash__(self):   return hash((self.red,self.green,self.blue))

2.压缩函数

划分方法

要把哈希码映射到[0,N-1]的区间中,最简单的方式就是进行求余数,例如f(i) = i modN

可是这样显然会有大量的冲突,一种稍微能够减小冲突的办法是将N改为一个素数

这样能够得到一些改善,但是pN+q类型的哈希码还是会被压缩成同一个数

MAD方法

MAD即Multiply-Add-and-Divide,这个方法通过下面这个式子进行映射

[(ai+b) mod p] mod N

N是区间的大小,p是比N大的素数,a和b是从区间[0,p-1]任意选择的整数且a>0

这个函数会尽可能的使映射均匀的分配到[0,N-1]中

3.冲突处理

尽管在求哈希值与压缩函数的过程中我们尽可能避免发生冲突,但还是会有可能造成冲突的,为此还需要进行冲突的处理

使用二级容器

把列表中的每一项都存储为一个二级容器,将映射到该项的键值存入到二级容器中,查找键时先定位到二级容器,再在二级容器中寻找。这里的二级容器的效率要求就不是那么高了,可以使用上文中最开始定义的映射的简单实现来做这个二级容器。在整个哈希表中,我们希望存储的键值项的数量n小于N,也就是n/N<1,n/N叫做这个哈希表的负载因子。

线性探测

这个简单说就是如果映射到这个地方已经有其他键值占上了,那么就向它的后一位放,如果后一位也有了,就继续向后放,知道找到一个空位,然后放进去。

查找的时候,映射到一个位置时要判断一下是不是要找的那个key,如果不是就向后一位找,知道找到是相同键了或者出现空位了,就停止

删除的时候,一样是先找到,然后为了不影响查找,不能简单的将其设置为空,应该用一个标记的对象填住该位置,这时查找的方法也要进行一些改动使其能够跳过这种标记位置。

这种方法的缺点是每一对键值会连续的存储,这种聚集的现象会导致效率的问题。

二次探测

为了改善线性探测聚集现象的发生,原先采用的(j+i)mod N(j为压缩函数得出的值,i为1,2,3….)探测方式改为(j+i^2)mod N

但是当元素超过了哈希表的一半时,这种方式无法保证找到空闲的位置。而且这种方式的删除或其他操作也会更复杂

双哈希策略

这种方式选择了再次进行哈希,如将探测方式改为(j+i*h(k))mod N,h()为一个哈希函数,k为键。

Python字典所采用的方式

字典采用的是(j+f(i))mod N的方式,f(i)是一个基于伪随机数产生器的函数,它提供一个基于原始位的可重复的但是随机的,连续的地址探测序列。

用Python具体实现

首先是一个哈希表的基类,采用MAD的压缩函数


class HashMapBase(MutableMapping):
"""哈希表的基类,需要在子类中实现_inner_getitem,_inner_setitem,
_inner_delitem与__iter__""" class item(): def __init__(self, key, value):
self.key = key
self.value = value def __eq__(self, other):
return self.key == other.key def __ne__(self, other):
return self.key != other.key def __init__(self,cap=11,p=109345121):
self._table = cap*[None]
self._n = 0 # 元素数量
self._prime = p # MAD中的参数
self._scale = 1 + random.randrange(p+1) # MAD中的参数
self._shift = random.randrange(p) # MAD中的参数 def _hash_func(self,key):
return (hash(key)*self._scale+self._shift)%self._prime%len(self._table) def __len__(self):
return self._n def __getitem__(self, k):
j = self._hash_func(k)
return self._inner_getitem(j,k) def __setitem__(self, key, value):
j = self._hash_func(key)
self._inner_setitem(j,key,value)
if self._n>len(self._table)//2: #调整大小,使负载因子小于等于0.5
self._resize(2*len(self._table)-1) def __delitem__(self, key):
j = self._hash_func(key)
self._inner_delitem(j,key)
self._n -= 1 def _resize(self,cap):
old = list(self.items())
self._table = cap*[None]
self._n = 0
for (k,v) in old:
self[k] = v

其中innergetitem,_inner_setitem,_inner_delitem的实现需要结合处理冲突的方式,猜测self.items()是内部调用了__iter方法实现的

使用二级容器


class HashMapOne(HashMapBase):
"""使用二级容器解决冲突的方式实现的哈希表""" def _inner_getitem(self,j,k):
bucket = self._table[j] #把二级容器叫做桶
if bucket is None:
raise KeyError('Key Error: '+ repr(k))
return bucket[k] def _inner_setitem(self,j,k,v):
if self._table[j] is None:
self._table[j] = MyMap()
oldsize = len(self._table[j])
self._table[j][k] = v
if len(self._table[j])>oldsize:
self._n += 1 def _inner_delitem(self,j,k):
bucket = self._table[j]
if bucket is None:
raise KeyError('Key Error: ' + repr(k))
del bucket[k] def __iter__(self):
for bucket in self._table:
if bucket is not None:
for key in bucket:
yield key

使用线性探测


class HashMapTwo():
"""使用线性探测解决冲突实现的哈希表"""
_AVAIL = object() # 标记删除位置 def _is_available(self, j):
"""判断该位置是否可用"""
return self._table[j] is None or self._table[j] is HashMapTwo._AVAIL def _find_slot(self, j, k):
"""寻找键k所在的索引
如果找到了,返回(True,索引)
如果没找到,返回(False,第一个可提供的索引位置)""" firstAvail = None
while True:
if self._is_available(j):
if firstAvail is None: # _AVAIL标记可以是第一个可提供的位置
firstAvail = j
if self._table[j] is None: # 跳过_AVAIL标记
return (False, firstAvail)
elif k == self._table[j].key:
return (True, j)
j = (j + 1) % len(self._table) # 向下一个查找 def _inner_getitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
return self._table[s].value def _inner_setitem(self, j, k, v):
found, s = self._find_slot(j, k)
if not found: # 使用第一个可提供的位置
self._table[s] = self.Item(k, v)
self._n += 1
else:
self._table[s].value = v def _inner_delitem(self, j, k):
found, s = self._find_slot(j, k)
if not found:
raise KeyError('Key Error: ' + repr(k))
self._table[s] = HashMapTwo._AVAIL # 删除标记 def __iter__(self):
for j in range(len(self._table)):
if not self._is_available(j):
yield self._table[j].key

参考《数据结构与算法Python语言实现》

用Python实现数据结构之映射的更多相关文章

  1. 用Python实现数据结构之二叉搜索树

    二叉搜索树 二叉搜索树是一种特殊的二叉树,它的特点是: 对于任意一个节点p,存储在p的左子树的中的所有节点中的值都小于p中的值 对于任意一个节点p,存储在p的右子树的中的所有节点中的值都大于p中的值 ...

  2. (python数据分析)第03章 Python的数据结构、函数和文件

    本章讨论Python的内置功能,这些功能本书会用到很多.虽然扩展库,比如pandas和Numpy,使处理大数据集很方便,但它们是和Python的内置数据处理工具一同使用的. 我们会从Python最基础 ...

  3. 转 Python常见数据结构整理

    http://www.cnblogs.com/jeffwongishandsome/archive/2012/08/05/2623660.html Python常见数据结构整理 Python中常见的数 ...

  4. python 与数据结构

    在上面的文章中,我写了python中的一些特性,主要是简单为主,主要是因为一些其他复杂的东西可以通过简单的知识演变而来,比如装饰器还可以带参数,可以使用装饰类,在类中不同的方法中调用,不想写的太复杂, ...

  5. [0x00 用Python讲解数据结构与算法] 概览

    自从工作后就没什么时间更新博客了,最近抽空学了点Python,觉得Python真的是很强大呀.想来在大学中没有学好数据结构和算法,自己的意志力一直不够坚定,这次想好好看一本书,认真把基本的数据结构和算 ...

  6. Python -- 堆数据结构 heapq - I love this game! - 博客频道 - CSDN.NET

    Python -- 堆数据结构 heapq - I love this game! - 博客频道 - CSDN.NET Python -- 堆数据结构 heapq 分类: Python 2012-09 ...

  7. python实现数据结构单链表

    #python实现数据结构单链表 # -*- coding: utf-8 -*- class Node(object): """节点""" ...

  8. 《用Python解决数据结构与算法问题》在线阅读

    源于经典 数据结构作为计算机从业人员的必备基础,Java, c 之类的语言有很多这方面的书籍,Python 相对较少, 其中比较著名的一本 problem-solving-with-algorithm ...

  9. 【转】Python之mmap内存映射模块(大文本处理)说明

    [转]Python之mmap内存映射模块(大文本处理)说明 背景: 通常在UNIX下面处理文本文件的方法是sed.awk等shell命令,对于处理大文件受CPU,IO等因素影响,对服务器也有一定的压力 ...

随机推荐

  1. FF笔试题

    一.单选题 1.下列关于继承的描述错误的是(  ) A.  在Java中允许定义一个父类的引用,指向子类对象 B.  在Java中继承是通过extends关键字来描述的,而且只允许继承自一个直接父类 ...

  2. 课程四(Convolutional Neural Networks),第一周(Foundations of Convolutional Neural Networks) —— 0.Learning Goals

    Learning Goals Understand the convolution operation Understand the pooling operation Remember the vo ...

  3. UML类图中的六种关系(物理设计阶段)

    UML类图中经常会用到各种箭头和线条来表示不同类或者接口之间的关系,因此非常好的理解各个图标的含义是很有必要的. 在物理设计阶段可以通过EA工具将类图搭建好,然后直接生成物理类,这样也可以减少物理设计 ...

  4. rsync实现目录同步

    rsync rsync是linux系统下的数据镜像备份工具.使用快速增量备份工具Remote Sync可以远程同步,支持本地复制,或者与其他SSH.rsync主机同步. 外文名 rsync 全     ...

  5. Android动态添加Fragment

    Android动态添加Fragment 效果图如下: 项目结构图如下: Fragment1: package com.demo.dongtaifragment; import android.app. ...

  6. mysql 开发进阶篇系列 24 查询缓存下

    一. 查询缓存 1.开启缓存 [root@xuegod64 etc]# vim my.cnf 设置了缓存开启,缓存最大限制128M,重启服务后,再次查询 -- 开启查询缓存后 SHOW VARIABL ...

  7. OkHttp3 简述

  8. Linux基础命令 ls

    目录 1. ls 列出目录的内容 -a --all: -A --almost-all: -b --escape: --block-size=SIZE: --color: --d --directory ...

  9. Mysql半同步复制模式说明及配置示例 - 运维小结

    MySQL主从复制包括异步模式.半同步模式.GTID模式以及多源复制模式,默认是异步模式 (如之前详细介绍的mysql主从复制).所谓异步模式指的是MySQL 主服务器上I/O thread 线程将二 ...

  10. WebSocket原理与实践

    开题思考:如何实现客户端及时获取服务端数据? Polling 指客户端每隔一段时间(周期性)请求服务端获取数据,可能有更新数据返回,也可能什么都没有,它并不在乎服务端数据有无更新.(Web端一般采用a ...