1. 视频题目
1.1 两数之和
1.1.1 描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
提示:
2 <= nums.length <= 104 -109 <= nums[i] <= 109 -109 <= target <= 109 只会存在一个有效答案
进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?
1.1.2 代码
直接使用暴力方法,枚举每两个数字的匹配。
采用双重循环,外循环枚举每一个数字,内循环枚举该数字的所有匹配。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range( 0,len(nums) ):
for j in range( i+1 , len(nums) ):
if target - nums[i] == nums[j] :
return [i,j]
return -1
在暴力方法的基础上,我们可以尝试着去做一些改进。
外循环枚举每一个数字暂时是不能改变的,但是内循环寻找匹配数字的方法可以进行升级
使用枚举的方法来寻找太过复杂,我们可以换为哈希表查找
一般而言,我们的直接思路是对数组的全体数字进行打表
在python当中可以使用dict,key为数字,value为下标
然后外循环枚举数组每一个数字x,内循环改为从哈希表当中查找target-x
这个思路要注意target - x = x的情况,即数组中同一个元素在答案中重复出现
此时相对于双重循环,是改进成为了两个循环:
第一个循环是打表,枚举数组每一个数字,进行建表
第二个循环是查找,同样枚举数组,对哈希表进行查找
那这两个循环是不是可以合并成为一个呢?或许可以
先稍微岔开话题,该题求的是两数之和,也就是拿着x去找target-x
在上面的讨论之中,我们其实是默认数组当中靠前出现的数字是x
无论是枚举还是哈希查找,都是在该数字的后面对target-x进行查找
那我们是不是可以换过来,假设数组当中靠后出现的数字是x
也就是说,我们现在先略过x,但是需要记下x,因为后面还要查找
然后主要根据在x之后出现的target-x来查找x,以求得两数之和
那么,我们略过数字x的时候,其实就是对数组进行遍历
而我们记下数字x的时候,其实就是在进行打表
好了,现在让我们整理一下思路,推广到一般情形
首先是外层循环,遍历数字,枚举每一个数字x
在每一次的循环当中,对于每一个数字x
我们需要看一看target-x是否在哈希表当中
如果在,那就直接返回x的下标和target-x的value
如果不在,那就记下x,即将数字和下表存入哈希表
接着就进入下一次循环,往后继续遍历数字
代码如下:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashtable = dict()
for i, num in enumerate(nums):
if target - num in hashtable:
return [hashtable[target - num], i]
hashtable[nums[i]] = i
return []
1.1.3 总结
优化解题办法的时候,一定要分析基础解法的弊端,以此为改进的出发点
1.2 最接近的三数之和
1.2.1 描述
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。
返回这三个数的和。
假定每组输入只存在恰好一个解。
示例 1:
输入:nums = [-1,2,1,-4], target = 1 输出:2 解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
示例 2:
输入:nums = [0,0,0], target = 1 输出:0
提示:
3 <= nums.length <= 1000 -1000 <= nums[i] <= 1000 -104 <= target <= 104
1.2.2 代码
题目要求与目标值最接近的三数之和,也就是需要找三数之和与目标值差值的最小值
也就是说,我们需要初始化一个较大的三数之和,然后尝试不同数字之间的搭配
一旦发现某三个数字之和与目标值有更小的残差,就用这个数字更新三数之和
最直接的想法当然是三重循环暴力枚举所有可能性的匹配,求得残差最小的搭配
但是也很显然,这种思路的时间复杂度为 ,用python似乎不能通过
所以我们此时就要考虑进阶了,来看看哪些地方是可以被优化的呢?
还是在三重循环的基础上进行思考,我们将目光放在最底层的循环
此时前两个数字已经被确定了,该层循环枚举的是第三个数字
假设其枚举的第一个数字组成的结果,比目标值要小
那么接下来枚举的数字需要比第一个数字更大,这样才能接近目标值
也就是说,如果接下来枚举的数字比第一个数字更小,那就是无效的操作
那该如何保证接下来枚举的数字是有效的呢?或许可以考虑排序
也就是说,使得枚举的顺序与数字大小的顺序相关联,我们假设是升序排列
那如果我们在之前的基础上,将三重循环直接改为在排序后的数组上进行,似乎不太好
如果三重循环都是在左边开始,那就很有可能造成重复组合的情况
假设固定第一个数字,那么当第二个数字右移一位,其恰好就是上一次的第三个数字
所以我们设第一重循环和第二重循环都是在数组左边,而第三重循环则改在右边
也就是说,我们一开始枚举的是最大值,越往后越小,直到终止或者等于目标值
因为升序排列,所以这个时候的第三层循环,只能保证枚举的数字一个比一个小
最接近三数之和的数字只会有两种可能,比目标值大或者比目标值小
而现在数组已经是有序的,而且每次枚举都比上一次小
所以当枚举的数字之和比目标值小时,我们就可以终止此次枚举
在数组有序的情况下,一旦这个枚举比目标值小,其就是所有比目标值小的枚举中最可能的那一个
需要注意,如果初次枚举的值小于目标值,那第三次循环只会越来越小,此次循环是无效的
也就是说,如果当前的和小于目标值,可以跳过此次的第三层循环,直接进行下一次的第二次循环
现在,我们是固定了前两个数字,第三个数字由大到小寻找
而且如果首次枚举之和小于目标值,还可以直接跳过此次的第三层循环
在这样的情况下,接下来是不是还可以继续优化呢?
或许可以尝试将目光移到第二层循环上面,该循环每次枚举的值都比前一次要大
如果某一次第二层循环首次枚举的值比目标值要大,那下一次循环的首次枚举会更大
因为第二层循环的首次枚举,是与固定的第一层循环的值,以及第三层循环的固定起始值(数组末尾),的和
所以如果某一次第二层循环首次枚举的值比目标值要大,那之后第二层循环的首次枚举都是无效的,残差更大
所以说,不同的第二层循环,可能在第三层循环需要不同的起始值
或许现在我们稍稍整理一下:
如果首次枚举的值比目标值要小,可以跳过第三层循环,直接进入下一个第二层循环,也就是第二层循环右移
如果首次枚举的值比目标值要大,第三层循环需要左移使得三数之和变小
一旦三数之和小于目标值,其就是所有比目标值小的枚举中最可能的那一个,不必向下枚举
所以此时需要提前进入第二层循环,使得枚举之和变大
而此时的第三重循环不用恢复到最右边,因为其一定会导致首次枚举的值远大于目标值
也就是说,此时第三层循环枚举的值,恰好是下一次第二层循环开始搜索的起始点
因为此时第三层循环右边的值,可以使得当前第二层循环的值大于目标值
所以其也可以使得下一次第二层循环的值远大于目标值
这也就是双指针思路的解法,共设左右两个指针,升序排列
数值小,左指针右移,增大三数之和;数值大,右指针左移,减小三数之和
因为最接近三数之和的数字只会有两种可能,比目标值大或者比目标值小
而在升序排列的情况下,我们的调整条件也就是比目标值大或者比目标值小
所以,总的来说,本题的思路就是先升序排列数组
然后枚举每一个数值(截止倒数第三个),使用双指针搜索余下的数组
然后一旦发现残差更小的组合,就更新三数之和
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
nums.sort()
res = 10**7
for i in range(len(nums)-2):
left = i + 1
right = len(nums) - 1
while left < right :
sum = nums[i] + nums[left] + nums[right]
if sum == target:
return target
if abs(sum-target) < abs(res-target):
res = sum
if sum > target:
right -= 1
elif sum < target:
left += 1
return res
1.2.3 总结
看题解,还有一些小优化,考虑的是如果直接等于目标值,可以直接跳出
而还有一个优化是:
当我们枚举中任意元素并移动指针时,可以直接将其移动到下一个与这次枚举到的不相同的元素,减少枚举的次数。
以及有一个以上代码的细节是,最后一个,其实是互斥的,所以后面的可以改为来节省时间
但是一定要注意互斥条件的判断,有时候下意识地使用是错误的
看视频教程的时候,视频里面好像是直接用C#暴力三重循环,然后过了
但是我尝试使用python进行暴力的三重循环就超时了
2. 作业题目
2.1 删除有序数组中的重复项
2.1.1 描述
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 int len = removeDuplicates(nums); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
示例 1:
输入:nums = [1,1,2] 输出:2, nums = [1,2] 解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4] 输出:5, nums = [0,1,2,3,4] 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 3 * 104 -104 <= nums[i] <= 104 nums 已按升序排列
2.1.2 代码
我的解法其实是相当于调用API,直接丢出重复的元素
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
cur = 0
while cur < len(nums)-1:
if nums[cur] == nums[cur+1]:
nums.pop(cur)
else:
cur += 1
return len(nums)
正经的解法应该是双指针,不过我们先从暴力解法开始
假设不做限制,那么最直观的方法应该是新建另一个数组
然后按照顺序扫描旧数组,并与新数组末尾元素做对比
如果数值相同,继续扫描旧数组的下一个元素
如果数值不同,则将元素加入新数组,再继续扫描旧数组
如果我们直接在原数组上进行操作,那就是双指针
快指针扫描整个旧数组,慢指针记录新数组的末尾空位置,也就是可以被覆盖的地方
两个指针的起始位置都是index=1的地方,因为index=0就是新数组的第一个元素,不需要操作
而且,fast指针会比较其指向的元素与前一个元素是否相同,所以需要从第二个元素开始
如果该元素与前一个元素相同,fast指针后移,slow指针位置不变
意即该位置的元素与前面的元素重复,也就是说这个位置可以被覆写
如果该元素与前一个元素不同,就把fast指向的元素覆写到slow指针位置,然后两者都后移
slow指针后移的位置,都是被fast扫描过的,也就是说都是可以被覆写的位置
其元素要么与前面的元素重复;要么是已经覆写到了前面,还是与前面的元素重复
以上思路来自力扣的B站官方视频题解1
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
fast = slow = 1
while fast < n:
if nums[fast] != nums[fast - 1]:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
2.1.3 总结
要注意极端情况,例如原数组为空或者原数组只有一个元素
2.2 移除元素
2.2.1 描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 int len = removeElement(nums, val); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
示例 1:
输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3] 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100 0 <= nums[i] <= 50 0 <= val <= 100
2.2.2 代码
属于是上一道题的进阶,去除指定元素
我的代码还是使用了API
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
cur = 0
while cur < len(nums):
if nums[cur] == val:
nums.pop(cur)
else:
cur+=1
return len(nums)
正规思路应该还是双指针,于上一题十分相似
演化思路不再赘述,我们现在设slow记录新数组末尾空位置,fast扫描
那么对于fast在旧数组当中扫描到的每一个数值,
如果等于val则略过,fast继续后移扫描
如果不为val则放入新数组,即slow的位置,然后两指针皆后移
两指针的起始位置皆为旧数组的第一个位置
如果等于val,fast后移扫描,slow不动表明可以被覆写
如果不等于val,两指针皆后移,对下一个元素进行校验
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
if not nums :
return 0
slow = fast = 0
while fast < len(nums):
if nums[fast] == val:
fast += 1
else:
nums[slow] = nums[fast]
fast += 1
slow += 1
return slow
2.2.3 总结
直接上结论就是,slow标记可以被覆写的位置,fast进行扫描
slow的位置刚好是新数组的后一位,在数值上等于新数组长度
对于旧数组为空的情况,可以直接在开头用if判断
对于旧数组只有一个元素,则使用循环条件进行判别处理
2.3 三数之和
2.3.1 描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = [] 输出:[]
示例 3:
输入:nums = [0] 输出:[]
提示:
0 <= nums.length <= 3000 -105 <= nums[i] <= 105
2.3.2 代码
感觉有点像三数之和的排序及双指针搜索加上两数之和的哈希表去重
双指针的推演思路不再赘述,总之先对原数组进行升序排列
然后求和判断,大了右针左移,小了左针右移,两针相遇即为终止
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums = sorted(nums)
if len(nums) < 2:
return []
res = []
for i in range(len(nums)-2):
start = i + 1
end = len(nums) -1
while start < end:
sum = nums[i] + nums[start] + nums[end]
if sum == 0:
if [nums[i], nums[start], nums[end]] not in res:
res.append([nums[i], nums[start], nums[end]])
start += 1
elif sum < 0:
start += 1
else:
end -= 1
return res
还有一个进阶版,直接跳过重复的元素,避免对结果进行去重,减少用时
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
nums.sort()
ans = list()
# 枚举 a
for first in range(n):
# 需要和上一次枚举的数不相同
if first > 0 and nums[first] == nums[first - 1]:
continue
# c 对应的指针初始指向数组的最右端
third = n - 1
target = -nums[first]
# 枚举 b
for second in range(first + 1, n):
# 需要和上一次枚举的数不相同
if second > first + 1 and nums[second] == nums[second - 1]:
continue
# 需要保证 b 的指针在 c 的指针的左侧
while second < third and nums[second] + nums[third] > target:
third -= 1
# 如果指针重合,随着 b 后续的增加
# 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if second == third:
break
if nums[second] + nums[third] == target:
ans.append([nums[first], nums[second], nums[third]])
return ans
2.3.3 总结
需要注意的是,这种搜索类型的双指针,一般终止条件是两针相遇
然后就是,去重的操作要放在加入新元组时,判别新元组是否已经存在
或者就像题解的进阶版,直接跳过对重复的元素的组合
我之前尝试过直接新建一个哈希表,然后发现一个三元组就加入,然后超时了
或者是直接收集全部的元组,放在最后一起去重,也超时了