搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
解题思路:二分查找
定义左侧下标left,右侧下标right,计算中间下标mid
每次根据nums[mid]和target之间的大小进行判断,相等则直接返回下标,小于则left右移,大于则right左移
查找结束如果没有相等则返回left作为插入位置
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums) - 1
while left <= right :
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
二分查找模板
class Solution:
def firstBadVersion(self, arr):
# 第一点
lo, hi = 0, len(arr)-1
while lo < hi:
# 第二点
mid = (lo+hi) // 2
# 第三点
if f(x):
lo = mid + 1
else:
hi = mid
return lo
快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
解题思路:没啥思路呢,感觉比较需要数学思维。
可能需要一个循环求解,那什么时候循环结束呢,想到三种情况:
1.过程转换后一个数变为1,这个数是快乐数,返回True
2.过程转换后又出现相同的数,说明进入了死循环,返回False;至于检查是否有相同的数可以想到使用集合set()
3.一直运行,直到无穷大;这一步比较麻烦,好像没什么方法可以知道这个数最终是快乐数,还是会一直计算下去,查阅题解后发现不用考虑这种情况,理由如下:
我们思考:每一位数的最大数字的下一个数字是多少?比如说,9的下一个数为81(9的平方),99的下一个数位162(9的平方+9的平方),999的下一个数为243(9的平方+9的平方+9的平方),也就是说,对于一个三位数的数字,它不可能大于243,就算是最坏的情况,也是在小于243的所有数字上循环,最终重复到一个数字本身,并不会无限期进行下去,因此可以排除第三种情况。
下面思考转换过程,题目要求将每一位数字平方后相加,也就是数位分离后求平方和,这里可以直接使用divmod(a,b),返回的两个数值分别为a//b,a%b;然后再使用一个哈希集合判断一个数字是否出现过;最后过程转换结束时判断结果是否等于1。
class Solution:
def isHappy(self, n: int) -> bool:
#获取下一个数字
def get_next(n):
next_num = 0
while n > 0:
n, mod = divmod(n, 10) # 数位分离
next_num += mod ** 2
return next_num
#定义一个哈希集
seen = set()
while n != 1 and n not in seen:
seen.add(n)
n = get_next(n)
return n == 1
同构字符串
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。
示例 1:
输入: s = “egg”, t = “add”
输出: true
示例
输入: s = “foo”, t = “bar”
输出: false
示例 3:
输入: s = “paper”, t = “title”
输出: true
解题思路:需要保证两个字符串中,字符重复出现的次数相同,可以使用索引,判断两者索引是否相同
s.index(str)方法,检测字符串s中是否包含子字符串str,返回第一次出现的位置,我们将其作为索引
map()方法,根据函数对指定序列做映射,可以将字符映射为索引,map是通过hash存储的,不能直接进行比较,需要转换为list比较
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
return list(map(s.index, s)) == list(map(t.index,t))
单词规律
给定一种规律 pattern 和一个字符串 str ,判断 str 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 str 中的每个非空单词之间存在着双向连接的对应规律。
示例1:
输入: pattern = “abba”, str = “dog cat cat dog”
输出: true
示例 2:
输入:pattern = “abba”, str = “dog cat cat fish”
输出: false
示例 3:
输入: pattern = “aaaa”, str = “dog cat cat dog”
输出: false
示例 4:
输入: pattern = “abba”, str = “dog dog dog dog”
输出: false
说明:
你可以假设 pattern 只包含小写字母, str 包含了由单个空格分隔的小写字母。
解题思路:和上一题一模一样。需要将单词字符串拆分为字符列表。
class Solution:
def wordPattern(self, pattern: str, str: str) -> bool:
str = str.split()
return list(map(pattern.index, pattern)) == list(map(str.index,str))
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:
输入: s = “rat”, t = “car”
输出: false
说明:
你可以假设字符串只包含小写字母。
进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
解题思路:如果只包含26个小写字符,可以开一个数组保存[a-z],每次遍历s和t,遍历s时数组对应字符的位置+1,表示该字符出现了一次;遍历t时数组对应字符的位置-1,最后判断结果数组是否都为0。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
res = [0] * 26
for i in range(len(s)):
res[ord(s[i]) - 97] += 1
res[ord(t[i]) - 97] -= 1
for i in range(26):
if res[i] != 0:
return False
return True
两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
说明:
输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。
解题思路:两个set()搞定,如果在两个set()都出现,说明是交集,如果没保存过则保存
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
res = []
for i in set(nums1):
if i in set(nums2) and i not in res:
res.append(i)
return res
两个数组的交集||
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]
说明:
输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
我们可以不考虑输出结果的顺序。
进阶:
如果给定的数组已经排好序呢?你将如何优化你的算法?
如果 nums1 的大小比 nums2 小很多,哪种方法更优?
如果 nums2 的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
解题思路:和上一题不一样的是,上一题的答案唯一,而这道题有多个交集。
方法一:哈希表
同一个数字在两个数组中都可能出现多次,可以用哈希表存储每个数字出现的次数。对于一个数字,其在交集中出现的次数等于该数字在两个数组中出现次数的最小值。
首先遍历第一个数组,并在哈希表中记录第一个数组中的每个数字以及对应出现的次数,然后遍历第二个数组,对于第二个数组中的每个数字,如果在哈希表中存在这个数字,则将该数字添加到答案,并减少哈希表中该数字出现的次数。这里第一个数组应为较短的数组。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
#将较短字符遍历后将数字和频率存入哈希表
if len(nums1) > len(nums2):
return self.intersect(nums2, nums1)
m = collections.Counter() # 计数器
for num in nums1:
m[num] += 1
intersection = []
for num in nums2:
if (count := m.get(num, 0)) > 0: #count要么为key是num的value,要么为0
intersection.append(num)
m[num] -= 1
if m[num] == 0:
m.pop(num)
return intersection
这里使用了计数器collections.Counter(),以及海象赋值法:=,在循环过程进行赋值。
复杂度分析
时间复杂度:O(m+n),其中 m 和 n 分别是两个数组的长度。需要遍历两个数组并对哈希表进行操作,哈希表操作的时间复杂度是 O(1),因此总时间复杂度与两个数组的长度和呈线性关系。
空间复杂度:O(min(m,n)),其中 m和 n 分别是两个数组的长度。对较短的数组进行哈希表的操作,哈希表的大小不会超过较短的数组的长度。为返回值创建一个数组 intersection,其长度为较短的数组的长度。
方法二:排序,如果两个数组是有序的,则可以便捷地计算两个数组的交集。
首先对两个数组进行排序,然后使用两个指针遍历两个数组。
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,将该数字添加到答案,并将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort()
nums2.sort()
res = []
i, j = 0, 0
while i < len(nums1) and j < len(nums2):
if nums1[i] == nums2[j]:
res.append(nums1[i])
i += 1
j += 1
elif nums1[i] < nums2[j]:
i += 1
elif nums1[i] > nums2[j]:
j += 1
return res
复杂度分析
时间复杂度:O(m log m+n log n),其中 m 和 n 分别是两个数组的长度。对两个数组进行排序的时间复杂度是 O(m log m+n log n),遍历两个数组的时间复杂度是 O(m+n),因此总时间复杂度是 O(m log m+n log n)。
空间复杂度:O(min(m,n)),其中 m 和 n 分别是两个数组的长度。为返回值创建一个数组 intersection,其长度为较短的数组的长度。不过在 C++ 中,我们可以直接创建一个 vector,不需要把答案临时存放在一个额外的数组中,所以这种实现的空间复杂度为 O(1)。
结语
如果nums 2的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中。那么就无法高效地对nums 2进行排序,因此推荐使用方法一而不是方法二。在方法一中,nums2 只关系到查询操作,因此每次读取nums 2中的一部分数据,并进行处理即可。
分割数组的最大值
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)
示例:
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
解题思路:贪心+二分查找
根据题意,最大值一定在区间 [max(nums),sum(nums)] 之中。
7,2,5,10,8中max=10,sum=32
在…中找到最大/最小值可以使用二分,需要问题具有二段性
在范围10 11 12 …17 18…32中,可以通过二分查找找到答案18,因此上界为数组的sum,下界为数组的max
二分查找时,通过mid是否满足条件判断是在左区间还是右区间,这就需要一个额外的判断函数
这里使用了贪心的思想,模拟分割的过程,从前往后遍历,如果当前分割子数组的和sum加上当前值超过了用于判断的mid,则把当前值作为开头,新开一个分割子数组,并且将子数组数cnt+1,结束后验证cnt是否不超过n
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
def check(x):
#total表示当前分割子数组之和,cnt表示已经分割出的子数组数量
total, cnt = 0, 1
for num in nums:
#当total加上当前值超过x,把当前值作为新的分割子数组的开头,再一次分段,目的是要保证当前子数组的和不能超过假设的值x
if total + num > x:
cnt += 1
total = num
else:
total += num
#最后验证cnt是否不超过m
#如果cnt的值比m大,就表示分组的太多,也就是代表当初假设的太小了,需要在mid与数组元素和之间继续二分。
#如果cnt的值比m小,就表示结果值不是最优的,因为还可以继续多分些组,找到那个最小值。因此需要在数组元素的最大值与mid之间继续二分。
return cnt <= m
#二分查找 上界为所有元素之和,下界为所有元素最大值,因为结果值一定出现在数组中的最大值与数组元素和之间。
left = max(nums)
right = sum(nums)
while left < right:
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid + 1
return left
根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
示例 1:
输入:
“tree”
输出:
“eert”
解释:
'e’出现两次,'r’和’t’都只出现一次。
因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。
示例 2:
输入:
“cccaaa”
输出:
“cccaaa”
解释:
'c’和’a’都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
示例 3:
输入:
“Aabb”
输出:
“bbAa”
解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意’A’和’a’被认为是两种不同的字符。
解题思路:collections.Counter()计数器
class Solution:
def frequencySort(self, s: str) -> str:
return ''.join(i * j for i,j in collections.Counter(s).most_common())
collections.Counter(s).most_common()返回一个字典,key为字符,value为出现次数,从大到小排列
i为字符,j为该字符出现的次数,使用join()方法将字符拼接为字符串
Counter(计数器):用于追踪值的出现次数
Counter类继承dict类,所以它能使用dict类里面的方法
创建一个Counter类
import collections
obj = collections.Counter('aabbccc')
print(obj)
#输出:Counter({'c': 3, 'a': 2, 'b': 2})
elements()获取元素
import collections
obj = collections.Counter('aabbccc')
print(sorted(obj.elements()))
#输出:['a', 'a', 'b', 'b', 'c', 'c', 'c']
most_common(指定一个参数n,按出现次数从大到小列出前n个元素,不指定参数,则列出所有)
import collections
obj = collections.Counter('aabbbcccc')
print(obj.most_common(2))
#输出:[('c', 4), ('b', 3)]
有序数组中的单一元素
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: [3,3,7,7,10,11,11]
输出: 10
解题思路:二分查找
由于每个元素会出现两次,只有一个数字出现一次,那么该单次数字之前要么没数字,要么是2n个数字,由于索引从0开始,所以单次数字的索引要么为0要么为2n,因此我们只需检索偶数索引即可
检查mid时,我们判断mid与其之后的数字是否相同
如果相同,由于mid是偶数索引,前面有偶数个数字(索引从0开始),所以mid之前不可能有单次数字;所以我们跳过mid和mid之后那个数字,将mid+2置为下界,left=mid+2
如果不同,说明单次数字可能是mid或者在mid之前的数字,我们将mid置为上界,rigth=mid
最后只能检测一个数字,即left=right时,该数即为单次数字
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
left = 0
right = len(nums) - 1
while left < right:
mid = (left + right) // 2
#只检查偶数索引
if mid % 2 == 1:
mid -= 1
if nums[mid] == nums[mid+1]:
left = mid + 2
else:
right = mid
return nums[left]