查找:查找表、对撞指针、滑动数组、二分查找

查找表

考虑的基本数据结构

第一类:查找有无——set

  • 元素’a’是否存在,通常用set:集合
  • set只存储键,而不需要对应其相应的值。
  • set中的键不允许重复

第二类:查找对应关系(键值对应)——dict

  • 元素’a’出现了几次:dict → \rightarrow 字典
  • dict中的键不允许重复

第三类:改变映射关系——map

  • 通过将原有序列的关系映射统一表示为其他

349.两个数组的交集1【简单】

LeetCode传送门
给定两个数组,编写一个函数来计算它们的交集。
说明:输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序。

示例 :

  • 输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
  • 输出:[9,4]

思路1:
把nums1记录为set,判断nums2的元素是否在set中,是的话,就放在一个公共的set中,最后公共的set就是我们要的结果。

代码:

class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
	nums1 = set(nums1)
	return set([i for i in nums2 if i in nums1])

思路2:
通过set的内置方法来实现,直接求set的交集。

代码:

class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
	set1 = set(nums1)
	set2 = set(nums2)
	return set2 & set1 # 或 return set1.intersection(set2)

350.两个数组的交集2【简单】

LeetCode传送门
给定两个数组,编写一个函数来计算它们的交集。
说明:输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。我们可以不考虑输出结果的顺序。

示例:

  • 输入:nums1 = [1,2,2,1], nums2 = [2,2]
  • 输出:[2,2]

思路:
元素出现的次数有用,那么对于存储次数就是有意义的,所以选择数据结构时,就应该选择dict的结构,通过字典的比较来判断;记录每个元素的同时要记录这个元素的频次。
记录num1的字典,遍历nums2,比较nums1的字典的nums的key是否大于零,从而进行判断。

代码:

class Solution:
    def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
        from collections import Counter
        nums1_dict=Counter(nums1)
        res=[]
        for num in nums2:
            if nums1_dict[num]>0:
                res.append(num)
                nums1_dict[num]-=1
        return res

242.有效的字母异位词【简单】

LeetCode传送门
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1:

  • 输入: s = “anagram”, t = “nagaram”
  • 输出: true

思路:
判断异位词即判断变换位置后的字符串和原来是否相同,那么不仅需要存储元素,还需要记录元素的个数。可以选择dict的数据结构,将字符串s和t都用dict存储,而后直接比较两个dict是否相同。

代码:

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        from collections import Counter
        s=Counter(s)
        t=Counter(t)
        if s==t:
            return True
        else:
            return False

202.快乐数【简单】

LeetCode传送门
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。

示例:

  • 输入:19
  • 输出:true
  • 解释:
    1 2 + 9 2 = 82 8 2 + 2 2 = 68 6 2 + 8 2 = 100 1 2 + 0 2 + 0 2 = 1 1^2 + 9^2 = 82\\ 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1 12+92=8282+22=6862+82=10012+02+02=1

思路:
重复执行「将该数替换为它每个位置上的数字的平方和」这一过程,猜测会有以下三种可能:

  • 最终会得到1;
  • 最终会进入循环;
  • 值会越来越大,最后接近无穷大。

第三个情况比较难以检测和处理。考虑各个位数的最大数字的下一位数是多少:

DigitsLargestNext
1981
299162
3999243
49999324
1399999999999991053

对于 33 位数的数字,它不可能大于 243243。这意味着它要么被困在 243243 以下的循环内,要么跌到 11。44 位或 44 位以上的数字在每一步都会丢失一位,直到降到 33 位为止。所以我们知道,最坏的情况下,算法可能会在 243243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 11。但它不会无限期地进行下去,所以我们排除第三种选择。

这样这道题的思路就很明显了,当 n 不等于 1 时就循环,每次循环时,将其最后一位到第一位的数依次平方求和,比较求和是否为1。那么这里也可以按此判断,因为只需要判断有或无,不需要记录次数,故用set的数据结构。每次对求和的数进行add,当新一次求和的值存在于set中时,就return false.

