散列函数及哈希表(python实现)

定义

  通过某个函数 f f f,使得
存 储 位 置 = f ( k e y ) 存储位置 = f(key) =f(key)
那样我们可以通过查找关键字不需要比较就可获得需要的记录的存储位置。这就是散列技术

  散列技术在记录的存储位置和它的关键字之间建立一个确定的对应关系 f f f,使得每个关键字 k e y key key 对应一个存储位置 f ( k e y ) f(key) f(key)。查找时,根据这个确定的对应关系找到给定值 k e y key key 的映射 f ( k e y ) f(key) f(key),若查找集合中存在这个记录,则必定在 f ( k e y ) f(key) f(key) 的位置上。

  在这里,对应关系 f f f 称为散列函数,又称为哈希(hash)函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。其中关键字对应的记录存储位置我们称为散列地址。

哈希表的构造

  要构造出哈希表,那么首先要构造散列函数,下面结合代码的实现具体给出集中散列函数的构造方法。

直接定址法

  也就是人为直接为地址赋值,不需要一定的规律及方法,最后的典例会使用这种方法。

  一般情况下,可以取关键字的某个线性函数值为散列地址,即
f ( k e y ) = a ⋅ k e y + b f(key) = a \cdot key + b f(key)=akey+b
其中 a , b a,b a,b 为常数。

数字分析法

  核心思想是抽取,是使用数值的一部分来计算散列值存储位置的方法,常用于数值的位数比较大的情况下。比如一个电话号码159xxxx1234,取最后四位作为地址,构造哈希表,或者取将最后四位数字的和作为地址,这些设置都是人为的,那么此时地址为 1 + 2 + 3 + 4 = 10 1 + 2 + 3 + 4 = 10 1+2+3+4=10

  下面的平方取中法的其中一部分算法就是采用了这样的思想。

平方取中法

  假设关键字是 123 123 123,那么该关键字的平方是 123 ∗ 123 = 15129 123 * 123 = 15129 123123=15129,然后取中间三位数就是 512 512 512,所以散列地址是512;再假设关键字是 4321 4321 4321,那么平方后为 18671041 18671041 18671041,抽取中间的 3 3 3位数可以是 671 671 671,也可以是 710 710 710,这些都是没有什么影响的。

  总的思想是:

1.将关键字平方;

2.然后判断关键字的位数;

3.从个位开始利用取整进行位数的缩减;

4.利用取余从高位进行位数的缩减到我们指定的位数(3)。

  这个算法重要的是关键字的位数问题,只有在知道位数之后才可以进行抽取这个操作,下面是几行判断位数的代码:

def judge_e(r):
    n = 1
    while r >= (10 ** n):    # 判断位数
        n = n + 1
    return n

  然后是剩下的代码:

