12. 哈希表(2)

1. 哈希函數python

    上一節有一個哈希函數用來作2次探查用。數組

    問題:如何選用哈希函數?ide

        固然是散列獲得的衝突越小越好,也就是每一個key都能儘可能被等可能的散列到m個槽中的任何一個,而且與其餘key被散列到哪一個槽位無關。(均勻散列)
函數


2. 裝載因子(load factor)單元測試

    值 = 已經被使用的槽數 / 槽的總槽數
測試

    當咱們一直往哈希表裏插入數據的時候,很快空間會不夠用,這時會根據裝載因子進行擴容。
spa

    好比,以前插入是8個元素,總槽數是13,這時他的裝載因子是0.62;若是咱們繼續插入的話,很快槽數將不夠用,當裝載因子超過0.8的時候,要從新去開闢空間,並進行"重哈希"操做。orm

    

3. 重哈希(rehashing)blog

    過程:首先開闢一塊新的空間,好比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集合就會比較輕鬆了。

相關文章
相關標籤/搜索