Leetcode - 两数之和(哈希表)

暴力枚举

当我们使用遍历整个数组的方式寻找 target - x 时,需要注意到每一个位于 x 之前的元素都已经和 x 匹配过,因此不需要再进行匹配。而每一个元素不能被使用两次,所以我们只需要在 x 后面的元素中寻找 target - x。

在循环中,如果找到两个数的和等于目标值,return [i, j] 将立即结束函数并返回包含当前索引 ij 的列表,表示找到了符合条件的数对。

如果在循环结束后仍未找到符合条件的数对,那么最后的 return [] 将返回一个空列表,表示没有找到满足条件的数对。

class Solution(object):
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
    n = lens(nums)
    for i in range(n):
        for i in range(i+1, n):
           if target == nums[i] + nums[j]:
               return [i, j]
    return []

时间复杂度:O(N^2),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。

哈希表

哈希表(Hash Table) 是一种用于存储 键值对 的基本数据结构。

字典例子

用一个简单的例子来说明哈希表的原理:

假设:有一本中文词典,里面包含了所有的汉字,但是这些汉字是按任意顺序随意排版的,那么想要在其中找到某一个汉字,你就需要从头至尾一个一个核查,如果运气差,这个汉字正好在词典的末尾,那你需要遍历整本词典才能找到你要查的汉字。

优化:因为汉字和拼音之间存在着一种确定的关系,为了提高查找速度,现在将所有汉字按照拼音(key)进行排序(拼音可以根据首字母,第二个字母依次进一步排序),并且每个拼音都有一个对应页码(index),从该页开始,存放拼音对应的汉字(value)。所以找到拼音,也就能在对应的页码找到对应的汉字。其中,拼音和页码之间,有着某种固定的映射关系,可以通过某种方式计算出来(hash function)。

由此可见,哈希表可以根据一个 key 值来直接访问数据,因此查找速度快。

但是,上面的例子,还存在一个问题,放在同一页码(具有相同拼音)的汉字可能不止一个(同音字),这时候通过拼音(key)获取到的汉字(value)应该是哪个呢?这就出现了碰撞(hash collision)

为了解决碰撞,实现哈希表可以有以下两种方式:

  • 数组 + 链表
  • 数组 + 二叉树

 

因为哈希表中 key 必须是唯一的,所以图示给拼音加了后缀 _1 和 _2。key han_1 和 han_2 通过哈希函数 F(x) 计算出来的页码都是 244。这时就产生了哈希碰撞。为了解决碰撞问题,新建了一个链表,链表的每个结点都包含了一个键值对,当输入 key han_2 时,哈希表在 244 位置找到了键值对 [han_1 - 汉],但是通过比对发现找到的键值对的 key 是 han_1,不等于 han_2,所以继续遍历到链表的下一个结点,下一个结点存放了键值对 [han_2 - 汗],通过比较发现 key 确实是 han_2,因此返回了汉字(value)。 

所以,哈希表本质上就是一个数组。只不过数组存放的是单一的数据,而哈希表中存放的是键值对。

选举例子

假如有89名候选人参加大选。为了方便记录投票,每个候选人胸前会贴上自己的参赛号码。这89名选手的编号依次是1到89。 通过编号快速找到对应的选手信息。你怎么做?

可以把这89人的编号跟数组下标对应,查询编号x的人时,只需将下标为x的数组元素取出,时间复杂度就是O(1)。看来按编号查对应人信息,效率很高。

这就是散列,编号是自然数,并且与数组的下标一一映射,所以利用数组支持根据下标随机访问时间复杂度是O(1),即可实现快速查找编号对应的人信息。

假设编号不能设置这么简单,要加上州名、职位等更详细信息,所以编号规则稍微修改,用6位数字表示。比如051167,其中,前两位05表示州,中间两位11表示职位,最后两位还是原来的编号1到89。

关键字

关键字 是哈希数组中的元素,可以是任意类型的,它可以是整型、浮点型、字符型、字符串,甚至是结构体或者类。

哈希函数

好的哈希函数应该具备以下两个特质:
  a)单射;
  b)雪崩效应:输入值 x x x 的 1 1 1 比特的变化,能够造成输出值 y y y 至少一半比特的变化;

雪崩效应是为了让哈希值更加符合随机分布的原则,哈希表中的键分布的越随机,利用率越高,效率也越高。
  常用的哈希函数有:直接定址法除留余数法数字分析法平方取中法折叠法随机数法 等等。

直接定址法 就是 关键字 本身就是 哈希值

平方取中法 就是对 关键字 进行平方,再取中间的某几位作为 哈希值

折叠法 是将关键字分割成位数相等的几部分(注意最后一部分位数不够可以短一些),然后再进行求和,得到一个 哈希值
  例如,对于关键字 5201314 5201314 5201314,将它分为四组,并且相加得到: 52 + 01 + 31 + 4 = 88 52+01+31+4 = 88 52+01+31+4=88,这就是哈希值。

除留余数法 就是 关键字 模上 哈希表 长度,表示成函数值就是 f ( x ) = x   m o d   m f(x)

哈希冲突

哈希函数在生成 哈希值 的过程中,如果产生 不同的关键字得到相同的哈希值 的情况,就被称为 哈希冲突

对不同的关键字可能得到同一散列地址,即k1≠k2,而F(k1)=F(k2),这种现象称为冲突

解决方案:

开放地址法

  开放定址法 就是一旦发生冲突,就去寻找下一个空的地址,只要哈希表足够大,总能找到一个空的位置,并且记录下来作为它的 哈希地址。

