《数据结构与算法》-哈希查找算法


  本节介绍一种查找算法——哈希查找算法;该算法除了查找表自身之外,还涉及到哈希表、哈希函数、处理冲突的方法等内容。最后介绍哈希查找的python实现及其性能分析;


1. 基本概念

哈希函数:

  一个把查找表中的关键字映射成该关键字对应的地址的函数;

冲突:

  由同一个哈希函数,把不同的关键字映射到同一地址,这种情况称“冲突”;

同义词:

  发生“冲突”的两个关键字;

哈希表:

  哈希表就是一种以键-值(key-value)存储数据的结构,建立了关键字key和存储地址value之间的一种直接映射关系;

2. 构造哈希函数

2.1 直接定位法

  该方法直接利用某个线性函数对关键字映射,值为映射地址,哈希函数为:
\[ H(key) = a \times key + b \]
优缺点:

  • 计算简单,并且不会产生冲突;
  • 适合关键字分布均匀的情况;
  • 如果关键字分布不均匀,则会浪费大量空间;

2.2 除留余数法

  采用下面的哈希函数,对关键字进行映射:
\[ H(key) = key \% p \]
其中,设查找表表长为\(m\)\(p\)是一个不大于但最接近或者等于m的质数;

优缺点:

  • 简单,常用;
  • p的选择影响效果,因此\(p\)是一个不大于但最接近或者等于m的质数;
# -*-coding:utf-8-*-
# @Time: 2019-04-16
# @ Author: chen, lsqin


class HashFunction:
    """构造哈希函数

    直接定址法
    除留余数法
    """
    # ------------- method 1: 直接定址法 ---------------
    def linear_function(self, key, a=1, b=1):
        """直接定位法
        Argument:
            key:
                需要映射的关键字
            a:
                斜率
            b:
                偏置
        Return:
            value:
                哈希值
        """
        return a * key + b

    # ------------- method 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):
        """除留余数
        Argument:
            key:
                需要映射的关键字
        Return:
            value:
                哈希值
        """
        max_prime = self._max_prime(key)  # 小于查找表长度的最大质数
        return key % max_prime

if __name__ == '__main__':
    hf = HashFunction()
    hf.remainder_function(3)

2.3 数字分析法

  适用于已知的关键字集合;如果更换了关键字,就需要重新构造新的散列函数;

2.4 平方取中法

  取关键字的平方值的中间几位作为哈希值;

2.5 折叠法

  将关键字分割成位数相同的几部分,然后取这几部分的叠加和作为哈希值;

3. 处理冲突的方法

3.1 开放定址法

  开放地址法,指的是存放新表项的空闲地址既向它的同义项开放,又向它的非同义项开放,递推公式为:
\[ H_i = (H(key) +d_i) \%m \qquad i= 0,1,... \]
上述递推公式中可以看出,构造哈希函数的方法是除留余数法;其中,\(H(key)\)表示哈希值;在这种情况下,不同关键词可能映射到同一地址,也就是出现“冲突”情况,因此使用\(d_i\)来解决冲突,第\(i\)次解决冲突后的哈希值为\(H_i\);根据\(d_i\)取值方法的不同可以分成以下三种方法:线性探测法、平方探测法、再哈希法、伪随机序列法

线性探测法:

  当\(d_i = 1, 2, \cdots, m-1\)时,称为线性探测法;

  当发生冲突时,即不同关键词映射到同一地址时,顺序查看存储地址的下一个单元,直到找到空闲单元;其中,\(m\)是表长,\(d_i = 1, 2, \cdots, m-1\)说明最多能探测\(m-1\)次,当探测到表尾地址\(m-1\)时,下一个探测地址为表头地址0;

  线性探测法可能将第\(i\)个关键字的同义词存入第\(i+1\)个地址,而原本属于第\(i+1\)个地址的关键字可能存储\(i+2\),这样下去,就可能造成大量元素聚集在相邻的地址中

平方探测法:

  当\(d_i = 1^2, -1^2, 2^2, -2^2,\cdots, k^2, -k^2(k \leq m/2)\),其中\(m\)是表长,同时必须可以表示成\(4k+3\)的质数,也称为二次探测法;

  平方探测法可以避免出现堆积问题,缺点是不能探测到哈希表上的所有单元,但至少能探测到一半单元;

再哈希法:

  当\(d_i=H_2(key)\),称为再哈希法;即需要两个哈希函数,当使用第一个哈希函数\(H(key)\)发生冲突时,则再利用第二个哈希函数\(H_2(key)\)计算该关键字的地址;

伪随机序列法:

  当\(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)

猜你喜欢

转载自www.cnblogs.com/chenzhen0530/p/10723947.html