LeetCode 剑指Offer 数据结构之 哈希表 总结 Part1
python中的哈希表主要涉及到字典和集合,剑指Offer中涉及到3道题,今天来做一个总结。哈希表的优点是查找时间降低为O(1),主要出现在问题中涉及到“重复”,“不重复”,“唯一”等要求。一般哈希表的考察会和其他算法,数据结构相结合。
剑指Offer03:
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof
解法1:不考虑索引,维护一个哈希数据结构:集合
def findRepeatNumber(self, nums):
d = set()
for i in nums:
if i not in d:
d.add(i)
else:
return i
return None
算是python的语法糖了,一找到就返回
复杂度分析:
时间复杂度 O(N): 遍历数组使用 O(N) ,HashSet 添加与查找元素皆为 O(1)。
空间复杂度 O(N) : HashSet 占用 O(N) 大小的额外空间。
优化方向,哈希表大小可否替换为O(1)?
解法2:
充分利用题设,考虑索引,一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内,说明必然有 元素的 索引 和 值 是 一对多 的关系。
做法是,遇到一个数,将其替换到对应的索引位置上去。如果再遇到一个数,替换到索引的位置上时和原索引位置上的数一致,则找到重复。
def findRepeatNumber(self, nums):
i = 0
while i<len(nums):
if nums[i]==i:
i+=1
continue
if nums[nums[i]]==nums[i]:
return nums[i]
else:
nums[nums[i]],nums[i] = nums[i],nums[nums[i]]
return None
复杂度分析:
时间复杂度 O(N): 遍历数组使用 O(N) ,每轮遍历的判断和交换操作使用 O(1) 。
空间复杂度 O(1) : 使用常数复杂度的额外空间。
剑指 Offer 50:
第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = “abaccdeff”
返回 “b”
s = “”
返回 " "
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof
因为涉及到第x次出现+重复,如果明确要记录第x次出现做返回显然集合是做不了了,使用字典作为哈希数据结构。key为值,value为出现的位置索引。
解法1:
def firstUniqChar(self, s: str) -> str:
if not s:
return " "
S = {}
for index,i in enumerate(list(s)):
if i in S.keys():
S[i]=index
else:
S[i]=-1
if min(S.values())==-1:
return min(S.keys(), key=(lambda k: S[k]))
else:
return " "
维护字典S,发现再次出现就更新value,第一次出现就置value=-1
更新完字典以后,因为字典是无序的,需要找到字典value最小值对应的key进行返回,采用lambda表达式:key=(lambda k: S[k]) 取出S中key = k的值
min(object, function)
表示用function对object(可以是dict,list等……)做一轮映射,取出映射后的最小值对应的object元素
min(S.keys(), key=(lambda k: S[k])) 对S的key做key->value的映射,并取出value最小值对应的key
解法2:
考虑只求第一次找到,就可以返回,无需记录具体的出现位置,还是可以用集合来做。
def firstUniqChar(self, s: str) -> str:
dic = {}
for c in s:
dic[c] = not c in dic
for c in s:
if dic[c]: return c
return ' '
解法3:
python中具有有序set
import collections
def firstUniqChar(self, s):
'''
有序哈希表 插入的key有顺序
:param s:
:return:
'''
dic = collections.OrderedDict()
for c in s:
dic[c] = not c in dic
for k, v in dic.items():# 遍历的是dict 不是s
if v:
return k
return ' '
python 3.6后集合便是有序集合,所以:
dic = {} 等价于 dic = collections.OrderedDict()
集合的优化原理详见:
https://www.cnblogs.com/xieqiankun/p/python_dict.html
原先是一个83的二维数组 查询做遍历
现在是长度为8的一维数组。然后又生成了一个空的二维数组 None3
前者存插入的索引,后者存hash值,key,value指针,查询时查后者不存在跳过,查询效率高!
剑指 Offer 48:
最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof
一般出现找最长显然需要使用动态规划算法
解法1:
def lengthOfLongestSubstring(self, s):
'''
自己写个定义dp空间的
:param s:
:return:
'''
dic = {}
dp = [0 for i in range(len(s))] # dp[j] 表示以 j 为结尾的 最长的不重复的字符串的长度
for j in range(len(s)):
i = dic.get(s[j], -1) # 历史上最近的一次出现s[j]的索引
dic[s[j]] = j # 更新索引
if i < -1:
# 没出现过
dp[j] = dp[j - 1] + 1 # 必然更长+1
elif j - i > dp[j - 1]:
# 距离上次出现s[j]已经较dp[j-1]更长,所以s[j]不在dp[j-1]中,dp[j-1]可以+1
dp[j] = dp[j - 1] + 1 # 必然更长+1
else:
# s[i]在dp[j-1]中,等于dp[j]的序列是基于dp[j-1]的序列,被s[i]截断,长度等于j-1
dp[j] = j-i
# dp空间需要判断下是否等于0
return max(dp) if isinstance(dp,list) and dp else 0
定义状态空间dp : dp[j]是以 j 为结尾的 最长的不重复的字符串的长度
一般dp是把全局最长不断更新到j,最后返回dp[-1]
这个问题里最长问题涉及到两端的比较,dp只是右端结尾的最长值,最后返回全局max(dp),用哈希数据结构:字典(key值,value最近一次出现的位置索引)来判断状态转移的几个分支。
优化方向:
1)dp空间的优化
状态转移只和dp[j],dp[j-1]有关,可以简化为一个常量,并定义一个全局最优值不断更新
2)dp状态转移的优化
3个分支可以合并掉1,2
解法2:
def lengthOfLongestSubstring(self, s):
'''
该做法精简了DP空间 利用哈希表确定集中分支的状态转移
:param s:
:return:
'''
dic = {}
res, tmp = 0, 0
# tmp 意思是dp[j] 以字符s[j] 为结尾,最长不重复子字符串 的长度
# 一般dp都更新到最后一位,返回df[-1]
# 这里通过右边界的形式更新,更新的不是当前全局max,而是右边界max 最后还要取全局max
# res表示历史最长 通过遍历不断更新保留当前全局max 最后返回
for j in range(len(s)):
i = dic.get(s[j], -1) # 获取索引 i# get不到返回-1
dic[s[j]] = j # 更新哈希表 把s[j] 左边最近的相同字符 s[i]更新进去
# 新的字符出现分支+s[i]~s[j] 中间是dp[j-1]的序列,所以dp[j-1]里没有s[j],+1
if tmp < j - i:
tmp = tmp + 1
else:
# s[i] 落在dp[j-1]的序列中,则dp[j] 应该等于s[i]的索引~j
tmp = j - i # dp[j - 1] -> dp[j]
# 更新全局最优
res = max(res, tmp) # max(dp[j - 1], dp[j])
return res
剑指Offer中哈希表的tag只有三道题,已经总结完毕,后续遇到好的题目将再做整理,开启Part2。