Python数据结构与算法篇(六)-- 哈希表

1 哈希表

        散列表(hash table),又名‘hash表’,它用的是数组支持按照下标随机访问数据(时间复杂度O(1))的特性,所以散列表其实就是基于数组结构的一种扩展。简单的来说,就是把键值通过散列函数求得hash值之后,对数组容量进行取模运算,得到存放在数组位置的下标值,当我们按照键值查询元素时,我们用同样的方法将键值转化数组下标,从对应的数组下标的位置取数据。散列表这种数据结构虽然支持非常高效的数据插入、删除、查找操作,但是散列表中的数据都是通过散列函数打乱之后无规律存储的。也就说,它无法支持按照某种顺序快速地遍历数据。如果希望按照顺序遍历散列表中的数据,那我们需要将散列表中的数据拷贝到数组中,然后排序,再遍历。因为散列表是动态数据结构,不停地有数据的插入、删除,所以每当我们希望按顺序遍历散列表中的数据的时候,都需要先排序,那效率势必会很低。为了解决这个问题,我们常常会将散列表和链表(或者跳表)结合在一起使用。

1.1 什么是哈希表

        散列表(Hash table,也叫哈希表),是根据关键码值(Key和value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

  • 哈希函数(Hash Function):将哈希表中元素的关键键值映射为元素存储位置的函数。
  • 哈希冲突(Hash Collision):不同的关键字通过同一个哈希函数可能得到同一哈希地址。

            哈希表的两个核心问题是:「哈希函数的构建」 和 「哈希冲突的解决方法」。

            常用的哈希函数方法有:直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。

            常用的哈希冲突的解决方法有两种:开放地址法和链地址法。

            有两种不同类型的哈希表:哈希集合和哈希映射。

    • 哈希集合是集合数据结构的实现之一,用于存储非重复值。
    • 哈希映射是映射数据结构的实现之一,用于存储(key, value)键值对。

              在标准模板库的帮助下,哈希表是易于使用的。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能。

              Python 将哈希表用于字典和集合。 哈希表是键值对的无序集合,其中每个键都是唯一的。 哈希表提供了有效的查找,插入和删除操作的组合。 这些是数组和链接列表的最佳属性。相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。

      1.2 哈希表的原理

              哈希表是一种数据结构,它使用哈希函数组织数据,以支持快速插入和搜索。哈希表的核心思想就是使用哈希函数将键映射到存储桶。更确切地说:

      • 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
      • 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。

                下面举一个简单的例子,我们来理解下:

                在示例中,我们使用 y = x % 5 y = x % 5 y=x%5 作为哈希函数。让我们使用这个例子来完成插入和搜索策略:

        • 插入:我们通过哈希函数解析键,将它们映射到相应的桶中。 例如,1987 分配给桶 2,而 24 分配给桶 4。
        • 搜索:我们通过相同的哈希函数解析键,并仅在特定存储桶中搜索。 例如,如果我们搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。

          1.2 设计哈希函数

                  哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶。在之前的示例中,我们使用 y = x y = x % 5 y=x 作为散列函数,其中 x x x 是键值, y y y 是分配的桶的索引。

                  散列函数将取决于键值的范围和桶的数量。下面是一些哈希函数的示例:

                  哈希函数的设计是一个开放的问题。其思想是尽可能将键分配到桶中,理想情况下,完美的哈希函数将是键和桶之间的一对一映射。然而,在大多数情况下,哈希函数并不完美,它需要在桶的数量和桶的容量之间进行权衡。

                  当然,我们也可以自定义一些哈希函数。一般的方法有:

          • 直接定制法。哈希函数为关键字到地址的线性函数。如, H ( k e y ) = a ∗ k e y + b H(key) = a * key + b H(key)=a∗key+b。这里,a 和 b 是设置好的常数。
          • 数字分析法。假设关键字集合中的每个关键字 key 都是由 s 位数字组成(k1,k2,…,Ks),并从中提取分布均匀的若干位组成哈希地址。
          • 平方取中法。如果关键字的每一位都有某些数字重复出现,并且频率很高,我们就可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址。
          • 折叠法。如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址。
          • 除留余数法。预先设置一个数 p,然后对关键字进行取余运算。即地址为 key % p。

            1.3 解决哈希冲突

                    理想情况下,如果我们的哈希函数是完美的一对一映射,我们将不需要处理冲突。不幸的是,在大多数情况下,冲突几乎是不可避免的。例如,在我们之前的哈希函数(y = x % 5)中,1987 和 2 都分配给了桶 2,这就是一个哈希冲突。

                    解决哈希冲突应该要思考以下几个问题:

            • 如何组织在同一个桶中的值?
            • 如果为同一个桶分配了太多的值,该怎么办?
            • 如何在特定的桶中搜索目标值?

                      那么一旦发生冲突,我们该如何解决呢?常用的方法有两种:开放定址法和链地址法。

              1. 开放定址法

                      即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在哈希表中形成一个探测序列,然后沿着这个探测序列依次查找下去。当碰到一个空的单元时,则插入其中。

                      常用的探测方法是线性探测法。 比如有一组关键字 {12,13,25,23},采用的哈希函数为 key % 11。当插入 12,13,25 时可以直接插入,地址分别为 1、2、3。而当插入 23 时,哈希地址为 23 % 11 = 1。

                      然而,地址 1 已经被占用,因此沿着地址 1 依次往下探测,直到探测到地址 4,发现为空,则将 23 插入其中。如下图所示:

              2. 链地址法

                      将哈希地址相同的记录存储在一张线性链表中。例如,有一组关键字 {12,13,25,23,38,84,6,91,34},采用的哈希函数为 key % 11。如下图所示:

              1.4 哈希表的应用

              1. 哈希表的基本操作

                      在很多高级语言中,哈希函数、哈希冲突都已经在底层完成了黑盒化处理,是不需要开发者自己设计的。也就是说,哈希表完成了关键字到地址的映射,可以在常数级时间复杂度内通过关键字查找到数据。

                      至于实现细节,比如用了哪个哈希函数,用了什么冲突处理,甚至某个数据记录的哈希地址是多少,都是不需要开发者关注的。接下来,我们从实际的开发角度,来看一下哈希表对数据的增删查操作。

                      哈希表中的增加和删除数据操作,不涉及增删后对数据的挪移问题(数组需要考虑),因此处理就可以了。

                      哈希表查找的细节过程是:对于给定的 key,通过哈希函数计算哈希地址 H (key)。

                      如果哈希地址对应的值为空,则查找不成功。反之,则查找成功。虽然哈希表查找的细节过程还比较麻烦,但因为一些高级语言的黑盒化处理,开发者并不需要实际去开发底层代码,只要调用相关的函数就可以了。

              2. 哈希表的优缺点

                      优势:它可以提供非常快速的插入-删除-查找操作,无论多少数据,插入和删除值需要接近常量的时间。在查找方面,哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。

                      不足:哈希表中的数据是没有顺序概念的,所以不能以一种固定的方式(比如从小到大)来遍历其中的元素。在数据处理顺序敏感的问题时,选择哈希表并不是个好的处理方法。同时,哈希表中的key 是不允许重复的,在重复性非常高的数据中,哈希表也不是个好的选择。


              2 常见题型

              2.1 题库列表

              • 705. 设计哈希集合

              • 706. 设计哈希映射

              • 136. 只出现一次的数字

              • 137. 只出现一次的数字 II

              • 260. 只出现一次的数字 III

              • 217. 存在重复元素

              • 219. 存在重复元素 II

              • 220. 存在重复元素 III

              • 349. 两个数组的交集

              • 350. 两个数组的交集 II

              • 36. 有效的数独

              • 389. 找不同

              • 496. 下一个更大元素 I

              705. 设计哈希集合

              题目描述

              # 定义一个一维长度为 buckets 的二维数组 table。第一维度用于计算哈希函数,为 key 分桶。
              # 第二个维度用于寻找 key 存放的具体位置。第二维度的数组会根据 key 值动态增长,模拟真正的链表。
              class MyHashSet:
                  def __init__(self):
                      self.buckets = 1009
                      self.table = [[] for _ in range(self.buckets)]
                  def hash(self, key):
                      return key % self.buckets           # 用取余数的方法实现集合
                  
                  def add(self, key):						# 向哈希集合中插入一个值
                      hashkey = self.hash(key)
                      if key in self.table[hashkey]:
                          return
                      self.table[hashkey].append(key)
                      
                  def remove(self, key):					# 将给定值从哈希集合中删除。如果哈希集合中没有这个值,什么也不做
                      hashkey = self.hash(key)
                      if key not in self.table[hashkey]:
                          return
                      self.table[hashkey].remove(key)
                  def contains(self, key):				# 返回哈希集合中是否存在这个值
                      hashkey = self.hash(key)
                      return key in self.table[hashkey]
              

              706. 设计哈希映射

              题目描述

              # 最简单的思路就是用模运算作为哈希方法,为了降低哈希碰撞的概率,通常取素数的模,例如 模 1009或2069
              class MyHashMap:
                  def __init__(self):
                      self.buckets = 1009
                      self.table = [[] for _ in range(self.buckets)]
                  def hash(self, key):                # 哈希映射关系
                      return key % self.buckets
                  
                  def put(self, key: int, value: int) -> None:		# 向哈希映射中插入(键,值)的数值对。如果键对应的值已经存在,更新这个值
                      hashkey = self.hash(key)
                      for item in self.table[hashkey]:
                          if item[0] == key:
                              item[1] = value
                              return
                      self.table[hashkey].append([key, value])
                  def get(self, key: int) -> int:					# 返回给定的键所对应的值,如果映射中不包含这个键,返回-1
                      hashkey = self.hash(key)
                      for item in self.table[hashkey]:
                          if item[0] == key:
                              return item[1]
                      return -1
                  def remove(self, key: int) -> None:				# 如果映射中存在这个键,删除这个数值对
                      hashkey = self.hash(key)
                      for i, item in enumerate(self.table[hashkey]):
                          if item[0] == key:
                              self.table[hashkey].pop(i)
                              return
              

              136. 只出现一次的数字

                      题目描述:给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

               class Solution:
                      def singleNumber(self, nums: List[int]) -> int:
                          #一、用字典统计元素个数
                          # hash_map = Counter(nums)
                          hash_map = {}
                          
                          #方法一
                          for i in nums:
                              hash_map[i] = hash_map.get(i, 0) + 1
                              
                          #方法二
                          # for i in nums:
                          #     if i not in hash_map:
                          #         hash_map[i] = 1
                          #     else:
                          #         hash_map[i] += 1
                          
                          #二、找出只出现一次的元素
                          
                          #方法一
                          return list(hash_map.keys())[list(hash_map.values()).index(1)]
                      
                          #方法二
                          # new_hash_map = {v:k for k,v in hash_map.items()}
                          # return new_hash_map[1]
                      
                          #方法三
                          # return [key for key, value in hash_map.items() if value == 1]
                          
                          # 或
                          # for key,value in hash_map.items():
                          #     if value == 1:
                          #         return key
              

              137. 只出现一次的数字 II

                      题目描述:给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

              class Solution:
                  def singleNumber(self, nums: List[int]) -> int:
                      hash_map = Counter(nums)
                      return min(hash_map.items(), key=lambda x:x[1])[0]
              

              260. 只出现一次的数字 III

                      题目描述:给你一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。

              class Solution:
                  def singleNumber(self, nums: List[int]) -> List[int]:
                      hash_map = collections.Counter(nums)
                      res = []
                      for key, val in hash_map.items():
                          if val == 1:
                              res.append(key)
                      return res
              

              217. 存在重复元素

                      题目描述:给你一个整数数组 nums。如果任一值在数组中出现 至少两次,返回 true;如果数组中每个元素互不相同,返回 false。

              1. 哈希字典

              class Solution:
                  def containsDuplicate(self, nums: List[int]) -> bool:
                      hash_map = {}
                      for num in nums:
                          if num in hash_map:
                              hash_map[num] += 1
                          else:
                              hash_map[num] = 1
                      for index in hash_map:
                          if hash_map[index] >= 2:
                              return True
                      return False
              

              2. 哈希集合

              class Solution:
                  def containsDuplicate(self, nums: List[int]) -> bool:
                      hash_set = set()
                      for num in nums:
                          if num in hash_set:
                              return True
                          else:
                              hash_set.add(num)
                      return False
              

              3. 集合的性质

              class Solution:
                  def containsDuplicate(self, nums: List[int]) -> bool:
                      # return sum(nums) != sum(set(nums)) or nums.count(0) > 1
                      return len(nums) != len(set(nums))
              

              219. 存在重复元素 II

                      题目描述:给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 a b s ( i − j ) < = k abs(i - j) <= k abs(i−j)<=k。如果存在,返回 true;否则,返回 false 。

              1. 哈希字典

              # 找到重复元素和其索引
              class Solution:
                  def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
                      hash_map = {}
                      for i in range(len(nums)):
                          # 已经存在重复的情况
                          if nums[i] in hash_map and abs(i - hash_map[nums[i]]) <= k:
                              return True
                          else:
                              hash_map[nums[i]] = i
                      return False
              

              2. 哈希集合

              # 维护一个长度为 k 的集合
              class Solution:
                  def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
                      hash_set = set()
                      for i in range(len(nums)):
                          # 存在重复元素
                          if nums[i] in hash_set:
                              return True
                          hash_set.add(nums[i])
                          # 及时删除超出数组长度的元素
                          if len(hash_set) > k:
                              hash_set.remove(nums[i - k])
                      return False
              

              220. 存在重复元素 III

                      题目描述:给你一个整数数组 nums 和两个整数 k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得 a b s ( n u m s [ i ] − n u m s [ j ] ) < = t abs(nums[i] - nums[j]) <= t abs(nums[i]−nums[j])<=t,同时又满足 a b s ( i − j ) < = k abs(i - j) <= k abs(i−j)<=k。如果存在则返回 true,不存在返回 false。

              class Solution:
                  def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
                      bucket_dict = dict()
                      for i in range(len(nums)):
                          # 将nums[i]划分到大小为 t + 1 的不同桶中,分桶操作
                          num = nums[i] // (t + 1)
                          # print(num)
                          # 如果桶中已经有元素,有相同的分桶结果,表示存在相同元素
                          if num in bucket_dict:
                              return True
                          # 将 nums[i] 放入桶中
                          bucket_dict[num] = nums[i]
                          # print(bucket_dict)
                          # 判断左侧桶是否满足条件
                          if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
                              return True
                          # 判断右侧桶是否满足条件
                          if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
                              return True
                          # 将 i - k 之前的旧桶清除,因为之前的桶已经不满足条件了
                          if i >= k:
                              bucket_dict.pop(nums[i-k] // (t + 1))  
                      return False
              

              349. 两个数组的交集

                      题目描述:给定两个数组 nums1 和 nums2,返回它们的交集。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。

              1. 集合的交集

              class Solution:
                  def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
                      l1 = set(nums1)
                      l2 = set(nums2)
                      return list(l1 & l2)
              

              2. 哈希字典

              class Solution:
                  def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
                      hash_map = dict()
                      result = []
                      for num1 in nums1:
                          if num1 not in hash_map:
                              hash_map[num1] = 1         # 只做一次计数
                      for num2 in nums2:
                          if num2 in hash_map and hash_map[num2] != 0:
                              hash_map[num2] -= 1        # 及时对结果进行处理  
                              result.append(num2)
                      return result
              

              350. 两个数组的交集 II

                      题目描述:给你两个整数数组 nums1 和 nums2,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

              class Solution:
                  def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
                      hash_map = {}
                      result = []
                      for num1 in nums1:
                          if num1 in hash_map:
                              hash_map[num1] += 1
                          else:
                              hash_map[num1] = 1
                      for num2 in nums2:
                          if num2 in hash_map and hash_map[num2] != 0:
                              hash_map[num2] -= 1
                              result.append(num2)
                      return result
              

              36. 有效的数独

                      请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

              • 数字 1-9 在每一行只能出现一次。
              • 数字 1-9 在每一列只能出现一次。
              • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次
                class Solution:
                    def isValidSudoku(self, board: List[List[str]]) -> bool:
                        # 记录行数据、列数据、3x3格子数据,用于标记 1-9 共10个数字
                        rows = [[0 for _ in range(10)] for _ in range(10)]
                        columns = [[0 for _ in range(10)] for _ in range(10)]
                        boxes = [[0 for _ in range(10)] for _ in range(10)]
                        for i in range(9):
                            for j in range(9):
                                if board[i][j] != '.':
                                    # 1-9数字是否重复出现
                                    num = int(board[i][j])
                                    board_index = (i // 3) * 3 + j // 3  # 方格角标的计算用 box[(i/3)*3+(j/3)][n] 来表示
                                    if rows[i][num] > 0 or columns[j][num] > 0 or boxes[board_index][num] > 0:
                                        return False
                                    rows[i][num] = 1
                                    columns[j][num] = 1
                                    boxes[board_index][num] = 1
                        return True
                

                389. 找不同

                        题目描述:给定两个字符串 s 和 t,它们只包含小写字母。字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。请找出在 t 中被添加的字母。

                1. 哈希字典

                class Solution:
                    def findTheDifference(self, s: str, t: str) -> str:
                        return list(Counter(t) - Counter(s))[0]
                

                2. 异或运算

                class Solution:
                    def findTheDifference(self, s: str, t: str) -> str:
                        return chr(reduce(xor, map(ord, s + t)))
                

                496. 下一个更大元素 I

                题目描述

                class Solution:
                    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
                        hash_map = {}                       # 字典存储结果
                        stack = []                          # 列表模拟栈,单调栈
                        for i in range(len(nums2)):         # 遍历每个字符串
                            if not stack:                   # 如果当前栈为空,则直接入栈
                                stack.append(nums2[i])
                            else:
                                if nums2[i] < stack[-1]:    # 当前值小于栈顶元素,则入栈
                                    stack.append(nums2[i])
                                elif nums2[i] > stack[-1]:  # 当前值大于栈顶元素,不停出栈,把所有栈顶key值的value赋值为当前值
                                    while stack and stack[-1] < nums2[i]:
                                        hash_map[stack[-1]] = nums2[i]
                                        stack.pop()
                                    stack.append(nums2[i])  # 当前值入队列
                        while stack:                        # 如果栈中还有元素,则全部赋值为-1,表示右边没有更大值
                            hash_map[stack[-1]] = -1
                            stack.pop()
                        result = []
                        for i in nums1:                     # 返回每个key值对应的value
                            result.append(hash_map[i])
                        return result
                

                哈希表暂时告一段落,后面在学习中持续补充,谢谢大家的鼓励和支持!


                参考

                • 哈希表:https://www.programmercarl.com/哈希表理论基础.html#哈希表
                • Python数据结构-哈希表(Hash Table):https://www.jianshu.com/p/093346ca7f38
                • 数据结构(Python实现)------ 哈希表:https://blog.csdn.net/Avery123123/article/details/103583115