def  midsquare(l):
    r = l * l
    n = judge_e(r)
    
    if n % 2 == 1:
        mid_n = (n // 2) + 1
        count_n = mid_n - 2
        while count_n != 0:
            r = r // 10
            count_n = count_n - 1
        r = r % 1000
        return r
    elif n % 2 == 0:
        mid_n = n // 2
        count_n = mid_n - 2
        while count_n != 0:
            r = r // 10
            count_n = count_n - 1
        r = r % 1000
        return r

  最后给出验证代码:

x = [142, 124, 1245,938,172]
r = [0] * len(x)
for i in x:
    r[x.index(i)] = midsquare(i)
print(x)
x_square = [0] * len(x)
for i in x:
    x_square[x.index(i)] = i ** 2
print(x_square)
print(r)

  输出的结果为:

[142, 124, 1245, 938, 172]
[20164, 15376, 1550025, 879844, 29584]
[16, 537, 500, 984, 958]

折叠法

  折叠法是将关键字分成从左向右分割成位数相等的几个部分,最后一个部分的位数可以较小,比如关键字为 1234567891 1234567891 1234567891,以每个部分位数为 3 进行位数分割,得到 123 ∣ 456 ∣ 789 ∣ 1 123|456|789|1 1234567891

  这里的主要算法同抽取的思想一致,就不再赘述。

除留余数法

  直接给出公式:
f ( x ) = k e y   m o d   p f(x) = key\ mod \ p f(x)=key mod p
其中 p < = m p <= m p<=m

  值得注意的是 p p p 的取值问题,在我的算法里取的 p p p最接近关键字个数的质数

  提到质数,就不得不说到判断质数的函数,直接给出:

def is_prime(x):    # judge whether the number is prime
  if x <=1:
    return False
  for i in range(2, x):
    if x % i == 0:
      break
  else:
    return True
  return False

  接下来给出剩余的代码:

def after_mod(x:list):
    n = len(x)
    while is_prime(n) != 0:
        n = n - 1             # find the prime that is nearest to the n
    x_num = [0] * len(x)
    count = 0
    for i in x:
        x_num[count] = i % n
        count = count + 1
    return x_num

  验证一下:

r= after_mod([143, 123, 45, 124, 849, 129, 157])
print(r)

  结果为:

[5, 3, 3, 4, 3, 3, 1]

  可见,由于可能某几个关键字较为接近,导致经过取余后,得到的结果是相等的,这就出现了一种情况——散列冲突。解决的方法也很简单,这里我们运用线性探测法。通俗点说,就是在每次计算取余之前“探测”一下是否哈希表中已经存在了这样的数值。

  下面给出完整的改进版除留余数法代码:

# 除留余数法(改进版) --- 是处理散列冲突的一种方法(开放定址法),也称为线性探测法
def is_prime(x):    # judge whether the number is prime
  if x <=1:
    return False
  for i in range(2, x):
    if x % i == 0:
      break
  else:
    return True
  return False
def after_mod(x:list):
    n = len(x)
    while is_prime(n) != 0:
        n = n - 1             # find the prime that is nearest to the n
    x_num = [0] * len(x)
    count = 0
    for i in x:
        x_num[count] = i % n
        ex = x_num[count]
        while(1):
            if ex in x_num:
                ex= ex + 1
            else:
                x_num[count] = ex
                break
        count = count + 1
    return x_num

 用一样的数据进行验证:

r= after_mod([143, 123, 45, 124, 849, 129, 157])
print(r)

  结果为:

[6, 4, 5, 7, 8, 9, 2]

这样就没有重复的散列地址了。

哈希表的应用

  LeetCode上有一道经典的题目:

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

我在刚拿到题目的时候没有考虑时间复杂度的问题,直接采用暴力解法去实现,判断 nums 中的数字 a 和 target - a 是否均在 nums 中,这样需要采用循环,效率不高。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        r = [0, 0]
        for i in nums:
            ex = nums[:]
            r[0] = i
            ex.pop(ex.index(i))
            if (target - i) in ex :
                r[1] = target - i
                break
        n = [nums.index(r[0]), ex.index(r[1])+1]
        return n

  后来想到可以利用空间复杂度的牺牲去换取时间复杂度的减小,最好的方法就是哈希表的利用了,然后采用新的方法:

def hashmap(nums, target):
    hashmap={
    
    }
    for i,num in enumerate(nums):
        if hashmap.get(target - num) is not None:
            return [i, hashmap.get(target - num)]
        hashmap[num] = i

验证一下:

nums = [9, 2, 13, 34]
target = 22
re = hashmap(nums, target)
print(re)

结果是:

[2, 0]

大大提高了时间效率。

总结

  散列函数以及哈希表都是在提高空间复杂度的情况下去换取时间复杂度,在算法中有着广泛的运用。其中散列函数可以人为的利用各种不同的方法来使得哈希表最为合理。

− − − − − − − − − − − − − − − − ----------------
该文章首发于 zyairelu.cn
欢迎来到我的网站进行评论及研讨
个人邮箱[email protected]
− − − − − − − − − − − − − − − − ----------------

猜你喜欢

转载自blog.csdn.net/weixin_42731543/article/details/102605813