《數據結構與算法》-哈希查找算法

[TOC]python


  本節介紹一種查找算法——哈希查找算法;涉及的內容有:哈希函數、解決衝突的方法、哈希表、哈希查找的python實現;算法


1. 基本概念

關鍵字序列:函數

​   在哈希查找中,咱們把查找表稱爲關鍵字序列spa

哈希函數:code

  一個把關鍵字序列映射成相應哈希地址的函數;blog

衝突:索引

  由同一個哈希函數,將關鍵字序列中不一樣的關鍵字映射到相同的哈希地址,這種狀況稱「衝突」;ip

同義詞:utf-8

  關鍵字序列中,發生「衝突」的兩個關鍵字;rem

哈希表:

  哈希表就是一種以**鍵-值(key-value)**存儲數據的結構,創建了關鍵字key和存儲地址Addr之間的一種直接映射關係;

  通俗一些說就是,咱們須要把關鍵字序列映射到另外一個序列(哈希表)中,那麼怎麼映射呢?方法就是使用某個哈希函數對關鍵字進行映射獲得哈希地址,那麼該關鍵字在新的序列中的位置就由這個哈希地址來決定;如何選擇哈希函數呢?若是將不一樣的關鍵字使用某個哈希函數映射獲得的哈希地址同樣怎麼辦?這就是下面將要討論的問題。

2. 構造哈希函數

  上面咱們提到了兩個問題,首先看第一個問題:如何選擇哈希函數?下面介紹的幾個哈希函數,包括:直接定位法、除留餘數法、數字分析法、平方取中法以及摺疊法,最長用的當數除留餘數法

2.1 直接定位法

  該方法直接利用某個線性函數對關鍵字映射,值爲映射的哈希地址,哈希函數爲: $$ H(key) = a \times key + b $$ 優缺點:

  • 計算簡單,而且不會產生衝突;
  • 適合關鍵字分佈均勻的狀況;
  • 若是關鍵字分佈不均勻,則會浪費大量空間;

2.2 除留餘數法

  採用下面的哈希函數,對關鍵字進行映射: $$ H(key) = key \ % \ p $$ 其中,設查找表表長爲$m$,$p$是一個不大於但最接近或者等於m的質數;

優缺點:

  • 簡單,經常使用;
  • $p$的選擇影響效果,取$p$爲不大於但最接近或者等於m的質數;

2.3 數字分析法

  適用於已知的關鍵字集合;若是更換了關鍵字,就須要從新構造新的散列函數;

2.4 平方取中法

  取關鍵字的平方值的中間幾位做爲哈希值;

2.5 摺疊法

  將關鍵字分割成位數相同的幾部分,而後取這幾部分的疊加和做爲哈希值;

3. 處理衝突的方法

  使用處理衝突的方法,默認在關鍵詞序列中存在不一樣關鍵字映射到相同哈希地址的狀況;

3.1 開放定址法

  開放地址法,使用下面遞推公式獲得關鍵字序列中的元素在新的序列中的哈希地址爲: $$ H_i = (H(key) +d_i) %m \qquad i= 0,1,... $$ 上述遞推公式中能夠看出,$H(key)$表示將關鍵詞使用某種哈希函數映射後的哈希地址;以後$(H(key) +d_i) %m$表示在以前映射地址的基礎上從新映射,直到在新的序列中找到空閒位置;能夠看出$d_i$的選擇方式不一樣,對應着不一樣的處理」衝突「的方法,所以, 根據$d_i$取值方法的不一樣分紅下列方法:線性探測法、平方探測法、再哈希法、僞隨機序列法