再散列函数法

        再散列函数法 就是一旦发生冲突,就采用另一个哈希函数,可以是 平方取中法、折叠法、除留余数法 等等的组合,一般用两个哈希函数,产生冲突的概率已经微乎其微了。
  再散列函数法 能够使关键字不产生聚集,当然,也会增加不少哈希函数的计算时间。

链地址法

当然,产生冲突后,我们也可以选择不换位置,还是在原来的位置,只是把 哈希值 相同的用链表串联起来。这种方法被称为 链地址法

公共溢出区法

一旦产生冲突的数据,统一放到另外一个顺序表中,每次查找数据,在哈希数组中到的关键字和给定关键字相等,则认为查找成功;否则,就去公共溢出区顺序查找,这种方法被称为 公共溢出区法

哈希表的完整实现

class HashTable:
    def __init__(self):
        # 初始化哈希表大小为2的17次方,并设置掩码。
        # 掩码是一种用于屏蔽或提取某些位的操作。在哈希表中,我们使用掩码        
        # 来确保哈希地址在合理的范围内,即在数组的有效索引范围内。

        # 这是因为 maxn 是2的某个次方,而减1会得到一个二进制表示的所有位都是1的数。这个操作的效 
          果是,将哈希表的大小限制为2的幂,确保哈希地址不会超出数组的范围。
        self.maxn = 1 << 17
        self.mask = self.maxn - 1
        # 创建一个数组用于存储哈希表数据,初始化为None。
        self.data = [None] * self.maxn

def hash_init(ht):
    # 初始化哈希表,将所有元素设置为None。
    for i in range(len(ht.data)):
        ht.data[i] = None

def hash_get_addr(key, mask):
    # 计算哈希地址,使用按位与运算符和掩码。
    return key & mask

def hash_search_key(ht, key, addr):
    # 在哈希表中搜索键值的函数。
    # 通过hash_get_addr函数计算起始地址。
    start_addr = hash_get_addr(key, ht.mask)
    addr[0] = start_addr

    # 搜索,直到找到键值或遇到空槽。
    while ht.data[addr[0]] != key:
        addr[0] = hash_get_addr(addr[0] + 1, ht.mask)

        # 如果遇到空槽,说明键值不在表中。
        if ht.data[addr[0]] is None:
            return False

        # 如果搜索回到起始地址,说明键值不在表中。
        if addr[0] == start_addr:
            return False

    # 在哈希表中找到键值。
    return True

def hash_insert(ht, key):
    # 向哈希表插入键值的函数。
    addr = hash_get_addr(key, ht.mask)
    ret_addr = [0]

    # 如果键值已经在表中,返回其地址。
    if hash_search_key(ht, key, ret_addr):
        return ret_addr[0]

    # 如果槽被占用,找到下一个可用的槽。
    while ht.data[addr] is not None:
        addr = hash_get_addr(addr + 1, ht.mask)

    # 将键值插入找到的槽。
    ht.data[addr] = key
    return addr

def hash_remove(ht, key):
    # 从哈希表中移除键值的函数。
    addr = [0]

    # 如果键值不在表中,返回None。
    if not hash_search_key(ht, key, addr):
        return None

    # 将对应键值的槽设置为None,表示移除。
    ht.data[addr[0]] = None
    return addr[0]

哈希表-两数之和

使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N^2)降低到 O(1)。

这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。

class Solution(object):
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        hashTable = dict()
        # enumerate(nums) 是 Python 中用于在迭代过程中同时获取元素索引和元素值的一个内置函数。它返回一个迭代器,其中每个元素是一个包含两个值的元组,分别是索引和对应的元素值。
        for i, num in enumerate(nums):
            if target - num in hashTable:
                # 返回的列表包含两个元素,第一个是哈希表中 target - num 对应的索引,第二个是当前遍历到的索引 i
                return [hashTable[target - num], i]
                # 这一行代码在哈希表中添加或更新一个键值对,其中键是当前遍历到的数字 num,值是当前数字在数组 nums 中的索引 i,这样的目的是为了建立一个记录已经遍历过的数字及其索引的哈希表,以便后续的查找。当遇到一个新数字时,将其存储在哈希表中
            hashTable[nums[i]] = i
        return [] 

代码分析 

  1. 初始化:

    • hashTable 是一个字典,用于存储数字和它们在数组中的索引。
    • enumerate(nums) 用于同时获取数组 nums 中的元素值和对应的索引。
  2. 遍历数组:

    • 通过 enumerate(nums) 迭代遍历数组,同时获取当前数字的值 num 和索引 i
  3. 检查差值是否在哈希表中:

    • 在每一次迭代中,检查 target - num 是否在 hashTable 中。
      • 如果存在,说明找到了两个数的和等于目标值,返回它们的索引。
      • 如果不存在,则将当前数字 num 作为键,将其索引 i 作为值存入 hashTable
  4. 返回结果:

    • 如果整个数组遍历完成都没有找到满足条件的两个数,最后返回一个空列表 []
# Example
nums = [2, 7, 11, 15]
target = 9

# 执行代码
solution = Solution()
result = solution.twoSum(nums, target)

# 输出结果
print(result)

 

第一次迭代:

num = 2, i = 0

计算 target - num,即 9 - 2 = 7,并检查 7 是否在 hashTable 中,此时不在,将 (2, 0) 存入 hashTable

第二次迭代:

num = 7, i = 1

计算 target - num,即 9 - 7 = 2,此时 2hashTable 中,返回 [0, 1]

时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值