哈希算法及python字典的实现

Tim Xu bio photo By Tim Xu xiaoxubeii@gmail.com Comment

哈希表定义

根据维基百科定义,散列表(Hash table,也叫哈希表),是根据关键字(Key value)而直接访问在内存存储位置的数据结构。也就是说,它通过把键值通过一个函数的计算,映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。 一个简单的例子就是,查字典时的过程,字典的索引(可能是笔画或者拼音)就是key,后面对应的页码就是根据哈希函数得出的你所查字的真实地址。

散列函数(哈希函数)

如果要实现哈希表,首先第一步是要确定关键字和存储位置的对应关系,即确定散列函数。一般来说,散列函数会满足下面几个条件:

  1. 对输入的值,总是可以得到一个固定长度的输出值
  2. 不同的输入值可能得到相同的输出值

构造散列函数有多种方式,比如直接寻址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法。

碰撞处理

由于对于不同的关键词,散列函数可能输出相同的值。即k1!=k2,但是f(k1)=f(k2),这种现象称为碰撞(Collision)。所以,第二步还需要处理碰撞,常用方法有以下几种:

  1. 开放寻址法(open addressing)
  2. 单独链表法
  3. 双散列
  4. 再散列

python中的字典

在python中,字典就是一个哈希表实现。

typedef struct {
    Py_ssize_t me_hash;
    PyObject *me_key;
    PyObject *me_value;
} PyDictEntry;

struct _dictobject {
    PyObject_HEAD
    Py_ssize_t ma_fill;
    Py_ssize_t ma_used;
    Py_ssize_t ma_mask;
    PyDictEntry *ma_table;
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
    PyDictEntry ma_smalltable[PyDict_MINSIZE];
}

python的字典解决碰撞的方式是使用开放寻址法,在这就可以体现python的一个设计思路:利用空间换时间。

一道关于HashMap存储方式的面试题

偶然间在网上看到一道面试题,感觉很有意思:如果以自定义类型为key,放入HashMap后,我们在外部将key的属性进行更改,然后再用这个key从HashMap中可以取出什么元素? 我觉得准确的答案应该是:这要看如何生成哈希值了。 在python的字典中,一般不会出现这个问题。因为对象的哈希值是固定,就是对象的id:

class T():
    pass
    
print id(T())
139696630028048

当然我们还可以自定义哈希值:

class T():
    def __init__(self, name):
        self.name = name
        
    def __hash__(self):
        return hash(self.name)

这样,在用T的对象作为key时,其实就是使用的self.name的哈希值。在取值时,情况就稍微复杂些,字典不光会判断哈希值,还会判断对象是否相等。如果我们想通过修改属性来获取其他key的元素,我们可以重写__eq__()

class T():
    def __init__(self, name):
        self.name = name
        
    def __hash__(self):
        return hash(self.name)
        
    def __eq__(self,r):
        return self.name == r.name

dict = {}
t1 = T(name='t1')
dict[t1] = 1
t2 = T(name='t2')
dict[t2] = 2
t2.name = 't1'

print dict[t2]
1