文章目录
- 002. 二进制加法(反转相加 / 位运算)
- 006. 排序数组中两个数字之和(二分查找 / 双指针)
- 007. 数组中和为 0 的三个数(双指针)
- 008. 和大于等于 target 的最短子数组(滑动窗口 / 双指针)
- 009. 乘积小于 K 的子数组(滑动窗口 / 双指针)
- 010. 和为 k 的子数组(前缀和+哈希表)
- 011. 0 和 1 个数相同的子数组(前缀和+哈希表)
- 012. 左右两边子数组的和相等(前缀和)
- 013. 二维子矩阵的和(前缀和)
- 014. 字符串中的变位词(滑动窗口 / 双指针)
- 015. 字符串中的所有变位词(滑动窗口 / 双指针)
- 016. 不含重复字符的最长子字符串(滑动窗口 / 双指针)
002. 二进制加法(反转相加 / 位运算)
给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。
输入为 非空 字符串且只包含数字 1 和 0。
示例 1:
输入: a = "11", b = "10"
输出: "101"
示例 2:
输入: a = "1010", b = "1011"
输出: "10101"
提示:
- 每个字符串仅由字符 ‘0’ 或 ‘1’ 组成。
- 1 < = a . l e n g t h , b . l e n g t h < = 1 0 4 1 <= a.length, b.length <= 10^4 1<=a.length,b.length<=104
- 字符串如果不是 “0” ,就都不含前导零。
方法一:从后到前每位依次加
类似于把多位数字以链表的形式存储后来实现加法(如123+456=579,但是存储数字是以链表的形式存,即 1 → 2 → 3 + 4 → 5 → 6 = 5 → 7 → 9 1\to2\to3 + 4\to5\to6 = 5\to7\to9 1→2→3+4→5→6=5→7→9)的一种思路。
步骤(以 a = “11”, b = “10” 为例):
- 把数字反转 —— a = “11”, b = “01”
- 将反转后的两个数字从前往后 每位 进行加。
之所以需要反转就是因为可能出现最前面进位的情况,就如这个例子,加完后进位需要前面再补个 1。当然这道题以字符串的形式存储数字,自然不反转倒也可以,因为在前面补 1很方便,每位计算也不难。但如果是链表存多位数的话,首先每位计算时都要遍历链表到后面的位,假如两个4位数相加,就需要遍历4+3+2+1次两个链表,这样时间复杂度很高;其次,在进位时也比较复杂。不过这道题的话这么做确实没有必要,可以更简化一些,这里只是提供个思路。
class Solution:
def addBinary(self, a: str, b: str) -> str:
a = a[::-1]
b = b[::-1]
sum = ""
jinwei = 0
min_len = min(len(a), len(b))
for i in range(min_len):
c = int(a[i]) + int(b[i]) + jinwei
if c < 2:
sum += str(c)
jinwei = 0
else:
sum += str(c % 2)
jinwei = 1
if len(a) == len(b):
if jinwei:
sum += str(jinwei)
return sum[::-1]
longer = a if len(a) > len(b) else b
for i in range(min_len, len(longer)):
c = int(longer[i]) + jinwei
if c < 2:
sum += str(c)
jinwei = 0
else:
sum += str(c % 2)
jinwei = 1
if jinwei:
sum += str(jinwei)
return sum[::-1]
上面的代码有些繁杂,改变一下,这样可能更容易理解。不过经过多次提交后这个用时一直都大于上面代码的用时,上面的代码一般是击败80%-90%(32ms,36ms),下面这个只能击败30%左右(40ms,44ms)
class Solution:
def addBinary(self, a: str, b: str) -> str:
i = len(a) - 1
j = len(b) - 1
sum = ""
jinwei = 0
while(i >= 0 or j >= 0):
add_a = int(a[i]) if i >= 0 else 0
add_b = int(b[j]) if j >= 0 else 0
c = add_a + add_b + jinwei
if c < 2:
sum += str(c)
jinwei = 0
else:
sum += str(c % 2)
jinwei = 1
i -= 1
j -= 1
if jinwei:
sum += "1"
return sum[::-1]
方法二:位运算
我们可以设计这样的算法来计算:
- 把 a 和 b 转换成整型数字 x 和 y,在接下来的过程中,x 保存结果,y 保存进位。
- 当进位不为 0 时
计算当前 x 和 y 的无进位相加结果:answer = x ^ y
计算当前 x 和 y 的进位:carry = (x & y) << 1
完成本次循环,更新x = answer,y = carry
- 返回 x 的二进制形式
为什么这个方法是可行的呢?在第一轮计算中,answer 的最后一位是 x 和 y 相加之后的结果,carry 的倒数第二位是 x 和 y 最后一位相加的进位。接着每一轮中,由于 carry 是由 x 和 y 按位与并且左移得到的,那么最后会补零,所以在下面计算的过程中后面的数位不受影响,而每一轮都可以得到一个低 i 位的答案和它向低 i + 1 位的进位,也就模拟了加法的过程。
class Solution:
def addBinary(self, a: str, b: str) -> str:
x, y = int(a, 2), int(b, 2)
while y:
sum = x ^ y
jinwei = (x & y) << 1
x, y = sum, jinwei
return bin(x)[2:]
006. 排序数组中两个数字之和(二分查找 / 双指针)
题目:剑指 Offer II 006. 排序数组中两个数字之和
给定一个已按照 升序排列 的整数数组 numbers
,请你从数组中找出两个数满足相加之和等于目标数 target
。
函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ,所以答案数组应当满足 0 < = a n s w e r [ 0 ] < a n s w e r [ 1 ] < n u m b e r s . l e n g t h 0 <= answer[0] < answer[1] < numbers.length 0<=answer[0]<answer[1]<numbers.length 。
假设数组中存在且只存在一对符合条件的数字,同时一个数字不能使用两次。
示例 1:
输入:numbers = [1,2,4,6,10], target = 8
输出:[1,3]
解释:2 与 6 之和等于目标数 8 。因此 index1 = 1, index2 = 3 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[0,2]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[0,1]
提示:
- 2 <= numbers.length <= 3 * 1 0 4 10^4 104
- -1000 <= numbers[i] <= 1000
- numbers 按 递增顺序 排列
- -1000 <= target <= 1000
- 仅存在一个有效答案
方法一:穷举
具体实现略。
方法二:二分法
二分查找相关讲解:【二分查找】详细图解
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
length = len(numbers)
for i in range(length - 1):
low = i + 1
high = length - 1
while low <= high:
mid = (low + high) // 2
if numbers[mid] == target - numbers[i]:
return [i, mid]
elif numbers[mid] > target - numbers[i]:
high = mid - 1
else:
low = mid + 1
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n 是数组的长度。需要遍历数组一次确定第一个数,时间复杂度是 O ( n ) O(n) O(n),寻找第二个数使用二分查找,时间复杂度是 O ( log n ) O(\log n) O(logn),因此总时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
空间复杂度: O ( 1 ) O(1) O(1)。
方法三:双指针
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
low = 0
high = len(numbers) - 1
while low < high:
sum = numbers[low] + numbers[high]
if sum == target:
return [low, high]
elif sum > target:
high -= 1
else:
low += 1
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。两个指针移动的总次数最多为 n n n 次。
空间复杂度: O ( 1 ) O(1) O(1)。
007. 数组中和为 0 的三个数(双指针)
题目:剑指 Offer II 007. 数组中和为 0 的三个数
给定一个包含 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 < = n u m s . l e n g t h < = 3000 0 <= nums.length <= 3000 0<=nums.length<=3000
- − 1 0 5 < = n u m s [ i ] < = 1 0 5 -10^5 <= nums[i] <= 10^5 −105<=nums[i]<=105
这道题其实是 006. 排序数组中两个数字之和 的进阶版,可以参考上道题的思路来进一步解决这道题。
方法一:穷举
具体实现略。
方法二:固定一个值 + 双指针
可以先看懂上一道题的双指针方法再看这道题。
首先把 nums
按从小到大排列,固定第一个值 i,0 - nums[i]
即为上一题的 target
,然后剩下两个数的查找方法就用上一题双指针的方法即可。此外,这里还要注意去重的问题,因为题目中要求是 不重复 的三元组。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
res = []
nums.sort() # 从小到大排序
length = len(nums)
for i in range(length - 2):
if i > 0 and nums[i] == nums[i-1]: # 去重
continue
low = i + 1
high = length - 1
while low < high:
if nums[i] + nums[low] + nums[high] == 0:
res.append([nums[i], nums[low], nums[high]])
while low < high: # 去重
low += 1
if nums[low] != nums[low - 1]:
break
while low < high: # 去重
high -= 1
if nums[high] != nums[high + 1]:
break
elif nums[i] + nums[low] + nums[high] > 0:
high -= 1
else:
low += 1
return res
在上一道题中还有个二分查找的方法,不过这题用二分法就需要遍历两次,且去重等条件的实现有些复杂,不推荐使用。
008. 和大于等于 target 的最短子数组(滑动窗口 / 双指针)
题目:剑指 Offer II 008. 和大于等于 target 的最短子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [ n u m s l , n u m s l + 1 , . . . , n u m s r − 1 , n u m s r ] [nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r] [numsl,numsl+1,...,numsr−1,numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
- 1 < = t a r g e t < = 1 0 9 1 <= target <= 10^9 1<=target<=109
- 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
- 1 < = n u m s [ i ] < = 1 0 5 1 <= nums[i] <= 10^5 1<=nums[i]<=105
方法一:滑动窗口
最简单的滑动窗口,类似枚举,不过这个超时了。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
for window_size in range(1, len(nums) + 1):
for i in range(len(nums) + 1 - window_size):
sum_window = sum(nums[i: i + window_size])
if sum_window >= target:
return window_size
return 0
方法二:双指针
双指针left,right
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
left = 0
sum_window = 0
min_len = 1e5 + 1
for right, value in enumerate(nums):
sum_window += value
while sum_window >= target:
min_len = min(min_len, right - left + 1)
sum_window -= nums[left]
left += 1
return min_len if min_len <= 1e5 else 0
以 target = 7, nums = [2,3,1,2,4,3]
为例:
[2] 3 1 2 4 3 sum_window < target
[2 3] 1 2 4 3 sum_window < target
[2 3 1] 2 4 3 sum_window < target
[2 3 1 2] 4 3 sum_window >= target, min_len = 4
2 [3 1 2] 4 3 sum_window < target
2 [3 1 2 4] 3 sum_window >= target, min_len = 4
2 3 [1 2 4] 3 sum_window >= target, min_len = 3
2 3 1 [2 4] 3 sum_window < target
2 3 1 [2 4 3] sum_window >= target, min_len = 3
2 3 1 2 [4 3] sum_window >= target, min_len = 2
2 3 1 2 4 [3] sum_window < target
![](https://i-blog.csdnimg.cn/blog_migrate/1784102c779020ad289d2a69c9d1ea6c.png)
009. 乘积小于 K 的子数组(滑动窗口 / 双指针)
题目:剑指 Offer II 009. 乘积小于 K 的子数组
给定一个正整数数组 nums
和整数 k
,请找出该数组内乘积小于 k
的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:
输入: nums = [1,2,3], k = 0
输出: 0
提示:
- 1 < = n u m s . l e n g t h < = 3 ∗ 1 0 4 1 <= nums.length <= 3 * 10^4 1<=nums.length<=3∗104
- 1 < = n u m s [ i ] < = 1000 1 <= nums[i] <= 1000 1<=nums[i]<=1000
- 0 < = k < = 1 0 6 0 <= k <= 10^6 0<=k<=106
方法一:滑动窗口枚举
不出意外,这个又超时了
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
def multiply(num_list): # 累乘
res = 1
for num in num_list:
res *= num
return res
num = 0
for size in range(1, len(nums) + 1):
for i in range(len(nums) + 1 - size):
sum_window = multiply(nums[i: i + size])
if sum_window < k:
num += 1
return num
方法二:双指针
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
length = len(nums)
num = 0
left = 0
sum = 1
for right, value in enumerate(nums):
sum *= value
while sum >= k and left <= right:
sum /= nums[left]
left += 1
num += right - left + 1
return num
以 nums = [10,5,2,6], k = 100
为例:
right = 0
[10] 5 2 6 sum < k num += 0-0+1 = 1 [10]
right = 1
[10 5] 2 6 sum < k num += 1-0+1 = 2 [5] [10 5]
right = 2
[10 5 2] 6 sum = k
10 [5 2] 6 sum < k num += 2-1+1 = 2 [2] [5 2]
right = 3
10 [5 2 6] sum < k num += 3-1+1 = 3 [6] [2 6] [5 2 6]
![](https://i-blog.csdnimg.cn/blog_migrate/97b5648cc2756c450ebc1342c582f62e.png)
010. 和为 k 的子数组(前缀和+哈希表)
给定一个整数数组 nums
和一个整数 k
,请找到该数组中和为 k
的连续子数组的个数。
示例 1:
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
示例 2:
输入:nums = [1,2,3], k = 3
输出: 2
提示:
- 1 < = n u m s . l e n g t h < = 2 ∗ 1 0 4 1 <= nums.length <= 2 * 10^4 1<=nums.length<=2∗104
- − 1000 < = n u m s [ i ] < = 1000 -1000 <= nums[i] <= 1000 −1000<=nums[i]<=1000
- − 1 0 7 < = k < = 1 0 7 -10^7 <= k <= 10^7 −107<=k<=107
方法一:枚举法
这里我使用了 cumsum 计算累加和,Counter 计数。
相关讲解:Python中的numpy.cumsum();python Counter() 函数
方法很简单,不过超时了。
import numpy as np
from collections import Counter
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
num = 0
for left in range(len(nums)):
accumulate = np.cumsum(nums[left:])
counter = Counter(accumulate)
num += counter[k]
return num
方法二:前缀和+哈希表
以 nums = [1, 3, 2, 4, 1, 1, -2], k = 6
为例:
终点 | 前缀和 | 前缀和 - k | 解释 |
---|---|---|---|
(0) 起点 | 0 | ||
(1) num = 1 | 1 | -5 | |
(2) num = 3 | 4 | -2 | |
(3) num = 2 | 6 | 0 | 以 (3) 为终点,从起点 (0) 开始,存在距离目标为 0 的路径为 1 → 2 → 3 1\to2\to3 1→2→3 |
(4) num = 4 | 10 | 4 | 以 (4) 为终点,从起点 (0) 开始,比目标k多出距离 4; 而从起点 (0) 开始到点 (2) 的距离正好为 4,因此解为 3 → 4 3\to4 3→4 |
(5) num = 1 | 11 | 5 | |
(6) num = 1 | 12 | 6 | 以 (6) 为终点,从起点 (0) 开始,比目标 k 多出距离 6; 而从起点 (0) 开始到点 (3) 的距离正好为 6,因此解为 4 → 5 → 6 4\to5\to6 4→5→6 |
(7) num = -2 | 10 | 4 | 以 (4) 为终点,从起点 (0) 开始,比目标 k 多出距离 4; 而从起点 (0) 开始到点 (2) 的距离正好为 4, 因此解为 3 → 4 → 5 → 6 → 7 3\to4\to5\to6\to7 3→4→5→6→7 |
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
num = 0
prefix_sum = 0 # 前缀和
prefix_dic = {0: 1} # 统计各个前缀和的个数
for value in nums:
prefix_sum += value
# 统计
if prefix_sum - k in prefix_dic.keys():
num += prefix_dic[prefix_sum - k]
# 更新字典
if prefix_sum not in prefix_dic.keys():
prefix_dic[prefix_sum] = 1
else:
prefix_dic[prefix_sum] += 1
return num
![](https://i-blog.csdnimg.cn/blog_migrate/70738faeab74e1f9ac83ea0e195b5fa6.png)
011. 0 和 1 个数相同的子数组(前缀和+哈希表)
题目:剑指 Offer II 011. 0 和 1 个数相同的子数组
给定一个二进制数组 nums
, 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:
输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。
提示:
- 1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^5 1<=nums.length<=105
- n u m s [ i ] nums[i] nums[i] 不是 0 就是 1
方法一:前缀和+哈希表
思路与上一题相似。
「0 和 1 的数量相同」等价于「1 的数量减去 0 的数量等于 0」,我们可以将数组中的 0 视作 −1,则原问题转换成「求最长的连续子数组,其元素和为 0」。
即 nums[i] == 0
时,将其改为 nums[i] == -1
;nums[i] == 1
时不变,仍为 nums[i] == 1
。
这样就将「0 和 1 的数量相同」问题转化为「求和为 0 的 最长 子数组的长度」
与上一道题的区别在于这题要得到 最长字数组的长度,因此字典(哈希表)中的 value 不再存储个数,而是变成 这个前缀和第一次出现的位置(即只有第一次出现时更新字典)。至于为什么是 第一次,毫无疑问,越靠前的位置长度越长。
以 nums = [0, 1, 1, 0, 1]
为例,等价于上一题的 nums = [-1, 1, 1, -1, 1], target = 0
:
终点 | 前缀和 | 前缀和 - target (=前缀和) | 解释 |
---|---|---|---|
(-1)起点 | 0 | ||
(0) num = -1 | -1 | -1 | |
(1) num = 1 | 0 | 0 | 以 (1) 为终点,从起点 (-1) 开始, 存在距离目标为 0 的路径为 0 → 1 0\to1 0→1 |
(2) num = 1 | 1 | 1 | |
(3) num = -1 | 0 | 0 | 以 (3) 为终点,从起点 (-1) 开始,存在距离目标为 0 的 路径为 0 → 1 → 2 → 3 0\to1\to2\to3 0→1→2→3 和 2 → 3 2\to3 2→3,显然最长的是 0 → 1 → 2 → 3 0\to1\to2\to3 0→1→2→3,这也是为什么字典中的 value 存储 的是 这个前缀和第一次出现的位置(即只有第一次出现 时才更新字典) |
(4) num = 1 | 1 | 1 | 以 (4) 为终点,从起点 (-1) 开始,比目标 target 多出距离 1; 而从起点 (-1) 开始到点 (2) 的距离正好为 1,因此满足条件的 路径为 3 → 4 3\to4 3→4 |
class Solution:
def findMaxLength(self, nums: List[int]) -> int:
res = 0
for i in range(len(nums)):
if nums[i] == 0:
nums[i] = -1
prefix_sum = 0
prefix_dic = {0:-1} # 前缀和: 首次出现的位置; 0设为-1
for i in range(len(nums)):
prefix_sum += nums[i]
if prefix_sum in prefix_dic.keys():
res = max(res, i - prefix_dic[prefix_sum])
else:
prefix_dic[prefix_sum] = i
return res
![](https://i-blog.csdnimg.cn/blog_migrate/5d509bdd35d9da882e51013e0a1e4940.png)
012. 左右两边子数组的和相等(前缀和)
题目:剑指 Offer II 012. 左右两边子数组的和相等
给你一个整数数组 nums
,请计算数组的 中心下标 。
数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。
如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
示例 1:
输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
示例 2:
输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。
示例 3:
输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。
提示:
- 1 < = n u m s . l e n g t h < = 1 0 4 1 <= nums.length <= 10^4 1<=nums.length<=104
- − 1000 < = n u m s [ i ] < = 1000 -1000 <= nums[i] <= 1000 −1000<=nums[i]<=1000
方法一:枚举法
class Solution:
def pivotIndex(self, nums: List[int]) -> int:
length = len(nums)
if length == 1:
return 0
if sum(nums[1:]) == 0:
return 0
for i in range(1, length-1):
if sum(nums[:i]) == sum(nums[i+1:]):
return i
if sum(nums[:length-1]) == 0:
return length - 1
return -1
![](https://i-blog.csdnimg.cn/blog_migrate/8ecb6e1adadfa4a49d06ba121e96eb3a.png)
方法二:前缀和
记数组的全部元素之和为 total \textit{total} total,当遍历到第 i i i 个元素时,设其左侧元素之和为 sum \textit{sum} sum,则其右侧元素之和为 total − nums i − sum \textit{total}-\textit{nums}_i-\textit{sum} total−numsi−sum。左右侧元素相等即为 sum = total − nums i − sum \textit{sum}=\textit{total}-\textit{nums}_i-\textit{sum} sum=total−numsi−sum,即 2 × sum + nums i = total 2\times\textit{sum}+\textit{nums}_i=\textit{total} 2×sum+numsi=total。
当中心下标左侧或右侧没有元素时,即为零个项相加,这在数学上称作「空和」( empty sum \text{empty sum} empty sum)。在程序设计中我们约定「空和是零」。
class Solution:
def pivotIndex(self, nums: List[int]) -> int:
total = sum(nums)
prefix_sum = 0
for i in range(len(nums)):
if 2 * prefix_sum + nums[i] == total:
return i
prefix_sum += nums[i]
return -1
![](https://i-blog.csdnimg.cn/blog_migrate/8c6ec32da214bcc267f3b52a2bfc1b06.png)
013. 二维子矩阵的和(前缀和)
给定一个二维矩阵 matrix
,以下类型的多个请求:
计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
实现 NumMatrix 类:
NumMatrix(int[][] matrix)
给定整数矩阵 matrix 进行初始化
int sumRegion(int row1, int col1, int row2, int col2)
返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。
示例 1:
输入:
["NumMatrix","sumRegion","sumRegion","sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出:
[null, 8, 11, 12]
解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
提示:
- m = = m a t r i x . l e n g t h m == matrix.length m==matrix.length
- n = = m a t r i x [ i ] . l e n g t h n == matrix[i].length n==matrix[i].length
- 1 < = m , n < = 200 1 <= m, n <= 200 1<=m,n<=200
- − 1 0 5 < = m a t r i x [ i ] [ j ] < = 1 0 5 -10^5 <= matrix[i][j] <= 10^5 −105<=matrix[i][j]<=105
- 0 < = r o w 1 < = r o w 2 < m 0 <= row1 <= row2 < m 0<=row1<=row2<m
- 0 < = c o l 1 < = c o l 2 < n 0 <= col1 <= col2 < n 0<=col1<=col2<n
- 最多调用 1 0 4 次 s u m R e g i o n 方法 最多调用 10^4 次 sumRegion 方法 最多调用104次sumRegion方法
方法一:一维前缀和
初始化时对矩阵的每一行计算前缀和,检索时对二维区域中的每一行计算子数组和,然后对每一行的子数组和计算总和。
具体实现方面,创建 m m m 行 n + 1 n+1 n+1 列的二维数组 sums \textit{sums} sums,其中 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数, sums [ i ] \textit{sums}[i] sums[i] 为 matrix [ i ] \textit{matrix}[i] matrix[i] 的前缀和数组。将 sums \textit{sums} sums 的列数设为 n + 1 n+1 n+1 的目的是为了方便计算每一行的子数组和,不需要对 col 1 = 0 \textit{col}_1=0 col1=0 的情况特殊处理。
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
m = len(matrix)
n = len(matrix[0]) if matrix else 0
self.sums = [[0] * (n+1) for _ in range(m)] # m * (n+1) 的二维矩阵
# 无法用[[0] * (n-1)] * m 替代
# 原因可查看:https://blog.csdn.net/hq_cjj/article/details/86659589
for i in range(m):
for j in range(n):
self.sums[i][j+1] = self.sums[i][j] + matrix[i][j]
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
total = sum([self.sums[i][col2+1] - self.sums[i][col1] for i in range(row1, row2+1)])
return total
# Your NumMatrix object will be instantiated and called as such:
# obj = NumMatrix(matrix)
# param_1 = obj.sumRegion(row1,col1,row2,col2)
-
时间复杂度:初始化 O ( m n ) O(mn) O(mn),每次检索 O ( m ) O(m) O(m),其中 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。
初始化需要遍历矩阵 matrix \textit{matrix} matrix 计算二维前缀和,时间复杂度是 O ( m n ) O(mn) O(mn)。
每次检索需要对二维区域中的每一行计算子数组和,二维区域的行数不超过 m m m,计算每一行的子数组和的时间复杂度是 O ( 1 ) O(1) O(1),因此每次检索的时间复杂度是 O ( m ) O(m) O(m)。 -
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。需要创建一个 m m m 行 n + 1 n+1 n+1 列的前缀和数组 sums \textit{sums} sums。
![](https://i-blog.csdnimg.cn/blog_migrate/9407e2129806b53f2c66dd92284955d4.png)
方法二:二维前缀和
假设 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。定义当 0 ≤ i < m 0 \le i<m 0≤i<m 且 0 ≤ j < n 0 \le j<n 0≤j<n 时, f ( i , j ) f(i,j) f(i,j) 为矩阵 matrix \textit{matrix} matrix 的以 ( i , j ) (i,j) (i,j) 为右下角的子矩阵的元素之和:
f ( i , j ) = ∑ p = 0 i ∑ q = 0 j matrix [ p ] [ q ] f(i,j)=\sum\limits_{p=0}^i \sum\limits_{q=0}^j \textit{matrix}[p][q] f(i,j)=p=0∑iq=0∑jmatrix[p][q]
f
(
i
,
j
)
f(i,j)
f(i,j) 的计算如下:
f
(
i
,
j
)
=
f
(
i
−
1
,
j
)
+
f
(
i
,
j
−
1
)
−
f
(
i
−
1
,
j
−
1
)
+
matrix
[
i
]
[
j
]
f(i,j)=f(i-1,j)+f(i,j-1)-f(i-1,j-1)+\textit{matrix}[i][j]
f(i,j)=f(i−1,j)+f(i,j−1)−f(i−1,j−1)+matrix[i][j]
![](https://i-blog.csdnimg.cn/blog_migrate/edf47fe664032742cafc5f0430628689.png)
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
m = len(matrix)
n = len(matrix[0]) if matrix else 0
self.sums = [[0] * (n+1) for _ in range(m+1)] # (m+1) * (n+1) 的二维矩阵
for i in range(m):
for j in range(n):
self.sums[i+1][j+1] = self.sums[i+1][j] + self.sums[i][j+1] - self.sums[i][j] + matrix[i][j]
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
total = self.sums[row2+1][col2+1] - self.sums[row1][col2+1] - self.sums[row2+1][col1] + self.sums[row1][col1]
return total
-
时间复杂度:初始化 O ( m n ) O(mn) O(mn),每次检索 O ( 1 ) O(1) O(1),其中 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。
初始化需要遍历矩阵 matrix \textit{matrix} matrix 计算二维前缀和,时间复杂度是 O ( m n ) O(mn) O(mn)。
每次检索的时间复杂度是 O ( 1 ) O(1) O(1)。 -
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是矩阵 matrix \textit{matrix} matrix 的行数和列数。需要创建一个 m + 1 m+1 m+1 行 n + 1 n+1 n+1 列的二维前缀和数组 sums \textit{sums} sums。
![](https://i-blog.csdnimg.cn/blog_migrate/6bf05e9a5a054104bb44283627878eb0.png)
014. 字符串中的变位词(滑动窗口 / 双指针)
给定两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的某个变位词。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
示例 1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例 2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
提示:
- 1 < = s 1. l e n g t h , s 2. l e n g t h < = 1 0 4 1 <= s1.length, s2.length <= 10^4 1<=s1.length,s2.length<=104
- s1 和 s2 仅包含小写字母
方法1:滑动窗口
固定窗口大小为 s1
的长度,利用 Counter 统计各个字母的数量,当两个Counter(Counter内部无先后顺序) 相同时,则返回True。
from collections import Counter
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
size = len(s1)
counter1 = Counter(s1)
for i in range(len(s2) - size + 1):
counter2 = Counter(s2[i: i+size])
if counter1 == counter2:
return True
return False
![](https://i-blog.csdnimg.cn/blog_migrate/384ddbc741756083f8ff15c2560c8af2.png)
另一种滑动窗口,不同的是这里统计用的是长为26的列表。
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
size1, size2 = len(s1), len(s2)
if size1 > size2:
return False
list1, list2 = [0] * 26, [0] * 26
for i in range(size1):
list1[ord(s1[i]) - ord('a')] += 1
list2[ord(s2[i]) - ord('a')] += 1
if list1 == list2:
return True
for i in range(size1, size2):
list2[ord(s2[i]) - ord('a')] += 1
list2[ord(s2[i-size1]) - ord('a')] -= 1
if list1 == list2:
return True
return False
![](https://i-blog.csdnimg.cn/blog_migrate/5720688aea0622ba53e818114771f4d4.png)
优化
注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 cnt 1 \textit{cnt}_1 cnt1 和 cnt 2 \textit{cnt}_2 cnt2 数组。(上面代码中的 l i s t 1 , l i s t 2 list1,list2 list1,list2 即是这里的 cnt 1 \textit{cnt}_1 cnt1 和 cnt 2 \textit{cnt}_2 cnt2)
从这个角度出发,我们可以用一个变量 diff \textit{diff} diff 来记录 cnt 1 \textit{cnt}_1 cnt1 与 cnt 2 \textit{cnt}_2 cnt2 的不同值的个数,这样判断 cnt 1 \textit{cnt}_1 cnt1 和 cnt 2 \textit{cnt}_2 cnt2 是否相等就转换成了判断 diff \textit{diff} diff 是否为 0.
每次窗口滑动,记一进一出两个字符为 x x x 和 y y y.
若 x = y x=y x=y 则对 cnt 2 \textit{cnt}_2 cnt2 无影响,可以直接跳过。
若 x ≠ y x\ne y x=y,对于字符 x x x,在修改 cnt 2 \textit{cnt}_2 cnt2 之前若有 cnt 2 [ x ] = cnt 1 [ x ] \textit{cnt}_2[x]=\textit{cnt}_1[x] cnt2[x]=cnt1[x],则将 diff \textit{diff} diff 加一;在修改 cnt 2 \textit{cnt}_2 cnt2 之后若有 cnt 2 [ x ] = cnt 1 [ x ] \textit{cnt}_2[x]=\textit{cnt}_1[x] cnt2[x]=cnt1[x],则将 diff \textit{diff} diff 减一。字符 y y y 同理。
此外,为简化上述逻辑,我们可以只用一个数组 cnt \textit{cnt} cntcnt,其中 cnt [ x ] = cnt 2 [ x ] − cnt 1 [ x ] \textit{cnt}[x]=\textit{cnt}_2[x]-\textit{cnt}_1[x] cnt[x]=cnt2[x]−cnt1[x],将 cnt 1 [ x ] \textit{cnt}_1[x] cnt1[x] 与 cnt 2 [ x ] \textit{cnt}_2[x] cnt2[x] 的比较替换成 cnt [ x ] \textit{cnt}[x] cnt[x] 与 0 的比较。
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
size1, size2 = len(s1), len(s2)
if size1 > size2:
return False
cnt = [0] * 26
for i in range(size1):
cnt[ord(s1[i]) - ord('a')] -= 1
cnt[ord(s2[i]) - ord('a')] += 1
diff = 0
for c in cnt:
if c != 0:
diff += 1
if diff == 0:
return True
for i in range(size1, size2):
x = ord(s2[i]) - ord('a')
y = ord(s2[i-size1]) - ord('a')
if x == y:
continue
# 右边
if cnt[x] == 0:
diff += 1
cnt[x] += 1
if cnt[x] == 0:
diff -= 1
# 左边
if cnt[y] == 0:
diff += 1
cnt[y] -= 1
if cnt[y] == 0:
diff -= 1
if diff == 0:
return True
return False
![](https://i-blog.csdnimg.cn/blog_migrate/8c6ea398a5a96140fb56d7997769b3f6.png)
方法2:双指针
回顾方法一的思路,我们在保证区间长度为 n n n 的情况下,去考察是否存在一个区间使得 cnt \textit{cnt} cnt 的值全为 0。
反过来,还可以在保证 cnt \textit{cnt} cnt 的值不为正的情况下,去考察是否存在一个区间,其长度恰好为 n n n。
初始时,仅统计 s 1 s_1 s1 中的字符,则 cnt \textit{cnt} cnt 的值均不为正,且元素值之和为 − n -n −n。
然后用两个指针 left \textit{left} left 和 right \textit{right} right 表示考察的区间 [ left , right ] [\textit{left},\textit{right}] [left,right]。 right \textit{right} right 每向右移动一次,就统计一次进入区间的字符 x x x。为保证 cnt \textit{cnt} cnt 的值不为正,若此时 cnt [ x ] > 0 \textit{cnt}[x]>0 cnt[x]>0,则向右移动左指针,减少离开区间的字符的 cnt \textit{cnt} cnt 值直到 cnt [ x ] ≤ 0 \textit{cnt}[x] \le 0 cnt[x]≤0。
注意到 [ left , right ] [\textit{left},\textit{right}] [left,right] 的长度每增加 1, cnt \textit{cnt} cnt 的元素值之和就增加 1。当 [ left , right ] [\textit{left},\textit{right}] [left,right] 的长度恰好为 n n n 时,就意味着 cnt \textit{cnt} cnt 的元素值之和为 0。由于 cnt \textit{cnt} cnt 的值不为正,元素值之和为 0 就意味着所有元素均为 0,这样我们就找到了一个目标子串。
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
size1, size2 = len(s1), len(s2)
if size1 > size2:
return False
cnt = [0] * 26
for i in range(size1):
cnt[ord(s1[i]) - ord('a')] -= 1
left = 0
for right in range(size2):
x = ord(s2[right]) - ord('a')
cnt[x] += 1
while cnt[x] > 0:
cnt[ord(s2[left]) - ord('a')] -= 1
left += 1
if right - left + 1 == size1:
return True
return False
![](https://i-blog.csdnimg.cn/blog_migrate/de97d912ec59a85dfcc14b3761fec50d.png)
015. 字符串中的所有变位词(滑动窗口 / 双指针)
题目:剑指 Offer II 015. 字符串中的所有变位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 变位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
变位词 指字母相同,但排列不同的字符串。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的变位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的变位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的变位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的变位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的变位词。
提示:
- 1 < = s . l e n g t h , p . l e n g t h < = 3 ∗ 1 0 4 1 <= s.length, p.length <= 3 * 10^4 1<=s.length,p.length<=3∗104
- s s s 和 p p p 仅包含小写字母
方法1:滑动窗口
与上一题思路一样
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
size_s, size_p = len(s), len(p)
if size_s < size_p:
return []
cnt1, cnt2 = [0] * 26, [0] * 26
for i in range(size_p):
cnt1[ord(p[i]) - ord('a')] += 1
cnt2[ord(s[i]) - ord('a')] += 1
res = []
if cnt1 == cnt2:
res.append(0)
for i in range(size_p, size_s):
cnt2[ord(s[i]) - ord('a')] += 1
cnt2[ord(s[i-size_p]) - ord('a')] -= 1
if cnt1 == cnt2:
res.append(i-size_p+1)
return res
![](https://i-blog.csdnimg.cn/blog_migrate/2e62b0558636f9f89ea7ffff3750ee99.png)
方法2:双指针
与上一题思路相同
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
size_s, size_p = len(s), len(p)
if size_s < size_p:
return []
cnt = [0] * 26
for i in range(size_p):
cnt[ord(p[i]) - ord('a')] -= 1
res = []
left = 0
for right in range(size_s):
x = ord(s[right]) - ord('a')
cnt[x] += 1
while cnt[x] > 0:
cnt[ord(s[left]) - ord('a')] -= 1
left += 1
if right - left + 1 == size_p:
res.append(left)
return res
![](https://i-blog.csdnimg.cn/blog_migrate/a75d8e2a4cb765da376e3b087134ad40.png)
016. 不含重复字符的最长子字符串(滑动窗口 / 双指针)
题目:剑指 Offer II 016. 不含重复字符的最长子字符串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长连续子字符串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子字符串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子字符串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:
输入: s = ""
输出: 0
提示:
- 0 < = s . l e n g t h < = 5 ∗ 1 0 4 0 <= s.length <= 5 * 10^4 0<=s.length<=5∗104
- s s s 由英文字母、数字、符号和空格组成
方法1:双指针/滑动窗口
找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:
以
(a)bcabcbb
\texttt{(a)bcabcbb}
(a)bcabcbb 开始的最长字符串为
(abc)abcbb
\texttt{(abc)abcbb}
(abc)abcbb;
以
a(b)cabcbb
\texttt{a(b)cabcbb}
a(b)cabcbb 开始的最长字符串为
a(bca)bcbb
\texttt{a(bca)bcbb}
a(bca)bcbb;
以
ab(c)abcbb
\texttt{ab(c)abcbb}
ab(c)abcbb 开始的最长字符串为
ab(cab)cbb
\texttt{ab(cab)cbb}
ab(cab)cbb;
以
abc(a)bcbb
\texttt{abc(a)bcbb}
abc(a)bcbb 开始的最长字符串为
abc(abc)bb
\texttt{abc(abc)bb}
abc(abc)bb;
以
abca(b)cbb
\texttt{abca(b)cbb}
abca(b)cbb 开始的最长字符串为
abca(bc)bb
\texttt{abca(bc)bb}
abca(bc)bb;
以
abcab(c)bb
\texttt{abcab(c)bb}
abcab(c)bb 开始的最长字符串为
abcab(cb)b
\texttt{abcab(cb)b}
abcab(cb)b;
以
abcabc(b)b
\texttt{abcabc(b)b}
abcabc(b)b 开始的最长字符串为
abcabc(b)b
\texttt{abcabc(b)b}
abcabc(b)b;
以
abcabcb(b)
\texttt{abcabcb(b)}
abcabcb(b) 开始的最长字符串为
abcabcb(b)
\texttt{abcabcb(b)}
abcabcb(b)。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
length = len(s)
if length == 0:
return 0
min_len = 1
left, right = 0, 0
occ = set(s[0])
while left < length - min_len: # 左指针到length-min_len-1就够了,再往后min_len也不会超过先前的了,不用到length
if left != 0:
occ.remove(s[left - 1]) # 左指针向右移动一格
while right + 1 < length and s[right + 1] not in occ:
right += 1
occ.add(s[right])
if right - left + 1 > min_len:
min_len = right - left + 1
left += 1
return min_len