双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞指针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。
在数组的区间问题上,暴力算法的时间复杂度往往是 O ( n 2 ) O(n^2) O(n2)。而双指针利用了区间「单调性」的性质,可以将时间复杂度降到 O ( n ) O(n) O(n)。
对撞指针:指的是两个指针 l e f t left left、 r i g h t right right 分别指向序列第一个元素和最后一个元素,然后 l e f t left left 指针不断递增, r i g h t right right 不断递减,直到两个指针的值相撞(即 l e f t = = r i g h t left == right left==right),或者满足其他要求的特殊条件为止。
left, right = 0, len(nums) - 1
while left < right:
if 满足要求的特殊条件:
return 符合条件的值
elif 一定条件 1:
left += 1
elif 一定条件 2:
right -= 1
return 没找到 或 找到对应值
对撞指针一般用来解决有序数组或者字符串问题:
下面我们根据具体例子来讲解如何使用对撞指针来解决问题。
描述:给定一个下标从 1 1 1 开始计数、升序排列的整数数组: n u m b e r s numbers numbers 和一个目标值 t a r g e t target target。
要求:从数组中找出满足相加之和等于 t a r g e t target target 的两个数,并返回两个数在数组中下的标值。
说明:
示例:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
这道题如果暴力遍历数组,从中找到相加之和等于 t a r g e t target target 的两个数,时间复杂度为 O ( n 2 ) O(n^2) O(n2),可以尝试一下。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
size = len(numbers)
for i in range(size):
for j in range(i + 1, size):
if numbers[i] + numbers[j] == target:
return [i + 1, j + 1]
return [-1, -1]
结果不出意外的超时了。所以我们要想办法减少时间复杂度。
可以考虑使用对撞指针来减少时间复杂度。具体做法如下:
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left = 0
right = len(numbers) - 1
while left < right:
total = numbers[left] + numbers[right]
if total == target:
return [left + 1, right + 1]
elif total < target:
left += 1
else:
right -= 1
return [-1, -1]
描述:给定一个字符串 s s s。
要求:判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。
说明:
示例:
输入: "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
输入:"race a car"
输出:false
解释:"raceacar" 不是回文串。
class Solution:
def isPalindrome(self, s: str) -> bool:
left = 0
right = len(s) - 1
while left < right:
if not s[left].isalnum():
left += 1
continue
if not s[right].isalnum():
right -= 1
continue
if s[left].lower() == s[right].lower():
left += 1
right -= 1
else:
return False
return True
描述:给定 n n n 个非负整数 a 1 , a 2 , . . . , a n a_1,a_2, ...,a_n a1,a2,...,an,每个数代表坐标中的一个点 ( i , a i ) (i, a_i) (i,ai)。在坐标内画 n n n 条垂直线,垂直线 i i i 的两个端点分别为 ( i , a i ) (i, a_i) (i,ai) 和 ( i , 0 ) (i, 0) (i,0)。
要求:找出其中的两条线,使得它们与 x x x 轴共同构成的容器可以容纳最多的水。
说明:
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
从示例中可以看出,如果确定好左右两端的直线,容纳的水量是由「左右两端直线中较低直线的高度 * 两端直线之间的距离」所决定的。所以我们应该使得「」,这样才能使盛水面积尽可能的大。
可以使用对撞指针求解。移动较低直线所在的指针位置,从而得到不同的高度和面积,最终获取其中最大的面积。具体做法如下:
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
ans = 0
while left < right:
area = min(height[left], height[right]) * (right-left)
ans = max(ans, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return ans
快慢指针:指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为 「快指针(fast)」,移动慢的指针被称为「慢指针(slow)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。
slow = 0
fast = 1
while 没有遍历完:
if 满足要求的特殊条件:
slow += 1
fast += 1
return 合适的值
快慢指针一般用于处理数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。关于链表相关的双指针做法我们到链表章节再详细讲解。
下面我们根据具体例子来讲解如何使用快慢指针来解决问题。
描述:给定一个有序数组 n u m s nums nums。
要求:删除数组 n u m s nums nums 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。
说明:
示例:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 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 。不需要考虑数组中超出新长度后面的元素。
因为数组是有序的,那么重复的元素一定会相邻。
删除重复元素,实际上就是将不重复的元素移到数组左侧。考虑使用双指针。具体算法如下:
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if len(nums) <= 1:
return len(nums)
slow, fast = 0, 1
while (fast < len(nums)):
if nums[slow] != nums[fast]:
slow += 1
nums[slow] = nums[fast]
fast += 1
return slow + 1
分离双指针:两个指针分别属于不同的数组,两个指针分别在两个数组中移动。
left_1 = 0
left_2 = 0
while left_1 < len(nums1) and left_2 < len(nums2):
if 一定条件 1:
left_1 += 1
left_2 += 1
elif 一定条件 2:
left_1 += 1
elif 一定条件 3:
left_2 += 1
分离双指针一般用于处理有序数组合并,求交集、并集问题。
下面我们根据具体例子来讲解如何使用分离双指针来解决问题。
描述:给定两个数组 n u m s 1 nums1 nums1 和 n u m s 2 nums2 nums2。
要求:返回两个数组的交集。重复元素只计算一次。
说明:
示例:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort()
nums2.sort()
left_1 = 0
left_2 = 0
res = []
while left_1 < len(nums1) and left_2 < len(nums2):
if nums1[left_1] == nums2[left_2]:
if nums1[left_1] not in res:
res.append(nums1[left_1])
left_1 += 1
left_2 += 1
elif nums1[left_1] < nums2[left_2]:
left_1 += 1
elif nums1[left_1] > nums2[left_2]:
left_2 += 1
return res
双指针分为「对撞指针」、「快慢指针」、「分离双指针」。
描述:给定一个字符数组 s
。
要求:将其反转。
说明:
s[i]
都是 ASCII 码表中的可打印字符。示例:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
left
,right
。left
指向字符数组开始位置,right
指向字符数组结束位置。s[left]
和 s[right]
,将 left
右移、right
左移。left == right
,跳出循环。class Solution:
def reverseString(self, s: List[str]) -> None:
left, right = 0, len(s) - 1
while left < right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
描述:给定一个整数数组 nums
。
要求:判断 nums
中是否存在三个元素 a
、b
、c
,满足 a + b + c == 0
。要求找出所有满足要求的不重复的三元组。
说明:
示例:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
输入:nums = [0,1,1]
输出:[]
直接三重遍历查找 a
、b
、c
的时间复杂度是: O ( n 3 ) O(n^3) O(n3)。我们可以通过一些操作来降低复杂度。
先将数组进行排序,以保证按顺序查找 a
、b
、c
时,元素值为升序,从而保证所找到的三个元素是不重复的。同时也方便下一步使用双指针减少一重遍历。时间复杂度为: O ( n l o g n ) O(nlogn) O(nlogn)。
第一重循环遍历 a
,对于每个 a
元素,从 a
元素的下一个位置开始,使用对撞指针 left
,right
。left
指向 a
元素的下一个位置,right
指向末尾位置。先将 left
右移、right
左移去除重复元素,再进行下边的判断。
nums[a] + nums[left] + nums[right] = 0
,则得到一个解,将其加入答案数组中,并继续将 left
右移,right
左移;nums[a] + nums[left] + nums[right] > 0
,说明 nums[right]
值太大,将 right
向左移;nums[a] + nums[left] + nums[right] < 0
,说明 nums[left]
值太小,将 left
右移。class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
nums.sort()
ans = []
for i in range(n):
if i > 0 and nums[i] == nums[i - 1]:
continue
left = i + 1
right = n - 1
while left < right:
while left < right and left > i + 1 and nums[left] == nums[left - 1]:
left += 1
while left < right and right < n - 1 and nums[right + 1] == nums[right]:
right -= 1
if left < right and nums[i] + nums[left] + nums[right] == 0:
ans.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
elif nums[i] + nums[left] + nums[right] > 0:
right -= 1
else:
left += 1
return ans
描述:给定一个有序数组 nums
。
要求:在原数组空间基础上删除重复出现 2
次以上的元素,并返回删除后数组的新长度。
说明:
nums
已按升序排列。示例:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 不需要考虑数组中超出新长度后面的元素。
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 不需要考虑数组中超出新长度后面的元素。
因为数组是有序的,所以重复元素必定是连续的。可以使用快慢指针来解决。具体做法如下:
slow
,fast
。slow
指针指向即将放置元素的位置,fast
指针指向当前待处理元素。slow - 2
是上上次放置了元素的位置。则应该检查 nums[slow - 2]
和当前待处理元素 nums[fast]
是否相同。
nums[slow - 2] == nums[fast]
时,此时必有 nums[slow - 2] == nums[slow - 1] == nums[fast]
,则当前 nums[fast]
不保留,直接向右移动快指针 fast
。nums[slow - 2] != nums[fast]
时,则保留 nums[fast]
。将 nums[fast]
赋值给 nums[slow]
,同时将 slow
右移。然后再向右移动快指针 fast
。slow
指针左边均为处理好的数组元素,而从 slow
指针指向的位置开始, fast
指针左边都为舍弃的重复元素。slow
就是新数组的长度。class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
size = len(nums)
if size <= 2:
return size
slow, fast = 2, 2
while (fast < size):
if nums[slow - 2] != nums[fast]:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
描述:给定一个数组 nums
。
要求:将所有 0
移动到末尾,并保持原有的非 0
数字的相对顺序。
说明:
示例:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
输入: nums = [0]
输出: [0]
slow
,fast
。slow
指向处理好的非 0
数字数组的尾部,fast
指针指向当前待处理元素。fast
指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 slow
右移。slow
指针左侧均为处理好的非零数,而从 slow
指针指向的位置开始, fast
指针左边为止都为 0
。遍历结束之后,则所有 0
都移动到了右侧,且保持了非零数的相对位置。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
slow = 0
fast = 0
while fast < len(nums):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
fast += 1
描述:给定一个数组 nums
,元素值只有 0
、1
、2
,分别代表红色、白色、蓝色。
要求:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。
说明:
nums[i]
为 0
、1
或 2
。示例:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
输入:nums = [2,0,1]
输出:[0,1,2]
快速排序算法中的 partition
过程,利用双指针,将序列中比基准数 pivot
大的元素移动到了基准数右侧,将比基准数 pivot
小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。
这道题我们也可以借鉴快速排序算法中的 partition
过程,将 1
作为基准数 pivot
,然后将序列分为三部分:0
(即比 1
小的部分)、等于 1
的部分、2
(即比 1
大的部分)。具体步骤如下:
left
、right
,分别指向数组的头尾。left
表示当前处理好红色元素的尾部,right
表示当前处理好蓝色的头部。index
遍历数组,如果遇到 nums[index] == 0
,就交换 nums[index]
和 nums[left]
,同时将 left
右移。如果遇到 nums[index] == 2
,就交换 nums[index]
和 nums[right]
,同时将 right
左移。index
移动到 right
位置之后,停止遍历。遍历结束之后,此时 left
左侧都是红色,right
右侧都是蓝色。注意:移动的时候需要判断 index
和 left
的位置,因为 left
左侧是已经处理好的数组,所以需要判断 index
的位置是否小于 left
,小于的话,需要更新 index
位置。
class Solution:
def sortColors(self, nums: List[int]) -> None:
left = 0
right = len(nums) - 1
index = 0
while index <= right:
if index < left:
index += 1
elif nums[index] == 0:
nums[index], nums[left] = nums[left], nums[index]
left += 1
elif nums[index] == 2:
nums[index], nums[right] = nums[right], nums[index]
right -= 1
else:
index += 1
描述:给定一个有序数组 nums
。
要求:在原数组空间基础上删除重复出现 2
次以上的元素,并返回删除后数组的新长度。
说明:
nums
已按升序排列。示例:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 不需要考虑数组中超出新长度后面的元素。
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 不需要考虑数组中超出新长度后面的元素。
因为数组是有序的,所以重复元素必定是连续的。可以使用快慢指针来解决。具体做法如下:
slow
,fast
。slow
指针指向即将放置元素的位置,fast
指针指向当前待处理元素。slow - 2
是上上次放置了元素的位置。则应该检查 nums[slow - 2]
和当前待处理元素 nums[fast]
是否相同。
nums[slow - 2] == nums[fast]
时,此时必有 nums[slow - 2] == nums[slow - 1] == nums[fast]
,则当前 nums[fast]
不保留,直接向右移动快指针 fast
。nums[slow - 2] != nums[fast]
时,则保留 nums[fast]
。将 nums[fast]
赋值给 nums[slow]
,同时将 slow
右移。然后再向右移动快指针 fast
。slow
指针左边均为处理好的数组元素,而从 slow
指针指向的位置开始, fast
指针左边都为舍弃的重复元素。slow
就是新数组的长度。class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
size = len(nums)
if size <= 2:
return size
slow, fast = 2, 2
while (fast < size):
if nums[slow - 2] != nums[fast]:
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0167 | 两数之和 II - 输入有序数组 | 网页链接、Github 链接 | 数组、双指针、二分查找 | 中等 |
0344 | 反转字符串 | 网页链接、Github 链接 | 双指针、字符串 | 简单 |
0345 | 反转字符串中的元音字母 | 双指针、字符串 | 简单 | |
0125 | 验证回文串 | 网页链接、Github 链接 | 双指针、字符串 | 简单 |
0011 | 盛最多水的容器 | 网页链接、Github 链接 | 贪心、数组、双指针 | 中等 |
0611 | 有效三角形的个数 | 贪心、数组、双指针、二分查找、排序 | 中等 | |
0015 | 三数之和 | 网页链接、Github 链接 | 数组、双指针、排序 | 中等 |
0016 | 最接近的三数之和 | 数组、双指针、排序 | 中等 | |
0018 | 四数之和 | 数组、双指针、排序 | 中等 | |
0259 | 较小的三数之和 | 数组、双指针、二分查找、排序 | 中等 | |
0658 | 找到 K 个最接近的元素 | 数组、双指针、二分查找、排序、滑动窗口、堆(优先队列) | 中等 | |
1099 | 小于 K 的两数之和 | 数组、双指针、二分查找、排序 | 简单 | |
0075 | 颜色分类 | 网页链接、Github 链接 | 数组、双指针、排序 | 中等 |
0360 | 有序转化数组 | 数组、数学、双指针、排序 | 中等 | |
0977 | 有序数组的平方 | 数组、双指针、排序 | 简单 | |
0881 | 救生艇 | 网页链接、Github 链接 | 贪心、数组、双指针、排序 | 中等 |
0042 | 接雨水 | 网页链接、Github 链接 | 栈、数组、双指针、动态规划、单调栈 | 困难 |
剑指 Offer 21 | 调整数组顺序使奇数位于偶数前面 | 数组、双指针、排序 | 简单 | |
0443 | 压缩字符串 | 双指针、字符串 | 中等 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0026 | 删除有序数组中的重复项 | 网页链接、Github 链接 | 数组、双指针 | 简单 |
0080 | 删除有序数组中的重复项 II | 网页链接、Github 链接 | 数组、双指针 | 中等 |
0027 | 移除元素 | 数组、双指针 | 简单 | |
0283 | 移动零 | 网页链接、Github 链接 | 数组、双指针 | 简单 |
0845 | 数组中的最长山脉 | 数组、双指针、动态规划、枚举 | 中等 | |
0088 | 合并两个有序数组 | 网页链接、Github 链接 | 数组、双指针、排序 | 简单 |
0719 | 找出第 K 小的数对距离 | 数组、双指针、二分查找、排序 | 困难 | |
0334 | 递增的三元子序列 | 贪心、数组 | 中等 | |
0978 | 最长湍流子数组 | 数组、动态规划、滑动窗口 | 中等 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0350 | 两个数组的交集 II | 数组、哈希表、双指针、二分查找、排序 | 简单 | |
0925 | 长按键入 | 双指针、字符串 | 简单 | |
0844 | 比较含退格的字符串 | 栈、双指针、字符串、模拟 | 简单 | |
1229 | 安排会议日程 | 数组、双指针、排序 | 中等 | |
0415 | 字符串相加 | 网页链接、Github 链接 | 数学、字符串、模拟 | 简单 |
在计算机网络中,滑动窗口协议(Sliding Window Protocol)是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。我们所要讲解的滑动窗口算法也是利用了同样的特性。
滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个固定长度或不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
滑动窗口利用了双指针中的快慢指针技巧,我们可以将滑动窗口看做是快慢指针两个指针中间的区间,也可以将滑动窗口看做是快慢指针的一种特殊形式。
滑动窗口算法一般用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。该算法可以将一部分问题中的嵌套循环转变为一个单循环,因此它可以减少时间复杂度。
按照窗口长度的固定情况,我们可以将滑动窗口题目分为以下两种:
下面来分别讲解一下这两种类型题目。
固定长度滑动窗口算法(Fixed Length Sliding Window):在给定数组 / 字符串上维护一个固定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
假设窗口的固定大小为 w i n d o w ‾ s i z e window\underline{}size windowsize。
window.append(nums[right])
。right - left + 1 >= window_size
时,判断窗口内的连续元素是否满足题目限定的条件。
left += 1
,使得窗口大小始终保持为 w i n d o w ‾ s i z e window\underline{}size windowsize。window.append(nums[right])
。left = 0
right = 0
while right < len(nums):
window.append(nums[right])
# 超过窗口大小时,缩小窗口,维护窗口中始终为 window_size 的长度
if right - left + 1 >= window_size:
# ... 维护答案
window.popleft()
left += 1
# 向右侧增大窗口
right += 1
下面我们根据具体例子来讲解一下如何使用固定窗口大小的滑动窗口来解决问题。
描述:给定一个整数数组 a r r arr arr 和两个整数 k k k 和 t h r e s h o l d threshold threshold 。
要求:返回长度为 k k k 且平均值大于等于 t h r e s h o l d threshold threshold 的子数组数目。
说明:
示例:
输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4
输出:3
解释:子数组 [2,5,5],[5,5,5] 和 [5,5,8] 的平均值分别为 4,5 和 6 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)。
输入:arr = [11,13,17,23,29,31,7,5,2,3], k = 3, threshold = 5
输出:6
解释:前 6 个长度为 3 的子数组平均值都大于 5 。注意平均值不是整数。
这道题目是典型的固定窗口大小的滑动窗口题目。窗口大小为 k k k。具体做法如下:
window_sum += arr[right]
。right - left + 1 >= k
时,判断窗口内的元素和平均值是否大于等于阈值 t h r e s h o l d threshold threshold。
left += 1
,使得窗口大小始终保持为 k k k。class Solution:
def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
left = 0
right = 0
window_sum = 0
ans = 0
while right < len(arr):
window_sum += arr[right]
if right - left + 1 >= k:
if window_sum >= k * threshold:
ans += 1
window_sum -= arr[left]
left += 1
right += 1
return ans
不定长度滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。
window.add(s[right])
。right += 1
。直到窗口中的连续元素满足要求。window.popleft(s[left])
。left += 1
。直到窗口中的连续元素不再满足要求。left = 0
right = 0
while right < len(nums):
window.append(nums[right])
while 窗口需要缩小:
# ... 可维护答案
window.popleft()
left += 1
# 向右侧增大窗口
right += 1
描述:给定一个字符串 s s s。
要求:找出其中不含有重复字符的最长子串的长度。
说明:
示例:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
用滑动窗口 w i n d o w window window 来记录不重复的字符个数, w i n d o w window window 为哈希表类型。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left = 0
right = 0
window = dict()
ans = 0
while right < len(s):
if s[right] not in window:
window[s[right]] = 1
else:
window[s[right]] += 1
while window[s[right]] > 1:
window[s[left]] -= 1
left += 1
ans = max(ans, right - left + 1)
right += 1
return ans
描述:给定一个只包含正整数的数组 n u m s nums nums 和一个正整数 t a r g e t target target。
要求:找出数组中满足和大于等于 t a r g e t target target 的长度最小的「连续子数组」,并返回其长度。如果不存在符合条件的子数组,返回 0 0 0。
说明:
示例:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
输入:target = 4, nums = [1,4,4]
输出:1
最直接的做法是暴力枚举,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。但是我们可以利用滑动窗口的方法,在时间复杂度为 O ( n ) O(n) O(n) 的范围内解决问题。
用滑动窗口来记录连续子数组的和,设定两个指针: l e f t left left、 r i g h t right right,分别指向滑动窗口的左右边界,保证窗口中的和刚好大于等于 t a r g e t target target。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
size = len(nums)
ans = size + 1
left = 0
right = 0
window_sum = 0
while right < size:
window_sum += nums[right]
while window_sum >= target:
ans = min(ans, right - left + 1)
window_sum -= nums[left]
left += 1
right += 1
return ans if ans != size + 1 else 0
描述:给定一个正整数数组 n u m s nums nums 和整数 k k k。
要求:找出该数组内乘积小于 k k k 的连续的子数组的个数。
说明:
示例:
输入: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 的子数组。
输入:nums = [1,2,3], k = 0
输出:0
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
if k <= 1:
return 0
size = len(nums)
left = 0
right = 0
window_product = 1
count = 0
while right < size:
window_product *= nums[right]
while window_product >= k:
window_product /= nums[left]
left += 1
count += (right - left + 1)
right += 1
return count
描述:给定一个未经排序的数组 nums
。
要求:找到最长且连续递增的子序列,并返回该序列的长度。
说明:
l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。示例:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为 3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
定义状态 dp[i]
表示为:以 nums[i]
结尾的最长且连续递增的子序列长度。
因为求解的是连续子序列,所以只需要考察相邻元素的状态转移方程。
如果一个较小的数右侧相邻元素为一个较大的数,则会形成一个更长的递增子序列。
对于相邻的数组元素 nums[i - 1]
和 nums[i]
来说:
如果 nums[i - 1] < nums[i]
,则 nums[i]
可以接在 nums[i - 1]
后面,此时以 nums[i]
结尾的最长递增子序列长度会在「以 nums[i - 1]
结尾的最长递增子序列长度」的基础上加 1
,即 dp[i] = dp[i - 1] + 1
。
如果 nums[i - 1] >= nums[i]
,则 nums[i]
不可以接在 nums[i - 1]
后面,可以直接跳过。
综上,我们的状态转移方程为:dp[i] = dp[i - 1] + 1
,nums[i - 1] < nums[i]
。
默认状态下,把数组中的每个元素都作为长度为 1
的最长且连续递增的子序列长度。即 dp[i] = 1
。
根据我们之前定义的状态,dp[i]
表示为:以 nums[i]
结尾的最长且连续递增的子序列长度。则为了计算出最大值,则需要再遍历一遍 dp
数组,求出最大值即为最终结果。
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
size = len(nums)
dp = [1 for _ in range(size)]
for i in range(1, size):
if nums[i - 1] < nums[i]:
dp[i] = dp[i - 1] + 1
return max(dp)
left
、right
,分别指向滑动窗口的左右边界,保证窗口内为连续递增序列。使用 window_len
存储当前窗口大小,使用 max_len
维护最大窗口长度。left
、right
都指向 0
。nums[right]
加入当前连续递增序列中,即当前窗口长度加 1
(window_len += 1
)。nums[right]
是否满足连续递增序列。right > 0
并且 nums[right - 1] >= nums[right]
,说明不满足连续递增序列,则将 left
移动到窗口最右侧,重置当前窗口长度为 1
(window_len = 1
)。right
,直到 right >= len(nums)
结束。max_len
。class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
size = len(nums)
left, right = 0, 0
window_len = 0
max_len = 0
while right < size:
window_len += 1
if right > 0 and nums[right - 1] >= nums[right]:
left = right
window_len = 1
max_len = max(max_len, window_len)
right += 1
return max_len
描述:给定一个由 0
、1
组成的数组 nums
,再给定一个整数 k
。最多可以将 k
个值从 0
变到 1
。
要求:返回仅包含 1
的最长连续子数组的长度。
说明:
nums[i]
不是 0
就是 1
。示例:
输入:nums = [1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], K = 2
输出:6
解释:[1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1]
将 nums[5]、nums[10] 从 0 翻转到 1,最长的子数组长度为 6。
输入:nums = [0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], K = 3
输出:10
解释:[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1]
将 nums[4]、nums[5]、nums[9] 从 0 翻转到 1,最长的子数组长度为 10。
left
、right
指向数组开始位置。使用 max_count
来维护仅包含 1
的最长连续子数组的长度。right
指针,扩大滑动窗口范围,并统计窗口内 0
元素的个数。0
元素的个数超过 k
时将 left
右移,缩小滑动窗口范围,并减小 0
元素的个数,同时维护 max_count
。max_count
。class Solution:
def longestOnes(self, nums: List[int], k: int) -> int:
max_count = 0
zero_count = 0
left, right = 0, 0
while right < len(nums):
if nums[right] == 0:
zero_count += 1
right += 1
if zero_count > k:
if nums[left] == 0:
zero_count -= 1
left += 1
max_count = max(max_count, right - left)
return max_count
描述:给定一个整数数组 nums
,以及两个整数 k
、t
。
要求:判断数组中是否存在两个不同下标的 i
和 j
,其对应元素满足 abs(nums[i] - nums[j]) <= t
,同时满足 abs(i - j) <= k
。如果满足条件则返回 True
,不满足条件返回 False
。
说明:
示例:
输入:nums = [1,2,3,1], k = 3, t = 0
输出:True
输入:nums = [1,0,1,1], k = 1, t = 2
输出:True
题目中需要满足两个要求,一个是元素值的要求(abs(nums[i] - nums[j]) <= t
) ,一个是下标范围的要求(abs(i - j) <= k
)。
对于任意一个位置 i
来说,合适的 j
应该在区间 [i - k, i + k]
内,同时 nums[j]
值应该在区间 [nums[i] - t, nums[i] + t]
内。
最简单的做法是两重循环遍历数组,第一重循环遍历位置 i
,第二重循环遍历 [i - k, i + k]
的元素,判断是否满足 abs(nums[i] - nums[j]) <= t
。但是这样做的时间复杂度为 O ( n × k ) O(n \times k) O(n×k),其中 n n n 是数组 nums
的长度。
我们需要优化一下检测相邻 2 * k
个元素是否满足 abs(nums[i] - nums[j]) <= t
的方法。有两种思路:「桶排序」和「滑动窗口(固定长度)」。
t + 1
。只需要使用一重循环遍历位置 i
,然后根据 nums[i] // (t + 1)
,从而决定将 nums[i]
放入哪个桶中。t
。而相邻桶之间的元素,只需要校验一下两个桶之间的差值是否不超过 t
。这样就可以以 O ( 1 ) O(1) O(1) 的时间复杂度检测相邻 2 * k
个元素是否满足 abs(nums[i] - nums[j]) <= t
。abs(i - j) <= k
条件则可以通过在一重循环遍历时,将超出范围的 nums[i - k]
从对应桶中删除,从而保证桶中元素一定满足 abs(i - j) <= k
。具体步骤如下:
t + 1
。我们将元素按照大小依次放入不同的桶中。nums
中的元素,对于元素 nums[i]
:
nums[i]
放入桶之前桶里已经有元素了,那么这两个元素必然满足 abs(nums[i] - nums[j]) <= t
,nums[i]
放入对应桶中。abs(nums[i] - nums[j]) <= t
。nums[i - k]
之前的桶清空,因为这些桶中的元素与 nums[i]
已经不满足 abs(i - j) <= k
了。True
,最终遍历完仍不满足条件就返回 False
。class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
bucket_dict = dict()
for i in range(len(nums)):
# 将 nums[i] 划分到大小为 t + 1 的不同桶中
num = nums[i] // (t + 1)
# 桶中已经有元素了
if num in bucket_dict:
return True
# 把 nums[i] 放入桶中
bucket_dict[num] = nums[i]
# 判断左侧桶是否满足条件
if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
return True
# 判断右侧桶是否满足条件
if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
return True
# 将 i - k 之前的旧桶清除,因为之前的桶已经不满足条件了
if i >= k:
bucket_dict.pop(nums[i - k] // (t + 1))
return False
k
的滑动窗口,每次遍历到 nums[right]
时,滑动窗口内最多包含 nums[right]
之前最多 k
个元素。只需要检查前 k
个元素是否在 [nums[right] - t, nums[right] + t]
区间内即可。k
个元素是否在 [nums[right] - t, nums[right] + t]
区间,可以借助保证有序的数据结构(比如 SortedList
)+ 二分查找来解决,从而减少时间复杂度。具体步骤如下:
window
维护一个长度为 k
的窗口,满足数组内元素有序,且支持增加和删除操作。left
、right
都指向序列的第一个元素。即:left = 0
,right = 0
。window.add(nums[right])
。k
个时,即 right - left > k
,移除窗口最左侧元素,并向右移动 left
。k
个时:
nums[right]
在 window
中的位置 idx
。window[idx]
与相邻位置上元素差值绝对值,若果满足 abs(window[idx] - window[idx - 1]) <= t
或者 abs(window[idx + 1] - window[idx]) <= t
时返回 True
。right
。3
~ 6
步,直到 right
到达数组末尾,如果还没找到满足条件的情况,则返回 False
。from sortedcontainers import SortedList
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
size = len(nums)
window = SortedList()
left, right = 0, 0
while right < size:
window.add(nums[right])
if right - left > k:
window.remove(nums[left])
left += 1
idx = bisect.bisect_left(window, nums[right])
if idx > 0 and abs(window[idx] - window[idx - 1]) <= t:
return True
if idx < len(window) - 1 and abs(window[idx + 1] - window[idx]) <= t:
return True
right += 1
return False
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
1343 | 大小为 K 且平均值大于等于阈值的子数组数目 | 网页链接、Github 链接 | 数组、滑动窗口 | 中等 |
0643 | 子数组最大平均数 I | 数组、滑动窗口 | 简单 | |
1052 | 爱生气的书店老板 | 数组、滑动窗口 | 中等 | |
1423 | 可获得的最大点数 | 数组、前缀和、滑动窗口 | 中等 | |
1456 | 定长子串中元音的最大数目 | 字符串、滑动窗口 | 中等 | |
0567 | 字符串的排列 | 哈希表、双指针、字符串、滑动窗口 | 中等 | |
1100 | 长度为 K 的无重复字符子串 | 哈希表、字符串、滑动窗口 | 中等 | |
1151 | 最少交换次数来组合所有的 1 | 数组、滑动窗口 | 中等 | |
1176 | 健身计划评估 | 数组、滑动窗口 | 简单 | |
0438 | 找到字符串中所有字母异位词 | 哈希表、字符串、滑动窗口 | 中等 | |
0995 | K 连续位的最小翻转次数 | 位运算、队列、数组、前缀和、滑动窗口 | 困难 | |
0683 | K 个关闭的灯泡 | 树状数组、数组、有序集合、滑动窗口 | 困难 | |
0220 | 存在重复元素 III | 网页链接、Github 链接 | 数组、桶排序、有序集合、排序、滑动窗口 | 困难 |
0239 | 滑动窗口最大值 | 网页链接、Github 链接 | 队列、数组、滑动窗口、单调队列、堆(优先队列) | 困难 |
0480 | 滑动窗口中位数 | 数组、哈希表、滑动窗口、堆(优先队列) | 困难 |
题号 | 标题 | 题解 | 标签 | 难度 |
---|---|---|---|---|
0674 | 最长连续递增序列 | 网页链接、Github 链接 | 数组 | 简单 |
0485 | 最大连续 1 的个数 | 数组 | 简单 | |
0487 | 最大连续1的个数 II | 数组、动态规划、滑动窗口 | 中等 | |
0076 | 最小覆盖子串 | 网页链接、Github 链接 | 哈希表、字符串、滑动窗口 | 困难 |
0718 | 最长重复子数组 | 网页链接、Github 链接 | 数组、二分查找、动态规划、滑动窗口、哈希函数、滚动哈希 | 中等 |
0209 | 长度最小的子数组 | 网页链接、Github 链接 | 数组、二分查找、前缀和、滑动窗口 | 中等 |
0862 | 和至少为 K 的最短子数组 | 队列、数组、二分查找、前缀和、滑动窗口、单调队列、堆(优先队列) | 困难 | |
1004 | 最大连续1的个数 III | 网页链接、Github 链接 | 数组、二分查找、前缀和、滑动窗口 | 中等 |
1658 | 将 x 减到 0 的最小操作数 | 数组、哈希表、二分查找、前缀和、滑动窗口 | 中等 | |
0424 | 替换后的最长重复字符 | 哈希表、字符串、滑动窗口 | 中等 | |
0003 | 无重复字符的最长子串 | 网页链接、Github 链接 | 哈希表、字符串、滑动窗口 | 中等 |
1695 | 删除子数组的最大得分 | 数组、哈希表、滑动窗口 | 中等 | |
1208 | 尽可能使字符串相等 | 字符串、二分查找、前缀和、滑动窗口 | 中等 | |
1493 | 删掉一个元素以后全为 1 的最长子数组 | 数组、动态规划、滑动窗口 | 中等 | |
0727 | 最小窗口子序列 | 字符串、动态规划、滑动窗口 | 困难 | |
0159 | 至多包含两个不同字符的最长子串 | 哈希表、字符串、滑动窗口 | 中等 | |
0340 | 至多包含 K 个不同字符的最长子串 | 哈希表、字符串、滑动窗口 | 中等 | |
0795 | 区间子数组个数 | 数组、双指针 | 中等 | |
0992 | K 个不同整数的子数组 | 数组、哈希表、计数、滑动窗口 | 困难 | |
0713 | 乘积小于 K 的子数组 | 网页链接、Github 链接 | 数组、滑动窗口 | 中等 |
0904 | 水果成篮 | 数组、哈希表、滑动窗口 | 中等 | |
1358 | 包含所有三种字符的子字符串数目 | 哈希表、字符串、滑动窗口 | 中等 | |
0467 | 环绕字符串中唯一的子字符串 | 字符串、动态规划 | 中等 | |
1438 | 绝对差不超过限制的最长连续子数组 | 队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) | 中等 |