1. 哈希函数

    上一节有一个哈希函数用来做2次探查用。

    问题:如何选用哈希函数?

        当然是散列得到的冲突越小越好,也就是每个key都能尽量被等可能的散列到m个槽中的任何一个,并且与其他key被散列到哪个槽位无关。(均匀散列)


2. 装载因子(load factor)

    值 = 已经被使用的槽数 / 槽的总槽数

    当我们一直往哈希表里插入数据的时候,很快空间会不够用,这时会根据装载因子进行扩容。

    比如,之前插入是8个元素,总槽数是13,这时他的装载因子是0.62;如果我们继续插入的话,很快槽数将不够用,当装载因子超过0.8的时候,要重新去开辟空间,并进行"重哈希"操作。

    

3. 重哈希(rehashing)

    过程:首先开辟一块新的空间,比如Cpython他的实现有自己的策略,增长相比之前的槽的总长度,用什么策略去增长?

    先来看一下Cpython解释器的代码:

        在dictobject.c的文件里,他有一个GROWTH_RATE这个关键字,在python不同的版本里,值是不一样的,不同的版本的cpython使用了不同的策略。

        image.png

        (1)在3.3.0里面,他这个值就是拿"已经使用的槽数"x2;

        (2)在3.2里面,他的这个增长是拿"已经使用的槽数"x4;

        (3)当前使用的(3.5)是拿("已经使用的槽数"x2)+("容量的值"/2)

        以上三点都是重哈希的扩容策略。

        重哈希实际上就是先扩容,当扩容之后,再去把之前的值全部哈希到新的哈希表里面去,进行一个重新插入的操作。

         就是在不断的插入数据的过程中,一旦装载因子超过指定的值,比如之前的0.8时,就会进行重哈希操作。

         下面通过代码模拟实现(实际是通过模拟cpython来实现哈希表)

        [

            注意:哈希表数组的槽,一个槽有3种状态:

                分别是:

                    ① 从未使用过或冲突过;

                    ② 使用过,但是被remove过;

                    ③ 这个槽正在被占用;

        ]

        这里将之前Array()的类拷贝过来使用,做一些小改动:

"""定义数组类"""
class Array(objece):
  def __init__(self, size=32, init=None):    #改动:新增了init参数
    self._size = size
    self._items = [init] * size
      
  def __getitem__(self, index):
    return self._items[index]
    
  def __setitem__(self, index):
    self._items[index] = value
    
  def __len__(self):
    return self._size
    
  def clear(self, value=None):
    for i in range(self._items):
      self._items[i] = value

  def __iter__(self):
    for item in self.items:
      yield item

"""定义一个哈希表数组的槽"""
class Slot(object):
  """注意:一个槽有三个状态,相比链接法解决冲突,二次探查法删除一个 key 的操作稍微复杂点"""
  def __init__(self, key, value):
    self.key, self.value = key, value
     
