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

Leetcode系列 专栏收录该内容
2 篇文章 0 订阅

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

引言

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

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

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

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

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

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

Python字典的基本操作

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

方法作用
Dict = {}创建字典
Dict[“a”] = 1键a的值为1
Dict.get(“a”)获取键a的值,没有则返回空
Dict.has_key(key)字典是否有键a,没有则返回False,有返回True
Dict.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/1
0的影响被除数为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.通过哈希表对同一特性的元素进行存储
  • 0
    点赞
  • 1
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值