代码:

class Solution:
    def isHappy(self, n: int) -> bool:
        exists=set()
        while n!=1:
            sums=0
            while n>0:
                tmp=n%10
                sums+=tmp**2
                n//=10
            if sums in exists:
                return False
            else:
                exists.add(sums)
            n=sums
        return True

290.单词规律【简单】

LeetCode传送门
给定一种规律 pattern 和一个字符串 str ,判断 str 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 str 中的每个非空单词之间存在着双向连接的对应规律。你可以假设 pattern 只包含小写字母, str 包含了由单个空格分隔的小写字母。

示例:

  • 输入: pattern = “abba”, str = “dog cat cat dog”
  • 输出: true

思路:
如何能既考虑顺序,也考虑 p a t t e r n pattern pattern s t r str str 键值对应的关系呢?

抓住变与不变,变的是键,但是不变的是各个字典中,对应的相同 i n d e x index index 下的值,如 d i c t 1 [ i n d e x ] = d i c t 2 [ i n d e x ] dict1[index] = dict2[index] dict1[index]=dict2[index],那么我们可以创建两个新的字典,遍历index对两个新的字典赋值,并比较value。

还有一个思路比较巧妙,既然不同,那么可以考虑怎么让它们相同,将原来的dict通过map映射为相同的key,再比较相同key的dict是否相同。

代码:
注:map是通过hash存储的。

class Solution:
    def wordPattern(self, pattern: str, str: str) -> bool:
        str=str.split()
        return list(map(pattern.index,pattern))==list(map(str.index,str))

205.同构字符串【简单】

LeetCode传送门
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。你可以假设 s 和 t 具有相同的长度。

示例 1:

  • 输入: s = “egg”, t = “add”
  • 输出: true

示例 2:

  • 输入:s = “foo”, t = “bar”
  • 输出:false

思路:
可以考虑通过建两个dict,比较怎样不同,也可以将不同转化为相同。

代码:

class Solution:
    def isIsomorphic(self, s: str, t: str) -> bool:
        n=len(s)
        st,ts={},{}
        for i in range(n):
            if s[i] in st:
                if t[i]!=st[s[i]]:
                    return False
            if t[i] in ts:
                if s[i]!=ts[t[i]]:
                    return False
            st[s[i]]=t[i]
            ts[t[i]]=s[i]
        return True
class Solution:
	def isIsomorphic(self, s: str, t: str) -> bool:
		return list(map(s.index,s)) == list(map(t.index,t))

451.根据字符出现频率排序【中等】

LeetCode传送门
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

示例 :

  • 输入:“tree”
  • 输出:“eert”
  • 解释:'e’出现两次,'r’和’t’都只出现一次。因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。

思路:
使用字典统计频率,对字典的value进行排序,最终根据key的字符串乘上value次数,组合在一起输出。

代码:

class Solution:
    def frequencySort(self, s: str) -> str:
        from collections import Counter
        s_dict=Counter(s)
        # sorted 返回的是列表元组
        s=sorted(s_dict.items(),key=lambda item:item[1],reverse=True)
        ans=''
        for k,v in s:
            ans+=k*v
        return ans

对撞指针

当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O ( n 2 ) O(n^2) O(n2) 减少至 O ( n ) O(n) O(n)

1.两数之和【简单】

LeetCode传送门
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

思路1: 暴力法 O ( n 2 ) O(n^2) O(n2)
第一遍遍历数组,第二遍遍历当前遍历值之后的元素,其和等于 target 则 return。

代码:

class Solution:
	def twoSum(self, nums: List[int], target: int) -> List[int]:
		n=len(nums)
		for i in range(n):
			for j in range(i+1,n):
				if nums[i]+nums[j]==target:
					return [i,j]

思路2: 排序+指针对撞 O ( n ) + O ( n log ⁡ n ) = O ( n ) O(n)+O(n\log n)=O(n) O(n)+O(nlogn)=O(n)
因为问题本身不是有序的,因此需要对原来的数组进行一次排序,排序后就可以用 O ( n ) O(n) O(n)的指针对撞进行解决。

