当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
1.创建一个长度为26的数组 arr,所有index对应的值都初始化为0
2.遍历字符串s,找到每个字母在数组里面的相对位置,数量每次增加1: arr [ord(s[i]) - ord('a')] += 1
3.遍历字符串t,找到每个字母在数组里面的相对位置,数量每次减少1, arr [ord(t[i]) - ord('a')] -= 1
4. 遍历字符串,如果找到一个非0的值,就说明s和t不是异位词,否则返回true
T: O(N) N是字符串长度
S: O(S) S=26,相当于字符集的大小,可以看成是O1)
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t): return False
bucket = [0] * 26
for i in range(len(s)):
bucket[ord(s[i]) - ord('a')] += 1
bucket[ord(t[i]) - ord('a')] -= 1
for i in range(len(bucket)):
if bucket[i] != 0:
return False
return True
2022年11月linkedin面试题,面试官给我的是两个已经升序排列的数组,让我求交集;
follow up问题是:如果数组里面有重复的元素,如何能够做到去重
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
d = collections.defaultdict()
res = []
for num in nums1:
if num not in d:
d[num] = 1
for num in nums2:
if num in d and d[num] != 0:
res.append(num)
d[num] -= 1
return res
T:max(m,n) 时间复杂度是两个数组里面取最大值
S:max(m,n) 空间复杂度是把其中一个数组放入哈希表的值,也是两个数组的size中取最大值
如果使用哈希集合存储元素,则可以在 O(1) 的时间内判断一个元素是否在集合中,从而降低时间复杂度。
首先使用两个集合分别存储两个数组中的元素,然后遍历较小的集合,判断其中的每个元素是否在另一个集合中,如果元素也在另一个集合中,则将该元素添加到返回值。该方法的时间复杂度可以降低到 O(m+n)。
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
set1 = set(nums1)
set2 = set(nums2)
if len(set1) > len(set2):
return [num for num in set1 if num in set2]
return [num for num in set2 if num in set1]
因为这样能够让遍历的时间复杂度尽可能的降低。遍历较小的集合(O(m)),查询较大的集合(O(1)),时间复杂度就是较小的集合。如果反过来,时间复杂度就会取决于较大的集合。所以用集合做这个题目的时候,一定要去思考:两个数组的大小关系是怎么样的。
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。
首先对两个数组进行排序,然后使用两个指针遍历两个数组。可以预见的是加入答案的数组的元素一定是递增的,为了保证加入元素的唯一性,我们需要额外记录变量 pre 表示上一次加入答案数组的元素(在python里面,pre 可以用 res[-1]来进行表示,回想下 time schedule的题目例子)。
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,且该数字不等于 pre ,将该数字添加到答案并更新 pre 变量,同时将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。
O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组排序的时间复杂度分别是 O(mlogm) 和 O(nlogn),双指针寻找交集元素的时间复杂度是O(m+n),因此总时间复杂度是 O(mlogm+nlogn)。
O(logm+logn),其中 m 和 n 分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间。(如果题目已经完成排序,空间复杂度就是 O(1),不算输出数组的大小)
# 如果排序的两个方法是并列的,时间复杂度应该是相加,还是两者取较大值?
在leetcode给的官方题解里面,因为两个sort是平行的关系,所以总的时间复杂度应该是两个sort的时间复杂度之和。可以思考成线程的一个关系,程序要先执行这个条件,再执行下一个条件,所以时间一定是相加,而不是取两个sort时间里面的较大值!一定要更正这个思维误区!总而言之:平行关系的时间复杂度,在数学上是相加的关系!
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort()
nums2.sort()
idx1, idx2 = 0,0
res = []
while idx1 < len(nums1) and idx2 < len(nums2):
if nums1[idx1] < nums2[idx2]:
if not res or res[-1] != nums1[idx1]:
res.append(nums1[idx1])
idx1 += 1
elif nums1[idx1] > nums2[idx2]:
if not res or res[-1] != nums1[idx2]:
res.append(nums2[idx2])
idx2 += 1
else:
res.append(nums1[idx1])
idx1 += 1
idx2 += 1
return res
class Solution:
def isHappy(self, n: int) -> bool:
seen = set()
while n != 1:
number = self.calculate_sum(n)
if number in seen:
return False
if number == 1:
return True
if number not in seen and number != 1:
seen.add(number)
n = number
return True # 最后还是要return true,因为要考虑到 n = 1 的情况!
def calculate_sum(self, n):
cur_sum = 0
while n:
cur_sum += (n % 10) * (n % 10)
n //= 10
return cur_sum
1. 这个题我做了很多次都做错,主要原因是在于,这个n有两个功能:
一、需要一个while循环把n遍历到0为止,这样可以把n所有位置上面的digit的平方相加,计算出新一层所需要的n。 -- 这个部分就是 getNext() 函数所实现的功能
二、需要把新的这个n重新放到一个while循环去遍历,并且每次都对这个n进行判断:
a)n == 1?如果相等,直接返回 true
b) n != 1 但是 n 已经出现过? 说明已经陷入了死循环,直接返回 False
c)n != 1 且 n 没有出现过:把 n 加入到 seen 的集合当中,继续重复遍历
class Solution:
def isHappy(self, n: int) -> bool:
seen = set()
while n != 1:
number = self.getNext(n)
if number in seen:
return False
if number == 1:
return True
if number not in seen and number != 1:
seen.add(number)
n = number
return True # 最后还是要return true,因为要考虑到 n = 1 的情况!
def getNext(self, n):
cur_sum = 0
while n:
cur_sum += (n % 10) * (n % 10)
n //= 10
return cur_sum
通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。
意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。
不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。
1.slow指针从 n 开始,fast指针从 getNext(n) 开始
2. slow指针每次走一步, fast指针每次走两步 (getNext(getNext(n)))
def isHappy(self, n):
def hasNext(n):
cur_sum = 0
while n:
cur_sum += (n % 10) * (n % 10)
n //= 10
return cur_sum
slow, fast = n, hasNext(n)
while slow != 1 and fast !=1 and slow != fast:
# print("slow",slow,"fast",fast)
slow = hasNext(slow) # slow 指针走一步
fast = hasNext(hasNext(fast)) # fast指针走两步
if slow == fast:
return False
return True
O(logn)。该分析建立在对前一种方法的分析的基础上,但是这次我们需要跟踪两个指针而不是一个指针来分析,以及在它们相遇前需要绕着这个循环走多少次。
如果没有循环,那么快跑者将先到达 1,慢跑者将到达链表中的一半。我们知道最坏的情况下,成本是 O(logn)。
一旦两个指针都在循环中,在每个循环中,快跑者将离慢跑者更近一步。一旦快跑者落后慢跑者一步,他们就会在下一步相遇。假设循环中有 k 个数字。如果他们的起点是相隔 k-1 的位置(这是他们可以开始的最远的距离),那么快跑者需要 k-1步才能到达慢跑者,这对于我们的目的来说也是不变的。因此,主操作仍然在计算起始 n 的下一个值,即O(logn)。
O(1),对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间。
def twoSum(self, nums: List[int], target: int) -> List[int]:
d = collections.defaultdict()
res = []
for idx, num in enumerate(nums):
if target - num in d:
res.append(idx)
res.append(d[target-num])
return res
else:
d[num] = idx