3.1.1 線性探測法

  當$d_i = 1, 2, \cdots, m-1$時,稱爲線性探測法;(什麼意思呢?也就是說先取$d_i=1$,若是再也不衝突,則衝突解決;若是仍存在衝突,繼續迭代$d_i$;下同)

  其中,$m$是新序列的表長,$d_i = 1, 2, \cdots, m-1$說明最多能探測$m-1$次,當探測到表尾地址$m-1$時,下一個探測地址爲表頭地址0;

  在新的序列中,線性探測法可能將存入第$i$個位置的關鍵詞的同義詞存入第$i+1$個地址,而本來屬於第$i+1$個地址的關鍵字可能存儲第$i+2$個位置,這樣下去,就可能形成大量元素彙集在相鄰的地址中

3.1.2 平方探測法

  當$d_i = 1^2, -1^2, 2^2, -2^2,\cdots, k^2, -k^2(k \leq m/2)$,其中$m$是新序列的表長,同時$m$必須能夠表示成​$4k+3$的質數,也稱爲二次探測法;

  平方探測法能夠避免出現堆積問題,缺點是不能探測到哈希表上的全部單元,但至少能探測到一半單元;

3.1.3 再哈希法

  當$d_i=H_2(key)$,稱爲再哈希法;

3.1.3 僞隨機序列法

  當$d_i = 僞隨機數序列$,稱爲僞隨機序列法;

**注意:**上文中提到的」新的序列「指的就是哈希表;當一個新的序列構造完成,哈希表也就獲得了;

**注意:**使用開放地址法解決衝突,不能隨便刪除哈希表中的元素,由於,若刪除元素將會截斷其餘具備相同哈希地址的關鍵字的查找地址;當想要刪除元素時,只能才採用邏輯上的刪除,即給該元素作一個刪除標記;當哈希表中執行屢次刪除後,哈希表看起來仍是滿的,實際上有不少元素已經被邏輯刪除。所以須要按期維護哈希表,將邏輯刪除的元素進行物理刪除;

3.2 拉鍊法

  未避免上述開放地址法帶來的缺點,即不能隨意刪除哈希表中的元素;這裏有一種稱爲拉鍊法的解決衝突的方法,即把全部同義詞存儲在一個線性鏈表中,這個線性鏈表由其哈希地址惟一標識。

  例如:關鍵字序列:${19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79}$,哈希函數$H(key) = key % 13$,採用拉鍊法處理衝突,創建的表以下圖:

4. 哈希查找

  哈希查找的過程與構造哈希表的過程基本一致:對於一個給定的關鍵字key,根據哈希函數能夠計算出哈希地址;

步驟以下: **Step 1:**初始化$Addr=Hash(key)$;

Step 2:檢測查找表中地址爲$Addr$的位置上是否有記錄,若沒有記錄,返回查找失敗;如有記錄,在與key相比較,若相等,返回查找成功,不然執行步驟Step 3

Step 3:用給定的處理衝突方法計算下一個哈希地址,並把$Addr$置爲該地址,轉入步驟Step 2

  下面使用python實現哈希查找,使用除留餘數構造哈希函數、線性探測法解決衝突;

# -*- coding:utf-8 -*-
# @Time: 2019-04-17
# @ Author: chen