但是问题是,返回的是数字的索引,如果只是对数组的值进行排序,那么数组原来表示的索引的信息就会丢失,所以在排序前要进行些处理。

  • 如果只是对数组的值进行排序,那么数组原来表示的索引的信息就会丢失的情况,可以在排序前:通过list(enumerate(nums))开始实现下标和值的绑定。

代码:

class Solution:
    def twoSum(self,nums,target):
        nums=list(enumerate(nums))
        nums.sort(key=lambda x:x[1])
        l,r=0,len(nums)-1
        while l<r:
            if nums[l][1]+nums[r][1]>target:
                r-=1
            elif nums[l][1]+nums[r][1]<target:
                l+=1
            else:
                return nums[l][0],nums[r][0]

思路3: 查找表 O ( n ) O(n) O(n)
遍历数组过程中,当遍历到元素v时,可以只看v前面的元素,是否含有target-v的元素存在。

  • 如果查找成功,就返回解;
  • 如果没有查找成功,就把v放在查找表中,继续查找下一个解。

即使 v 放在了之前的查找表中覆盖了 v,也不影响当前 v 元素的查找。因为只需要找到两个元素,只需要找 target-v 的另一个元素即可。

代码:

class Solution:
    def twoSum(self,nums,target):
        record=dict()
        for i in range(len(nums)):
            complement=target-nums[i]
            if record.get(complement) is not None:
                return i,record[complement]
            record[nums[i]]=i

15.三数之和【中等】

LeetCode传送门
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。

  • 返回结果为全部解,不需要考虑解的顺序;
  • 不同的三元组是指值不同;
  • 没有解时返回空列表。

示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]

思路:

  1. 为了保证不重复,可以限定 a ≤ b ≤ c a\leq b\leq c abc
    保证只有 ( a , b , c ) (a,b,c) (a,b,c)这个顺序会被枚举到,而 ( b , a , c ) (b,a,c) (b,a,c) ( c , b , a ) (c,b,a) (c,b,a)等就不会被枚举到。要实现这一点,可以将数组中的元素按照从小到大进行排序。
  2. 对于每一重循环而言,相邻两次枚举的元素不能相同,否则也会造成重复。
    举个例子,如果排完序的数组为[0, 1, 2, 2, 2, 3],使用三重循环枚举到的第一个三元组为 (0,1,2),如果第三重循环继续枚举下一个元素,那么仍然是三元组 (0,1,2),产生了重复。因此我们需要将第三重循环「跳到」下一个不相同的元素,即数组中的最后一个元素 33,枚举三元组 (0, 1, 3)(0,1,3)。
  3. 第二重循环和第三重循环实际上是并列的关系
    如果我们固定了前两重循环枚举到的元素 a 和 b,那么只有唯一的 c 满足 a + b + c = 0 a+b+c=0 a+b+c=0。当第二重循环往后枚举一个元素 b ′ b' b 时,由于 b ′ > b b' > b b>b,那么满足 a + b ′ + c ′ = 0 a+b'+c'=0 a+b+c=0 c ′ c' c 一定有 c ′ < c c' < c c<c,即 c ′ c' c 在数组中一定出现在 c 的左侧。也就是说,我们可以从小到大枚举 b,同时从大到小枚举 c,即第二重循环和第三重循环实际上是并列的关系。可以使用对撞指针实现。

代码:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        ans=[]
        for i in range(len(nums)-2):
            # 因为是排序好的数组,如果最小的都大于0可以直接排除
            if nums[i]>0:break
            # 排除i的重复值
            if i>0 and nums[i]==nums[i-1]:continue
            l,r=i+1,len(nums)-1
            while l<r:
                sums=nums[i]+nums[l]+nums[r]
                if sums==0:
                    ans.append([nums[i],nums[l],nums[r]])
                    l+=1
                    r-=1
                    while l<r and nums[l]==nums[l-1]:l+=1
                    while l<r and nums[r]==nums[r+1]:r-=1
                elif sums<0:
                    l+=1
                else:
                    r-=1
        return ans

