【不习算法】只会快速查询?你真的懂哈希表吗

    科技2022-07-21  115

    【不习算法】只会快速查询?你真的懂哈希表吗

    引言

    考虑这样一个场景,在某次破案行动中,警方已经获取目标嫌疑人的身份证号,现在有一个身份证信息数据库,存储着身份证号与个人信息的数据。此时警方需要通过身份证号在名单中获取目标嫌疑人的姓名、年龄、地址等信息,考虑这是一个千万级的数据库,如何查询才是有效的呢?

    我们考虑该国家的身份证是8位,目标身份证号为50585346,这时候出现了几种不同的查询方法:

    A警察采用了顺序遍历的方法,从这个字典中一页一页的往后翻00000000、00000001、…这显然是一种非常麻烦的方法;B警察稍微聪明一点,拿起字典从中间随便翻起一页,看到号码为26000000,目标身份证号比该号码大,于是又在后部分中翻起一页…这种“二分查找”的方法大大提高了查询效率。

    稍稍分析就可以看出A警察的查询效率为O(n), B警察的查询效率为O(log n),有没有一种方法能够实现最理想的查询,达到常数级的查询效率呢?这时哈希表便出现了,哈希表正是这样一种存储结构,能够实现最理想的查询效率,没有一种数据结构能在查询方面比哈希表更优秀。

    然而很多人对于哈希表作用的理解,到“快速查询”这一步就停止了,其实哈希表的作用远大于此,本文以LeetCode中几道题目为案例,对哈希表的多种功能进行剖析。

    知识储备:python实现哈希表的方法

    Python字典的基本操作

    Python中实现哈希表最常用的方法就是字典,下面对Python字典的基本操作进行总结:

    方法作用Dict = {}创建字典Dict[“a”] = 1键a的值为1Dict.get(“a”)获取键a的值,没有则返回空Dict.has_key(key)字典是否有键a,没有则返回False,有返回TrueDict.clear()字典清空Dict.keys()以列表返回一个字典所有的键Dict.values()以列表返回一个字典所有的值Dict.items()以列表返回一个字典所有的(键, 值) 元组数组

    通过Python库实现哈希表

    虽然在大多数情况下,通过Python字典实现哈希表已经足够了。然而针对一些特殊情况(比如后面的例题6),哈希表的值为一些特殊的变量(如列表时),我们需要提前对哈希表值的变量类型进行申明,这时候我们可以利用python的Collection库,具体实现方法如下:

    from collections import defaultdict dictn = defaultdict(list) #该哈希表的值为列表类型

    功能1:哈希表实现快速查询

    例1:LeetCode001 两数之和

    LeetCode001 两数之和 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个 整数,并返回他们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]

    很多人入门LeetCode都是从这道题目入手的,也是通过这道题了解到哈希标的强大。采用普通暴力破解方法解题时,需要对列表中的元素两两组合进行遍历,时间代价为O(n2),这显然不是最优的解决办法。

    于是我们引入哈希表快速查询的机制,对于列表中每一个元素num[i],都存在一个“配对值”com[i],使得num[i]+com[i]=target。于是我们可以用一个哈希字典来存储这样的映射关系,以题目示例中的nums数组为例,这样的映射关系为:

    nums = [2, 7, 11, 15] #原始序列 hashmap = {2:7,7:2,11:-2,15:-6} #转换的哈希字典

    这样一来,我们仅仅通过对原始序列进行一次遍历,每访问一个元素检查该元素是否为哈希字典中的key,如果是则返回正确的答案,如果否则将这一组新的映射关系添加到字典中,整个过程的时间代价为O(n)。具体代码实现如下:

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

    这道题目就是一道典型的通过哈希表实现快速查询的案例,这是LeetCode的第一道题目,难度不高。下面我们通过LeetCode的一道Medium题目对哈希表实现快速查询进行巩固。

    例2:LeetCode560 和为K的子数组(依图面试)

    LeetCode560 和为K的子数组(依图面试) 给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。 示例 1 : 输入:nums = [1,1,1], k = 2 输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

    这道题目是依图面试的原题,如果采用滑动窗口、动态规划,时间代价都不能满足需求,巧妙用哈希表 Θ(n)时间代价便可解决。本质上该题目是两数之和的变形题,建议读者在学习两数之后,自行继续代码书写进行巩固。

    以下为参考的案例代码:

    class Solution(object): def subarraySum(self, nums, k): pdict = {} result = 0 pre = [0] pdict[k] = 1 for i in range(len(nums)): # iterate all pre_num in pnums pre.append(pre[-1] + nums[i]) p = pre[-1] if pdict.has_key(p): # if p in dict, add result result = result + pdict[p] target = k + p # tagget number to find if not pdict.has_key(target): # if targer not in pdict pdict[target] = 1 # set value of target to 1 else: pdict[target] = pdict[target] + 1 # add value by one return result

    功能2:哈希表实现计数

    哈希表本质上是一种”键与值映射“的数据结构,这种映射关系使得哈希表在查找功能上表现优异。同时,正是存在这种映射关系,我们可以利用哈希表实现计数功能,此时哈希表的键为需要计数的元素,哈希表的值为该元素出现的次数。

    例3:LeetCode242 有效的字母异位词

    LeetCode242 有效的字母异位词 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 示例 1: 输入: s = "anagram", t = "nagaram" 输出: true 示例 2: 输入: s = "rat", t = "car" 输出: false

    这道题目难度不高,只要对”字母异位词“的含义理解了,很快就能解决。对于两个字符串A和B,如果字符串A中不同元素出现的次数和字符串B中不同元素出现的次数一样,那么字符串A和字符串B就是一组字母异位词。针对”统计元素出现的次数“这样的计数问题,哈希表就是一种有效的解决办法。

    下面是参考代码的实现:

    class Solution(object): def isAnagram(self, s, t): dict_s = {} if len(s) != len(t): return False for cs in s: if not dict_s.has_key(cs): dict_s[cs] = 1 else: dict_s[cs] = dict_s[cs] + 1 for ts in t: if not dict_s.get(ts) or dict_s.get(ts) == 0: return False dict_s[ts] = dict_s[ts] - 1 return True

    例4:LeetCode242 两个数组的交集 II

    LeetCode242 两个数组的交集 II 给定两个数组,编写一个函数来计算它们的交集。 示例 1: 输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2,2] 示例 2: 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[4,9]

    哈希表可以对字符串中元素出现的次数计数,同样地,也可以对数组中元素出现的次数计数。例4是一道利用数组元素计数解题的例题,建议读者参考例3的思路进行解决。

    以下为参考代码:

    class Solution(object): def intersect(self, nums1, nums2): dictn = {} resultlist = [] for num in nums1: if not dictn.get(num): dictn[num] = 1 else: dictn[num] = dictn[num] + 1 for num in nums2: if dictn.get(num) and dictn.get(num) != 0 : resultlist.append(num) dictn[num] = dictn[num] - 1 return resultlist

    功能3:哈希表实现位置记录

    例5:LeetCode166 从分数到小数

    LeetCode166 从分数到小数 给定两个整数,分别表示分数的分子 numerator 和分母 denominator,以字符串形式返回小数。 如果小数部分为循环小数,则将循环的部分括在括号内。 示例 1: 输入: numerator = 1, denominator = 2 输出: "0.5" 示例 2: 输入: numerator = 2, denominator = 1 输出: "2" 示例 3: 输入: numerator = 2, denominator = 3 输出: "0.(6)"

    做这道题目之前,需要有一个数学定理的支撑:

    当余数出现循环时,其对应的商也会出现循环。

    因此我们需要通过哈希表记录余数和商出现的位置。

    那么问题来了,下面那种映射关系才是考虑周全的呢?

    A. 余数 --> 位置

    B. 余数+商 --> 位置

    答案应该是B。读者可通过1/7和1/6两个示例进行对比验证。

    哈希表仅仅只是这道题目的解决工具,而这道题目的难点在于分类讨论与边界条件的考虑。该问题可分成整数部分和小数部分两部分进行解决,而每部分的算法设计中又要分不同情况进行考虑,尤其需要对特殊情况进行考虑。

    边界条件是LeetCode面试的考察重点,尤其对于测试岗位的面试来说,边界条件是否考虑周全是面试官所重点关注的。下面给出一些可能的边界条件,这些边界条件,你考虑全了吗?

    类子类型案例0的影响除数为00/10的影响被除数为01/0结果不完整只有整数部分4/2结果不完整只有小数部分1/3负数的影响分子为负数(-1)/ 3负数的影响分母为负数(2)/(- 3)负数的影响分子分母均为负数(-2)/(- 3)小数点后的不同情况小数点后需要添加01/30小数点后的不同情况小数点后为有限小数1/8小数点后的不同情况小数点后为无限循环小数1/7小数点后的不同情况小数点后为无限循环不小数该情况不可能出现

    下面给出参考代码:

    class Solution(object): def fractionToDecimal(self, numerator, denominator): result = "" dictn = {} # to store the postion of the fractional part position = 0 repeat = True def add(result, position, nstr): return result + nstr, position + len(nstr) #special case if numerator * denominator < 0: result, position = add(result, position, "-") if denominator == 0: return numerator, denominator = abs(numerator), abs(denominator) # calculate the part before "." if numerator >= denominator: result, position = add(result, position, str(numerator / denominator)) numerator = numerator % denominator else: result, position = add(result, position, "0") # calculate the part after "." # numerator % demoninator == 0 if numerator == 0: return result else: result, position = add(result, position, ".") # add zero numerator = numerator * 10 while numerator < denominator : result, position = add(result, position, "0") numerator = numerator * 10 while numerator % denominator != 0 : #print(dictn) reminder = numerator % denominator key = str(reminder) + " " + str(numerator / denominator) # Attention!!! We must treat both reminder and numerator / denominator as key if not dictn.get(key) : dictn[key] = position position = position + 1 else: position = dictn.get(key) repeat = False break result = result + str(numerator / denominator) numerator = reminder * 10 if repeat: result = result + str(numerator / denominator) else: result = result[:position] + "(" + result[position:] + ")" return result

    功能4:哈希表对同一特性的元素进行存储

    例6:LeetCode1424 对角线遍历 II

    LeetCode1424 对角线遍历 II 给你一个列表 nums ,里面每一个元素都是一个整数列表。请你依照下面各图的规则,按顺序返回 nums 中对角线上的整数。 示例1: 输入:nums = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,4,2,7,5,3,8,6,9] 示例2: 输入:nums = [[1,2,3,4,5],[6,7],[8],[9,10,11],[12,13,14,15,16]] 输出:[1,6,2,8,7,3,9,4,12,10,5,13,11,14,15,16]

    方法1:暴力破解法

    该题目首先想到的是暴力破解法,按照从上到小、从左到右的顺序进行遍历,具体的遍历方法如下图所示。然而这种方法会出现超时的现象。

    class Solution(object): def findDiagonalOrder(self, nums): def findmaxclength(nums): maxl = 0 for i in nums: maxl = max(len(i), maxl) return maxl maxc = findmaxclength(nums) maxr = len(nums) print(maxc, maxr) resultlist = [] # first column: from top to end for i in range(maxr): r_id = i while r_id >= 0: # i - c_id : row id c_id : column_id if i - r_id <= len(nums[r_id]) - 1: # to prevent index out of range resultlist.append(nums[r_id][i - r_id]) r_id = r_id - 1 # last row: from left to right for i in range(maxr, maxr + maxc): r_id = maxr - 1 while r_id >= 0: if i - r_id <= len(nums[r_id]) - 1: # to prevent index out of range resultlist.append(nums[r_id][i - r_id]) r_id = r_id - 1 return resultlist

    方法2:哈希表法

    如何对暴力破解中的超时问题进行优化呢?我们可以利用哈希表,将row+col相等的数存在列表中放入哈希表。哈希表的键为row+col,哈希表中的值为row+col相等的数所存放的列表,这样可以大大减少访问次数。

    from collections import defaultdict class Solution(object): def findDiagonalOrder(self, nums): dictn = defaultdict(list) resultlist = [] for row in range(len(nums)): for col in range(len(nums[row])): dictn[row + col].append(nums[row][col]) for key in sorted(dictn.keys()): resultlist.extend(dictn[key][::-1]) return resultlist

    一点小总结

    哈希表数据结构在解决LeetCode问题时,有以下几种功能: 1.通过哈希表实现快速查询 2.通过哈希表进行计数 3.通过哈希表进行位置记录 4.通过哈希表对同一特性的元素进行存储
    Processed: 0.014, SQL: 8