class HashSearch:
    def __init__(self, length=0):
        self.length = length  # 須要構造的哈希表長度
        self.table = [None for i in range(length)]  # 初始化哈希表

        self.li = None  # 關鍵字序列
        self.first_hash_value = None  # 關鍵字哈希值

    # ------------- hash function 1: 直接定址法 ---------------
    def _linear_func(self, key, a, b):
        """直接定位法
        Argument:
            key:
                須要映射的關鍵字
            a, b: int
                斜率、偏置
        Return:
            value:
                哈希值
        """
        self.first_hash_value = [a * item + b for item in key]

    # ------------- hash function 2: 除留餘數法 ---------------
    def _prime(self, value):
        """判斷是否爲質數"""
        for i in range(2, value // 2 + 1):
            if value % i == 0:
                return False
        return True

    def _max_prime(self, value):
        """不大於(小於或等於)給定值的最大質數"""
        for i in range(value, 2, -1):
            if self._prime(i):
                return i

    def _remainder_function(self, key, max_prime=None):
        """除留餘數
        Argument:
            key:
                須要映射的關鍵字
        Return:
            value:
                哈希值
        """
        if max_prime is None:
            max_prime = self._max_prime(len(key))  # 小於查找表長度的最大質數
        self.first_hash_value = [item % max_prime for item in key]

    # ------------- 構造哈希表 1: 開放地址法—線性探測法 ---------------
    def generate_hash_table_linear_probing(self, li, max_prime=None, a=1, b=1, hash_func='remainder_func'):
        """利用線性探測法解決衝突
        Argument:
            li: list
                關鍵字序列
            hash_func: str
                選擇使用的哈希函數;提供兩種方式:
                    remainder_func: 表示除留餘數法,默認;
                    linear_func: 表示線性定址法;
            max_prime: int
                當使用"remainder_func"時使用,指定最大質數;
            a, b: int
                當使用"linear_func"時使用,指定斜率、偏置;
        Return:
            table: list
                構造的哈希表
        """
        # ------ Step 1: 選擇哈希函數 ------
        self.li = li
        if hash_func == 'remainder_func':
            self._remainder_function(self.li, max_prime)
        elif hash_func == 'linear_func':
            self._linear_func(self.li, a, b)
        else:
            raise LookupError('select a correct hash function.')

        # ----- Step 2: 迭代構造哈希表 -----
        for first_hash, value in zip(self.first_hash_value, self.li):
            # ----- Step 3: 迭代解決衝突 -----
            for probing_times in range(1, self.length):
                if self.table[first_hash] is None:
                    self.table[first_hash] = value
                    break
                # ----- Step 4: 線性探測法處理衝突 -----
                first_hash = (first_hash + 1) % self.length

        return self.table

    def hash_serach_linear_probing(self, key, hash_table, max_prime=None, a=1, b=1, hash_func='remainder_func'):
        """在哈希表中查找指定元素
        Argument:
            key: int
                待查找的關鍵字
            hash_table: list
                查找表,上一步驟中構造的哈希表
            hash_func: str
                選擇使用的哈希函數;提供兩種方式:
                    remainder_func: 表示除留餘數法,默認;
                    linear_func: 表示線性定址法;
            max_prime: int
                當使用"remainder_func"時使用,指定最大質數;
            a, b: int
                當使用"linear_func"時使用,指定斜率、偏置;
        Return:
            查找成功,返回待查找元素在查找表中的索引位置;不然,返回-1
        """
        # ------ Step 1: 選擇哈希函數 ------
        if hash_func == 'remainder_func':
            first_hash = key & max_prime
        elif hash_func == 'linear_func':
            first_hash = a * key + b
        else:
            raise LookupError('select a correct hash function.')

        # ----- Step 2: 迭代解決衝突 -----
        for probing_times in range(1, self.length):
            if hash_table[first_hash] is None:
                return -1
            elif hash_table[first_hash] == key:
                return first_hash
            else:
                # ----- Step 3: 線性探測法處理衝突 -----
                first_hash = (first_hash + 1) % self.length


if __name__ == '__main__':

    LIST = [19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79]  # 關鍵字序列
    
    # ============
    # 當使用"除留餘數法"構造哈希函數時,max_prime應取不大於關鍵字序列長度的最大質數;
    # 	max_prime也能夠不指定,代碼裏本身計算其最大質數;
    # 當使用"線性定址法"構造哈希函數時,注意哈希表的大小選擇
    # ============
    max_prime = 13
    length = 16  # 構造哈希表的長度

    HS = HashSearch(length)  # 初始化

    # 構造的哈希表
    hash_table = HS.generate_hash_table_linear_probing(li=LIST, max_prime=max_prime, hash_func='remainder_func')
    print(hash_table)
    # 查找指定元素
    result = HS.hash_serach_linear_probing(1, hash_table, max_prime, hash_func='remainder_func')
    print(result)
相關文章
相關標籤/搜索