总结:

  • 当数组不是有序时需要注意,有序的特点在哪里,有序就可以用哪些方法解决?无序的话不便在哪里?
  • 处理重复值的套路:先转换为有序数组,再循环判断其与上一次值是否重复:
# 1.
for i in range(len(nums)):
	if i > 0 and nums[i] == nums[i-1]: continue
# 2.
while l < r:
	while l < r and nums[l] == nums[l-1]: l += 1

18.四数之和【中等】

LeetCode传送门
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。

思路:
4Sum可以当作是3Sum问题的扩展,注意事项仍是一样的,同样是不能返回重复值得解。首先排序。接着从 [ 0 , l e n − 1 ] [0,len- 1] [0,len1]遍历 i i i,跳过 i i i 的重复元素,再在 [ i + 1 , l e n − 1 ] [i+1,len-1] [i+1,len1] 中遍历 j j j,得到 i , j i,j ij后,再选择首尾的 l l l r r r,通过对撞指针的思路,四数和大的话 r − − r-- r,小的话 l + + l++ l++,相等的话纳入结果 list,最后返回。
另外,需要加些边界条件判断:当len小于4时,直接返回;当只有4个值且长度等于target时,直接返回本身即可。

代码:
Python标准库itertools.combinations:超出时间限制

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        from itertools import combinations
        ans=set()
        for i in combinations(nums,4):
            if sum(i)==target:
                ans.add(i)
        return list(ans)
class Solution:
	def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
		nums.sort()
		from itertools import combinations
		res = []
		for i in combinations(nums, 4):
			if sum(i) == target:
			res.append(i)
			res = set(res)
		return res
>> from itertools import combinations, permutations
>> permutations([1, 2, 3], 2)
<itertools.permutations at 0x7febfd880fc0>

>> list(permutations([1, 2, 3], 2))
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

>> list(combinations([1, 2, 3], 2))
[(1, 2), (1, 3), (2, 3)]

16.最接近的三数之和【中等】

LeetCode传送门
给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。 3 ≤ n u m s . l e n g t h ≤ 1 0 3 , − 1 0 3 ≤ n u m s [ i ] ≤ 1 0 3 , − 1 0 4 ≤ t a r g e t ≤ 1 0 4 3\leq nums.length\leq 10^3,-10^3\leq nums[i]\leq 10^3,-10^4\leq target\leq 10^4 3nums.length103,103nums[i]103,104target104

示例:

  • 输入:nums = [-1,2,1,-4], target = 1
  • 输出:2
  • 解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

思路:
开始时可以随机设定一个三个数的和为结果值,在每次比较中,先判断三个数的和是否和target相等,如果相
等直接返回和。如果不相等,则判断三个数的和与target的差是否小于这个结果值时,如果小于则进行则进行替
换,并保存和的结果值。

代码:

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        ans=nums[0]+nums[1]+nums[2]
        diff=abs(ans-target)
        for i in range(len(nums)):
            l,r=i+1,len(nums)-1
            t=target-nums[i]
            while l<r:
                if nums[l]+nums[r]==t:
                    return nums[i]+t
                else:
                    if abs(nums[l]+nums[r]-t)<diff:
                        diff=abs(nums[l]+nums[r]-t)
                        ans=nums[i]+nums[l]+nums[r]
                    if nums[l]+nums[r]<t:
                        l+=1
                    else:
                        r-=1
        return ans

454.四数相加2【中等】

LeetCode传送门
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,最终结果不会超过 231 - 1 。

思路1: O ( n 3 ) O(n^3) O(n3)
首先可以考虑把D数组中的元素都放入查找表,然后遍历前三个数组,判断target减去每个元素后的值是否在查找表中存在,存在的话,把结果值加1。那么查找表的数据结构选择用set还是dict?考虑到数组中可能存在重复的元素,而重复的元素属于不同的情况,因此用dict存储,最后的结果值加上dict相应key的value。

