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=x5 作为哈希函数。让我们使用这个例子来完成插入和搜索策略:

  • 插入:我们通过哈希函数解析键,将它们映射到相应的桶中。 例如,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)=akey+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. 设计哈希集合

题目描述

# 定义一个一维长度为 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(ij)<=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(ij)<=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

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


参考

猜你喜欢

转载自blog.csdn.net/xq151750111/article/details/129740670
今日推荐