"""定义哈希表"""        
class HashTable(object):
  UNUSED = None               #表示Slot没被使用过
  EMPTY = Slot(None, None)    #表示虽然Slot被使用过,但是被删除了
    
  def __init__(self):
    self._table = Array(8, init=HashTable.UNUSED)   #8表示长度数组(2的n次方),
                                                    #HashTable.UNUSED表示没有被使用过,初始化数组的每一个元素。
    self.length = 0                                 #表示已经使用的槽的个数。

  """定义装载因子"""
  @property    
  def _load_factor(self):
    return self.length / float(len(self._table))    #拿已使用的槽数/哈希表的总长度
  
  def __len__(self):
    return self.length                #直接返回哈希表的长度

  """定义哈希函数"""        
  def _hash(self, key):
    """
      根据key得到一个数组的下标,这里简化一下,使用内置的哈希函数,
      得到一个整数值,取他的绝对值,然后直接对数组的长度取模就可以。
    """               
    return abs(hash(key) % len(self._table))
  
  """定义哈希表常用操作"""
  def _find_key(self, key):         #作用:寻找一个槽,找到key的位置
    index =self._hash(key)          #调用hash函数得到第一个槽的位置
    _len = len(self._table)         #定义hash函数的长度
    while self._table[index] is not HashTable.UNUSED:   #当这个槽不是未使用的槽时,才会继续向下找
      if self._table[index] is HashTable.EMPTY:         #如果这个槽被使用过,且值被删了,当前为空
        index = (index*5 + 1) % _len                    #模拟的冲突解决策略,直接使用哈希的方式,
                                                        #这是Cpython里使用的一种解决哈希冲突的方式。
        continue
      elif self._table[index].key == key:     #如果这个槽被占用,且他的key和要查找的key一致
        return index                          #直接返回当前下标
      else:
        index = ((index*5)+1) % _len          #否则,直接寻找下一个槽的位置。
    return None                               #如果什么都没找到,返回None
   
  """实现一个辅助方法"""       
  def _slot_can_insert(self, index):    #判断槽是否能被插入新值
    return (self._table[index] is HashTable.EMPTY or self._table[index] is HashTable.UNUSED)    #判断槽是空的或者未被使用的
            
  """定义寻找一个槽来插入新值"""
  def _find_slot_for_insert(self, key):
    index = self._hash(key)
    _len = len(self._table)
    while not self._slot_can_insert(index):
      index = (index*5 + 1) % _len
    return index
          
  """实现一个 in 操作符"""
  def __contains__(self, key):
    index =self._find_key(key)      #先去查找key
    return index is not None        #表示找到了
          
  """实现哈希表常用方法"""
  def add(self, key, value):
    if key in self:                       #如果key在槽里面
      index = self._find_key(key)         #先找到这个key的位置
      self._table[index].value = value    #更新槽的值
      return False                        #表示没有执行插入操作,而是更新操作
    else:
      index = self._find_slot_for_insert(key)    #找到槽的位置插入它
      self._table[index] = Slot(key, value)      #然后把这个值赋给新的槽,这个槽的值就是(key,value)
      self.length += 1                            #将槽使用的长度+1
      if self._load_factor >= 0.8:                #如果他的装载因子超过0.8的时候,执行重哈希操作。
              self._rehash()
    return True

  def _rehash(self):
    old_table = self._table                         #将原来的哈希数组保存到old_table里面
    """ 
        接下来要开辟一个新长度的新数组,
        就以简单的扩容策略来写,
        把原来的长度乘以2
    """
    newsize = len(self._table) * 2                  #扩容策略也比较简单*2就行了
    self._table = Array(newsize, HashTable.UNUSED)  #给self._table赋新值
    self.length = 0                                 #将长度归0
    for slot in old_table:                          #遍历旧的,将旧的全部插到新的里面去
      if slot is not HashTable.UNUSED and slot is not HashTable.EMPTY:  #满足这两个条件后
        index = self._find_slot_for_insert(slot, key) #先去调用_find_slot_for_insert给它找个位置去插入
        self._table[index] = slot                 #给这个槽赋值
        self.length += 1                          #将长度递增

  def get(self, key, default=None): #定义get操作
    index =  self._find_key(key)    #先查找值是不是在里面
    if index is None:               #如果不在里面
      return default
    else:
      return self._table[index].value #否则返回这个位置的value

  def remove(self, key):                  #定义删除操作
    index = self._find_key(key)
    if index is None:                     #判断一下是否找到key,如果没找到
      raise KeyError()                    #返回keyerror
    value = self._table[index].value)     #否则将这个要删除的值取出来
    self.length -= 1                      #长度减1
    self._table[index] = HashTable.EMPTY  #将当前位置赋值给空槽
    return value                          #返回这个被删除的值

  def __iter__(self):           #迭代
    """
      知道python的字典是遍历他的key,
      所以这里也是用同样的方式实现
    """
    for slot in self._table:
      """若果这个槽不是空和未被使用的槽里面的话"""
      if slot is not in (HashTable.EMPTY and HashTable.UNUSED):
        yield slot.key


###单元测试####
def test_hash_table():
  h = HashTable()
  h.add('a', 0)
  h.add('b', 1)
  h.add('c', 2)
  assert len(h) == 3
  assert h.get('a') == 0
  assert h.get('b') == 1
  assert h.get('c') == 2
  assert h.get('asdf') is None

  h.remove('a')
  assert h.get('a') is None
  assert sorted(list(h)) == ['b', 'c']  #按key来进行排序

  n = 50
  for i in range(n):
    h.add(i)

  for i in range(n):
    assert h.get(i) == i


定义完哈希表后,后面去实现字典和set集合就会比较轻松了。