代码:

class Solution:
    def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
        from collections import Counter
        record=Counter(D)
        ans=0
        for i in range(len(A)):
            for j in range(len(B)):
                for k in range(len(C)):
                    num_find=0-A[i]-B[j]-C[k]
                    if record.get(num_find)!=None:
                        ans+=record[num_find]
        return ans

思路2:
对于C和D的数组,可以通过dict来记录其中和的个数,之后遍历结果在和中进行查找

代码:

class Solution:
    def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
        from collections import Counter
        record=Counter()
        for i in range(len(A)):
            for j in range(len(B)):
                record[A[i]+B[j]]+=1
        ans=0
        for i in range(len(C)):
            for j in range(len(D)):
                num_find=0-C[i]-D[j]
                if record.get(num_find)!=None:
                    ans+=record[num_find]
        return ans
class Solution:
	def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
		record = collections.Counter(a + b for a in A for b in B)
		return sum(record.get(- c - d, 0) for c in C for d in D)

49.字母异位词分组【中等】

LeetCode传送门
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
所有输入均为小写字母。不考虑答案输出的顺序。

示例:

  • 输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
  • 输出:
    [
    [“ate”,“eat”,“tea”],
    [“nat”,“tan”],
    [“bat”]
    ]

思路:
题目中不变的条件:如果将字符串统一排序,异位词排序后的字符串,显然都是相同的。
可以把异位词排序后的字符串当作key,把遍历的数组中的异位词当作value,对字典进行赋值,进而遍历字典的value,得到结果list。

代码:

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        from collections import defaultdict
        strs_dict=defaultdict(list)
        ans=[]
        for s in strs:
            key=''.join(sorted(list(s)))
            strs_dict[key]+=s.split(',')
        for v in strs_dict.values():
            ans.append(v)
        return ans

defaultdict:字典的子类。
defaultdict与dict实例化字典类型的区别:使用defaultdict,任何未定义的key都会默认返回一个根据method_factory参数不同的默认值, 而相同情况下dict()会返回KeyError。

447.回旋镖的数量【简单】

LeetCode传送门
给定平面上 n 对不同的点,“回旋镖” 是由点表示的元组 (i, j, k) ,其中 i 和 j 之间的距离和 i 和 k 之间的距离相等(需要考虑元组的顺序)。
找到所有回旋镖的数量。你可以假设 n 最大为 500,所有点的坐标在闭区间 [-10000, 10000] 中。

示例:

  • 输入:[[0,0],[1,0],[2,0]]
  • 输出:2
  • 解释:两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]

思路:
开始的思路就是三层遍历,i从0到len,j从i+1到len,k从j+1到len,然后比较三个点的距离,相等则结果数加一,这样的时间复杂度为 O ( n 3 ) O(n^3) O(n3)。考虑采用查找表的思路进行优化。

考虑在这道题中,可以通过查找表进行代替哪两层循环。当 i , j i,j i,j 两点距离等于 i , k i,k i,k 时,用查找表的思路,等价于:对距离 key (i,j或i,k的距离),其值 value (个数)为2。那么就可以做一个查找表,用来查找相同距离 key 的个数 value 是多少。遍历每一个节点i,扫描得到其他点到节点 i 的距离,在查找表中,对应的键就是距离的值,对应的值就是距离值得个数。在拿到对于元素i的距离查找表后,接下来就是排列选择问题了:当距离为 x x x 的值有 n ( n ≤ 2 ) n(n\leq2) n(n2) 个时,选择 j , k j,k j,k 的可能情况有:第一次选择有 n n n 种,第二次选择有 n − 1 n-1 n1 种。

对于距离值的求算,按照欧式距离的方法进行求算的话,容易产生浮点数,可以将根号去掉,用差的平方和来进行比较距离。

代码:

