Day06 代码随想录刷题
知识点:哈希表、列表、字典、集合、指针
一、LeetCode242. 有效的字母异位词
1.解题思路
考点为哈希表的多种实现方式和使用方式
2.代码实现
2.1 方法一:列表哈希
根据题意仅包含小写字母,其Unicode码点
值为连续的26位,可以利用列表list
创建一个哈希表HashList
,先利用字符串s
填充,再利用字符串t
消除,若出现多消少消的情况,则证明不是异位词。
其中ord()
可实现将变量转为Unicode码点
的int
值
class Solution:
#解法一:列表哈希
def isAnagram(self, s: str, t: str) -> bool:
#数据较少且连续,可用list实现,直接初始化指定szie的哈希表
HashList = [0]*26
#填充哈希表
for i in s:
HashList[ord(i) - ord('a')] += 1
#消除哈希表
for i in t:
HashList[ord(i) - ord('a')] -= 1
#检查哈希表
for i in range(26):
if HashList[i] != 0:
return False
return True
该解法有三个循环,前两个循环是遍历字符串,时间复杂度为O(n)
,最后一个是遍历哈希表,因为哈希表长度固定,故时间复杂度为O(1)
,最终该解法的时间复杂度为O(n)
;
该解法只使用了固定大小的额外空间来存储哈希表,不随输入规模的增长而增加空间消耗,所以空间复杂度 O(1)
。
2.2 方法二:defaultdic字典
defaultdict
是 Python
标准库 collections
模块中的一个类,它是一个字典(dict
)的子类。
defaultdict
接受一个工厂函数作为参数,这个factory_function
可以是list
、set
、str
等等。作用是当key
不存在时,返回的是工厂函数的默认值,比如list
对应[]
,str
对应的是空字符串,set
对应set()
,int
对应0
。
class Solution:
#解法二:defaultdict字典
def twoSum(self, nums: List[int], target: int) -> List[int]:
from collections import defaultdict
s_dict = defaultdict(int)
t_dict = defaultdict(int)
for x in s:
s_dict[x] += 1
for x in t:
t_dict[x] += 1
return s_dict == t_dict
该解法开辟了两个字典,且分别遍历了一遍,其复杂度取决于列表nums
的长度,故时间复杂度为O(n)
,空间复杂度为 O(n)
。
2.3 方法三:Counter
Counter
是 Python
标准库 collections
模块中的一个类,用于计数可哈希对象的出现次数。它是一个无序的容器类型,可以用于快速计数和统计元素,即自动生成哈希字典。
可以通过向 Counter
构造函数传递一个可迭代对象(如列表、字符串等)来创建一个 Counter
对象。 Counter
会统计该可迭代对象中每个元素出现的次数,并以字典的形式存储,其中key
是元素,value
是对应的出现次数。
class Solution:
#解法三:Counter哈希字典
def isAnagram(self, s: str, t: str) -> bool:
from collections import Counter
# Counter返回的是一个哈希表字典
s_count = Counter(s)
t_count = Counter(t)
return s_count == t_count
该解法存在两个随nums
变化的容器,创建和比较都是根据容器大小而定,所以时间复杂度为O(n)
,空间复杂度是 O(n)
二、LeetCode349.两个数组的交集
1.解题思路
考察哈希表的不同创建方式和使用,考察了列表、集合的特点
2.代码实现
2.1 方法一:字典哈希+列表
上一题讲到哈希字典,提到了defaultdict
和Counter
两种方式,这2种方式都是封装好可以直接用的dict
。面对大小未知,且具有可选默认值的哈希表可通过dict.get(key,default_value)
来填充。
本解法不能通过HashDict[num] += 1
来填充,因为第一个HashDict[num]
不存在,他没有初始化为0
,而HashDict.get(key,default_value)
顺带进行了初始化。
class Solution:
#解法一:字典哈希+列表
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
#创建哈希字典
HashDict = {}
#用nums1填充哈希表
for num in nums1:
HashDict[num] = HashDict.get(num,0) + 1
resList = []
#将nums2的值与哈希表进行比较,重复值取出存入list后从哈希表中消除
for num in nums2:
if num in HashDict:
resList.append(num)
del HashDict[num]
#返回结果list
return resList
该解法创建并遍历了一个随nums1
长度变化的哈希表,还遍历了nums2
长度的列表,所以时间复杂度为O(n)
;所以空间复杂度 O(n)
。
2.2 方法二:字典哈希+集合
上一解法通过列表进行存储交集元素,为了避免重复,每次存储后都在哈希表中进行了消除操作,方法二通过集合可避免重复,一步到位。
class Solution:
#解法二:字典哈希+集合
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
HashDict = {}
for num in nums1:
HashDict[num] = HashDict.get(num,0) + 1
#创建集合
resSet = set()
#将nums2的值和哈希表进行比较,重复值存入set
for num in nums2:
if num in HashDict:
resSet.add(num)
#转换格式后返回
return list(resSet)
该解法和解法二一样,故时间复杂度为O(n)
,空间复杂度为 O(n)
。
2.3 方法三:列表哈希
利用哈希列表实现取交集操作
class Solution:
#解法三:列表哈希
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
#初始化哈希表
HashList1 = [0]*1001
HashList2 = [0]*1001
resList = []
#填充哈希表
for num in nums1:
HashList1[num] += 1
for num in nums2:
HashList2[num] += 1
#取交集
for i in range(1001):
if HashList1[i]*HashList2[i] > 0:
resList.append(i)
return resList
该解法存在两个哈希列表,哈希表创建时空间复杂度为O(1)
,遍历nums
填充哈希表时时间复杂度为O(n)
,最后取交集操作遍历了哈希表,时间复杂度为O(1)
.虽然本题列表长度和元素范围都固定了,但是本解法还是倾向于认为时间复杂度为O(n)
,空间复杂度是 O(n)
2.4 方法四:列表哈希
直接利用集合取交集的操作(&
)完成任务
class Solution:
#解法四:集合
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
该解法将两个nums转化为集合,时间复杂度为O(n)
,取交集操作时间复杂度最大不超过O(n)
,故时间复杂度为O(n)
;创建了两个集合以及取交集三个空间,所以空间复杂度也为O(n)
。
三、LeetCode202.快乐数
1.解题思路
无限循环意味着重复出现同样的平方和,可用哈希表记录,考点还是哈希表的创建和使用。
2.代码实现
2.1 方法一:集合
num, r = divmod(num,10)
函数输入除数num
和被除数10
,返回商num
和余数r
。
class Solution:
#解法一:集合
def isHappy(self, n: int) -> bool:
#创建集合
resSet = set()
while n not in resSet:
#加入集合
resSet.add(n)
#更新n
n = self.getNext(n)
if n == 1:return True
#出现重复,退出 while 循环
return False
#将当前num按每位取平方和
def getNext(self, num):
nextNum = 0
while num:
num, r = divmod(num,10)
nextNum += r**2
return nextNum
两个函数分别说明:
getNext
函数的空间复杂度为O(n)
,由于数字n
最大有log(n) + 1
位,所以getNext
函数的时间复杂度可以表示为O(log(n))
;
isHappy
函数的空间复杂度为O(k)
,其中k
是最大的更新次数(k
取决于输入数字n
),时间复杂度为O(log(n))
。
class Solution:
#解法一的精简版:集合
def isHappy(self, n: int) -> bool:
#创建集合
resSet = set()
while n != 1:
#出现重复,返回
if n in resSet:return False
#加入集合
resSet.add(n)
#按位求平方和,更新n
n = sum(int(i)**2 for i in str(n))
#出现数字1,结束循环
return True
空间复杂度为 O(n)
,时间复杂度为O(log(n))
。
2.2 方法二:列表
和方法一类似,几乎一样
class Solution:
#解法二:列表
def isHappy(self, n: int) -> bool:
resList = []
while n not in resList:
#加入列表
resList.append(n)
#按位求平方和,更新n
nextNum = 0
n_str = str(n)
for i in n_str:
nextNum += int(i)**2
n = nextNum
if n == 1:return True
return False
空间复杂度为 O(n)
,时间复杂度为O(log(n))
,其中 k
是更新次数的上限。
class Solution:
#解法二的精简版:列表
def isHappy(self, n: int) -> bool:
#创建列表
resList = []
while n != 1:
#出现重复,返回
if n in resList:return False
#加入列表
resList.append(n)
#按位求平方和,更新n
n = sum(int(i)**2 for i in str(n))
#出现数字1,结束循环
return True
空间复杂度为 O(k)
,时间复杂度为O(log(n))
,其中 k
是更新次数的上限。
2.3 方法三:快慢指针
快乐数的按位求平方和出现循环这一情况与链表有环算法的思路类似。
class Solution:
#解法三:快慢指针
def isHappy(self, n: int) -> bool:
slow = n
fast = n
while self.getNext(fast) != 1 and self.getNext(self.getNext(fast)) != 1:
slow = self.getNext(slow)
fast = self.getNext(self.getNext(fast))
if slow == fast:
return False
return True
def getNext(self, num):
nextNum = 0
while num:
num, r = divmod(num,10)
nextNum += r**2
return nextNum
空间复杂度为O(1)
,对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间,时间复杂度为O(log(n))
四、LeetCode1.两数之和
1.解题思路
只找两个数,在先选定一个数value
的情况下,另一个目标数(target - value
)就已经确定了,在哈希表中寻找该目标数,若不存在则将当前value和索引存入哈希表,否则输出结果。
2.代码实现
2.1方法一:暴力解法
从头开始遍历,知道找到目标索引
class Solution:
#解法一:暴力解法(非常慢)
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)):
for j in range(i+1,len(nums)):
if nums[i] + nums[j] == target:
return [i,j]
时间复杂度为O(n^2)
,空间复杂度 O(1)
。
2.2方法二:字典
定义一个key为元素值,value为索引的反向哈希字典。
enumerate()
是一个内置函数,用于在迭代过程中同时获取索引和对应的元素。它接受一个可迭代对象作为参数,并返回一个由索引和元素组成的枚举对象。
class Solution:
#解法二:字典{value:index, ...}
def twoSum(self, nums: List[int], target: int) -> List[int]:
#创建字典
resDict = {}
#遍历nums的索引和元素值
for index,value in enumerate(nums):
#目标数在字典的key内,返回答案
if target - value in resDict:
return [resDict[target - value], index]
#目标数不在字典key内,当前遍历的数和索引加入字典
resDict[value] = index
return []
时间复杂度为O(n)
,最坏情况下可能涉及存储所有元素,故空间复杂度为O(n)
。
2.3方法三:集合
解法三类似解法二的字典,不同之处在于定义了一个集合,用于存储元素值。最后通过对目标值求原list的index来找回去索引。
class Solution:
#解法三:集合(记录value,根据value再回list里面找索引)
def twoSum(self, nums: List[int], target: int) -> List[int]:
#创建集合
resSet = set()
#遍历nums的索引和元素值
for index, value in enumerate(nums):
temp = target - value
#目标数在集合内,返回答案
if temp in resSet:
return [nums.index(temp),index]
#目标数不在集合内,当前遍历的数加入集合
resSet.add(value)
时间复杂度为O(n)
,空间复杂度为O(n)
。
2.4方法四:双指针
其中sorted()
函数使用的是Timsort
算法,是一种混合了归并排序和插入排序的稳定排序算法,Timsort
在平均情况下具有 O(nlogn)
的时间复杂度,其中 n
是待排序列表的长度。
class Solution:
#解法四:双指针
def twoSum(self, nums: List[int], target: int) -> List[int]:
#列表升序处理
nums_sorted = sorted(nums)
left = 0
right = len(nums) - 1
while left < right:
#计算当前两指针所指元素值之和
curRes = nums_sorted[left] + nums_sorted[right]
if curRes == target:
#满足两数之和的要求后求解各自index
left_index = nums.index(nums_sorted[left])
right_index = nums.index(nums_sorted[right])
#如果索引相同,代表list中存在2个元素值相同,且都是答案
if left_index == right_index:
right_index = nums[left_index + 1:].index(nums_sorted[right]) + left_index + 1
return [left_index, right_index]
#不满足两束之和则指针移动
elif curRes < target: left += 1
else: right -= 1
该解法先将数组排序好O(nlogn)
,再利用双指针法遍历一遍O(n)
得到结果,其实方法nums.index()
也是O(n)
,但最终时间复杂度还是O(nlogn)
, 为了保存下标信息另开了一个数组nums_sorted
,所以空间复杂度为O(n)
。