class Solution:
    def numberOfBoomerangs(self, points: List[List[int]]) -> int:
        ans=0
        from collections import Counter
        for i in points:
            record=Counter()
            for j in points:
                if i!=j:
                    record[self.dis(i,j)]+=1
            for k,v in record.items():
                ans+=v*(v-1)
        return ans
    def dis(self,point1,point2):
        return (point1[0]-point2[0])**2+(point1[1]-point2[1])**2
class Solution:
    def numberOfBoomerangs(self, points: List[List[int]]) -> int:
        from collections import Counter
        def f(x1,y1):
            d=Counter((x2-x1)**2+(y2-y1)**2 for x2,y2 in points)
            return sum(t*(t-1) for t in d.values())
        return sum(f(x1,y1) for x1,y1 in points)

149.直线上最多的点数【困难】

LeetCode传送门
给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。

示例:

  • 输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
  • 输出: 4

思路:
判断点是否在一条直线上,其实就等价于判断 i , j i,j i,j 两点的斜率是否等于 i , k i,k i,k 两点的斜率。
直接考虑使用查找表实现,即查找相同斜率 key 的个数 value 是多少。

  • i 和 j,j 和 i 是两种相同的情况,在遍历每个 i,查找 i 所在直线上包含的最多点数,应返回查找表中的最大的value值。如果某个斜率出现 n 次,返回的应该是 n+1 个点(加上点 i),因为返回的是最大值,i 和 j、j 和 i 不会重复计算。
  • 当有相同元素时,题目的要求是算作两个不同的点,但是在程序运行时,会将其考虑为相同的点,return回了inf。但在实际运行时,需要对相同元素的情况单独考虑(相同的点必定在同一条直线)。
    • 可以设定samepoint值,遍历时判断,如果相同时,samepoint值++,最后取v+samepoint的值作为结果数。
    • 考虑到如果全是相同值,那么这时dict中的record为空,也要将samepoint值当作结果数返回。
  • 边界条件(无法计算斜率):如果点集为空,返回0,如果点集只有一个点,返回1。
  • 计算斜率的方法:
    • 除数:速度快,但不精确。
    • 将除数化为分数最简形式(求最大公约数)。

代码:

class Solution:
    def maxPoints(self, points: List[List[int]]) -> int:
        if len(points)<=1:return len(points)
        ans=0
        from collections import defaultdict
        for i in range(len(points)):
            record=defaultdict(int)
            samepoint=0
            for j in range(len(points)):
                if points[i]==points[j]: # 包括j==i
                    samepoint+=1
                else:
                    record[self.get_slope(points,i,j)]+=1
            for v in record.values():
                ans=max(ans,v+samepoint)
            ans=max(ans,samepoint)
        return ans
    def get_slope(self,points,i,j):
        from decimal import Decimal
        if points[i][0]-points[j][0]==0:
            return float('inf')
        else:
            return Decimal(str((points[i][1]-points[j][1])))/Decimal(str((points[i][0]-points[j][0])))
class Solution:
    def maxPoints(self, points: List[List[int]]) -> int:
        if len(points)<=1:return len(points)
        ans=0
        from collections import defaultdict
        for i in range(len(points)):
            record=defaultdict(int)
            samepoint=0
            for j in range(len(points)):
                if points[i]==points[j]: # 包括j==i
                    samepoint+=1
                else:
                    record[self.get_slope(points,i,j)]+=1
            for v in record.values():
                ans=max(ans,v+samepoint)
            ans=max(ans,samepoint)
        return ans
    def gcd(self,dy,dx):
        if y==0:
            return x
        else:
            return gcd(y,x%y)
    def get_slope(self,points,i,j):
        dy=points[i][1]-points[j][1]
        dx=points[i][0]-points[j][0]
        g=gcd(dy,dx)
        if g!=0:
            dy//=g
            dx//=g
        return '{}/{}'.format(dy,dx)

滑动数组

滑动窗口是数组/字符串问题中常用的抽象概念。 窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即 [𝑖,𝑗) 。滑动窗口是可以将两个边界向某一方向“滑动”的窗口。

3.无重复字符的最长子串【中等】

LeetCode传送门
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例:

  • 输入: “abcabcbb”
  • 输出: 3
  • 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

思路1: 滑动窗口
使用 HashSet 将字符存储在当前窗口 [𝑖,𝑗) (最初 𝑗=𝑖 )中,然后向右侧滑动索引 𝑗 ,如果它不在 HashSet 中,我们会继续滑动 𝑗 ,直到 𝑠[𝑗] 已经存在于 HashSet 中。此时,找到的没有重复字符的最长子字符串将会以索引 𝑖 开头,如果对所有的 𝑖 这样做,就可以得到答案。

代码:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n=len(s)
        HashSet=set([])
        ans,i,j=0,0,0
        while i<n and j<n:
            if s[j] not in HashSet:
                HashSet.add(s[j])
                j+=1
                ans=max(ans,j-i)
            else:
                HashSet.remove(s[i])
                i+=1
        return ans

思路2: 哈希表优化的滑动窗口
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。当找到重复的字符时,可以立即跳过该窗口。也就是说,如果 𝑠[𝑗] 在 [𝑖,𝑗) 范围内有与 𝑗′ 重复的字符,不需要逐渐增加 𝑖 ,可以直接跳过 [𝑖,𝑗′] 范围内的所有元素,并将 𝑖 变为 𝑗′+1 。

代码:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n=len(s)
        ans,i=0,0
        HashMap={}
        for j in range(n):
            if s[j] in HashMap:
                i=max(HashMap.get(s[j],-1),i)#窗口左端变为j'+1(index)
            ans=max(ans,j-i+1)# [j'+1,j]字符长度
            HashMap[s[j]]=j+1# key为字符,value为j'+1
        return ans    

219.存在重复元素2【简单】

LeetCode传送门
给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。

思路:
固定滑动数组的长度为K+1,当这个滑动数组内如果能找到两个元素的值相等,就可以保证两个元素的索引的差是
小于等于k的。如果当前的滑动数组中没有元素相同,就右移滑动数组的右边界r,同时将左边界l右移。查看r++的元
素是否在l右移过后的数组里,如果不在就将其添加数组,在的话返回true表示两元素相等。
因为滑动数组中的元素是不同的,考虑用set作为数据结构。

代码:

class Solution:
    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        record=set()
        for i in range(len(nums)):
            if nums[i] in record:
                return True
            record.add(nums[i])
            if len(record)==k+1:
                record.remove(nums[i-k])
        return False

220.存在重复元素3【中等】

LeetCode传送门
在整数数组 nums 中,是否存在两个下标 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t ,且满足 i 和 j 的差的绝对值也小于等于 ķ 。如果存在则返回 true,不存在返回 false。

思路:

  • 将索引的差值固定,转化为了固定长度K+1的滑动窗口内,是否存在两个值的差距不超过 t,考虑使用滑动窗口的思想来解决。
  • 在遍历的过程中,目的是要在“已经出现、但还未滑出滑动窗口”的所有数中查找,是否有一个数与滑动数组中的数的差的绝对值最大为 t。对于差的绝对值最大为t,实际上等价于所要找的这个元素v的范围是在 v-t 到 v+t 之间,即查找“滑动数组”中的元素有没有范围内的数存在。因为只需证明是否存在即可,这时判断的逻辑是:如果在滑动数组查找比 v-t 大的最小的元素,如果这个元素小于等于 v+t,即可以证明存在 [v-t,v+t]。
  • 如何查找比v-t大的最小的元素:滑动数组作为set,是有序的数组,对于有序的数组,考虑二分查找实现查找比 v-t 大的最小的元素。

代码:

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
        record=set()
        for i in range(len(nums)):
            if len(record)!=0:
                rec=list(record)
                ix=self.lower_bound(rec,nums[i]-t)
                if ix!=-1 and rec[ix]<=nums[i]+t:
                    return True
            record.add(nums[i])
            if len(record)==k+1:
                record.remove(nums[i-k])
        return False
    # 大于target的最小元素
    def lower_bound(self,nums,target):
        low,high=0,len(nums)-1
        while low<high:
            mid=(low+high)//2
            if nums[mid]<target:
                low=mid+1
            else:
                high=mid
        return low if nums[low]>=target else -1

事实上,暴力遍历的运行时间要比上面的小,原因可能是上面的步骤中存在着大量的set和list的转换导致。

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
        if t==0 and len(nums)==len(set(nums)):
            return False
        for i in range(len(nums)):
            for j in range(1,k+1):
                if i+j>=len(nums):break
                if abs(nums[i+j]-nums[i])<=t:return True
        return False

二分查找

二分查找代码模板:

def binarySearch (arr, x): 
	n=len(arr)
	low,high=0,n-1
	while low<high:
		mid=(low+high)//2
		if f(x):
			low=mid+1
		else:
			high=mid
	return low		
  1. low 和 high分别对应搜索的上界和下界,但不一定为0和arr 最后一个元素的下标。
  2. 比较重要的就是这个f(x),在带入模板的情况下,写对函数就完了。
  3. ±1 的位置调整只出现了一次,而且最后返回low还是high都是对的,无需纠结。
  4. 即使区间为空、答案不存在、有重复元素、搜索开/闭区间的上/下界也同样适用

35.搜索插入位置【简单】

LeetCode传送门
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。

思路:
high 要设置为 len(nums) 的原因是target可能大于数组的最大值。

代码:

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        low,high=0,len(nums)
        while low<high:
            mid=(low+high)//2
            if nums[mid]<target:
                low=mid+1
            else:
                high=mid
        return low

540.有序数组中的单一元素【中等】

LeetCode传送门
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
注意:您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

思路:

  • 数组长度为奇数,如果某个元素的下标为奇数,如果它不是只出现一次,它应和它的前一个元素(i-1)相等,如果元素的下标为偶数,如果它不是只出现一次,它应和它的后一个元素(i+1)相等。
  • 异或的巧妙应用:如果mid是偶数,那么和1异或的话,那么得到的是mid+1,如果mid是奇数,得到的是mid-1。

代码:

class Solution:
    def singleNonDuplicate(self, nums: List[int]) -> int:
        low,high=0,len(nums)-1
        while low<high:
            mid=(low+high)//2
            if nums[mid]==nums[mid^1]:
                low=mid+1
            else:
                high=mid
        return nums[low]

410.分割数组的最大值【困难】

LeetCode传送门
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。注意:数组长度 n 满足以下条件:1 ≤ n ≤ 1000,1 ≤ m ≤ min(50, n)

思路:

  • 子数组各自和的最大值是有范围的,在区间 [ max ⁡ ( n u m s ) , ∑ ( n u m s ) ] [\max (nums),\sum(nums)] [max(nums),(nums)] 之中。
  • 使用二分查找在区间 [ max ⁡ ( n u m s ) , ∑ ( n u m s ) ] [\max (nums),\sum(nums)] [max(nums),(nums)] 中寻找数组和的最大值:令 l = max ⁡ ( n u m s ) , h = ∑ ( n u m s ) , m i d = ( l + h ) / / 2 l=\max (nums), h=\sum(nums),mid=(l+h)//2 l=max(nums),h=(nums),mid=(l+h)//2 ,计算数组和最大值不大于 m i d mid mid 对应的子数组个数 c n t cnt cnt
    • 如果 c n t > m cnt>m cnt>m,说明划分的子数组多了,即我们找到的 m i d mid mid 偏小。
    • 否则,说明划分的子数组少了,即 m i d mid mid 偏大(或者正好就是目标值)。

代码:

class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:
        def helper(mid):
            ans=tmp=0
            for num in nums:
                if tmp+num<=mid:
                    tmp+=num
                else:
                    ans+=1
                    tmp=num
            return ans+1
        
        low,high=max(nums),sum(nums)
        while low<high:
            mid=(low+high)//2
            if helper(mid)>m:
                low=mid+1
            else:
                high=mid
        return low

参考

Datawhale社区开源教程之leetcode编程实践
LeetCode题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值