Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口

LeetCode 01-算法入门与数组-⑤数组双指针&滑动窗口

一. 双指针基础知识

1. 双指针简介

双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为「对撞指针」。如果两个指针方向相同,则称为「快慢指针」。如果两个指针分别属于不同的数组 / 链表,则称为「分离双指针」。

在数组的区间问题上,暴力算法的时间复杂度往往是 O ( n 2 ) O(n^2) O(n2)。而双指针利用了区间「单调性」的性质,可以将时间复杂度降到 O ( n ) O(n) O(n)

2. 对撞指针

对撞指针:指的是两个指针 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),或者满足其他要求的特殊条件为止。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第1张图片

2.1 对撞指针求解步骤

  1. 使用两个指针 l e f t left left r i g h t right right l e f t left left 指向序列第一个元素,即: l e f t = 0 left = 0 left=0 r i g h t right right 指向序列最后一个元素,即: r i g h t = l e n ( n u m s ) − 1 right = len(nums) - 1 right=len(nums)1
  2. 在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移, l e f t + = 1 left += 1 left+=1。当满足另外一定条件时,将右指针左移, r i g h t − = 1 right -= 1 right=1
  3. 直到两指针相撞(即 l e f t = = r i g h t left == right left==right),或者满足其他要求的特殊条件时,跳出循环体。

2.2 对撞指针伪代码模板

left, right = 0, len(nums) - 1

while left < right:
    if 满足要求的特殊条件:
        return 符合条件的值 
    elif 一定条件 1:
        left += 1
    elif 一定条件 2:
        right -= 1

return 没找到 或 找到对应值

2.3 对撞指针适用范围

对撞指针一般用来解决有序数组或者字符串问题:

  • 查找有序数组中满足某些约束条件的一组元素问题:比如二分查找、数字之和等问题。
  • 字符串反转问题:反转字符串、回文数、颠倒二进制等问题。

下面我们根据具体例子来讲解如何使用对撞指针来解决问题。

2.4 两数之和 II - 输入有序数组

2.4.1 题目链接
  • 167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)
2.4.2 题目大意

描述:给定一个下标从 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 的两个数,并返回两个数在数组中下的标值。

说明

  • 2 ≤ n u m b e r s . l e n g t h ≤ 3 ∗ 1 0 4 2 \le numbers.length \le 3 * 10^4 2numbers.length3104
  • − 1000 ≤ n u m b e r s [ i ] ≤ 1000 -1000 \le numbers[i] \le 1000 1000numbers[i]1000
  • n u m b e r s numbers numbers 按非递减顺序排列。
  • − 1000 ≤ t a r g e t ≤ 1000 -1000 \le target \le 1000 1000target1000
  • 仅存在一个有效答案。

示例

  • 示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:27 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2]
  • 示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:24 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3]
2.4.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]

结果不出意外的超时了。所以我们要想办法减少时间复杂度。

思路 1:对撞指针

可以考虑使用对撞指针来减少时间复杂度。具体做法如下:

  1. 使用两个指针 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 指向数组值最大元素位置。
  2. 判断两个位置上的元素的和与目标值的关系。
    1. 如果元素和等于目标值,则返回两个元素位置。
    2. 如果元素和大于目标值,则让 r i g h t right right 左移,继续检测。
    3. 如果元素和小于目标值,则让 l e f t left left 右移,继续检测。
  3. 直到 l e f t left left r i g h t right right 移动到相同位置停止检测。
  4. 如果最终仍没找到,则返回 [ − 1 , − 1 ] [-1, -1] [1,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]
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)。只用到了常数空间存放若干变量。

2.5 验证回文串

2.5.1 题目链接
  • 125. 验证回文串 - 力扣(LeetCode)
2.5.2 题目大意

描述:给定一个字符串 s s s

要求:判断是否为回文串(只考虑字符串中的字母和数字字符,并且忽略字母的大小写)。

说明

  • 回文串:正着读和反着读都一样的字符串。
  • 1 ≤ s . l e n g t h ≤ 2 ∗ 1 0 5 1 \le s.length \le 2 * 10^5 1s.length2105
  • s s s 仅由可打印的 ASCII 字符组成。

示例

输入: "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。


输入:"race a car"
输出:false
解释:"raceacar" 不是回文串。
2.5.3 解题思路
思路 1:对撞指针
  1. 使用两个指针 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 指向字符串结束位置。
  2. 判断两个指针对应字符是否是字母或数字。 通过 l e f t left left 右移、 r i g h t right right 左移的方式过滤掉字母和数字以外的字符。
  3. 然后判断 s [ s t a r t ] s[start] s[start] 是否和 s [ e n d ] s[end] s[end] 相等(注意大小写)。
    1. 如果相等,则将 l e f t left left 右移、 r i g h t right right 左移,继续进行下一次过滤和判断。
    2. 如果不相等,则说明不是回文串,直接返回 F a l s e False False
  4. 如果遇到 l e f t = = r i g h t left == right left==right,跳出循环,则说明该字符串是回文串,返回 T r u e True True
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( l e n ( s ) ) O(len(s)) O(len(s))
  • 空间复杂度 O ( l e n ( s ) ) O(len(s)) O(len(s))

2.6 盛最多水的容器

2.6.1 题目链接
  • 11. 盛最多水的容器 - 力扣(LeetCode)
2.6.2 题目大意

描述:给定 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 轴共同构成的容器可以容纳最多的水。

说明

  • n = = h e i g h t . l e n g t h n == height.length n==height.length
  • 2 ≤ n ≤ 1 0 5 2 \le n \le 10^5 2n105
  • 0 ≤ h e i g h t [ i ] ≤ 1 0 4 0 \le height[i] \le 10^4 0height[i]104

示例

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第2张图片

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49
2.6.3 解题思路
思路 1:对撞指针

从示例中可以看出,如果确定好左右两端的直线,容纳的水量是由「左右两端直线中较低直线的高度 * 两端直线之间的距离」所决定的。所以我们应该使得「」,这样才能使盛水面积尽可能的大。

可以使用对撞指针求解。移动较低直线所在的指针位置,从而得到不同的高度和面积,最终获取其中最大的面积。具体做法如下:

  1. 使用两个指针 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 指向数组结束位置。
  2. 计算 l e f t left left r i g h t right right 所构成的面积值,同时维护更新最大面积值。
  3. 判断 l e f t left left r i g h t right right 的高度值大小。
    1. 如果 l e f t left left 指向的直线高度比较低,则将 l e f t left left 指针右移。
    2. 如果 r i g h t right right 指向的直线高度比较低,则将 r i g h t right right 指针左移。
  4. 如果遇到 l e f t = = r i g h t left == right left==right,跳出循环,最后返回最大的面积。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

3. 快慢指针

快慢指针:指的是两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢。移动快的指针被称为 「快指针(fast)」,移动慢的指针被称为「慢指针(slow)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第3张图片

3.1 快慢指针求解步骤

  1. 使用两个指针 s l o w slow slow f a s t fast fast s l o w slow slow 一般指向序列第一个元素,即: s l o w = 0 slow = 0 slow=0 f a s t fast fast 一般指向序列第二个元素,即: f a s t = 1 fast = 1 fast=1
  2. 在循环体中将左右指针向右移动。当满足一定条件时,将慢指针右移,即 s l o w + = 1 slow += 1 slow+=1。当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即 f a s t + = 1 fast += 1 fast+=1
  3. 到快指针移动到数组尾端(即 f a s t = = l e n ( n u m s ) − 1 fast == len(nums) - 1 fast==len(nums)1),或者两指针相交,或者满足其他特殊条件时跳出循环体。

3.2 快慢指针伪代码模板

slow = 0
fast = 1
while 没有遍历完:
    if 满足要求的特殊条件:
        slow += 1
    fast += 1
return 合适的值

3.3 快慢指针适用范围

快慢指针一般用于处理数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。关于链表相关的双指针做法我们到链表章节再详细讲解。

下面我们根据具体例子来讲解如何使用快慢指针来解决问题。

3.4 删除有序数组中的重复项

3.4.1 题目链接
  • 26. 删除有序数组中的重复项 - 力扣(LeetCode)
3.4.2 题目大意

描述:给定一个有序数组 n u m s nums nums

要求:删除数组 n u m s nums nums 中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。

说明

  • 不能使用额外的数组空间,在原地修改数组,并在使用 O ( 1 ) O(1) O(1) 额外空间的条件下完成。

示例

  • 示例 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 。不需要考虑数组中超出新长度后面的元素。
3.4.3 解题思路
思路 1:快慢指针

因为数组是有序的,那么重复的元素一定会相邻。

删除重复元素,实际上就是将不重复的元素移到数组左侧。考虑使用双指针。具体算法如下:

  1. 定义两个快慢指针 s l o w slow slow f a s t fast fast。其中 s l o w slow slow 指向去除重复元素后的数组的末尾位置。 f a s t fast fast 指向当前元素。
  2. s l o w slow slow 在后, f a s t fast fast 在前。令 s l o w = 0 slow = 0 slow=0 f a s t = 1 fast = 1 fast=1
  3. 比较 s l o w slow slow 位置上元素值和 f a s t fast fast 位置上元素值是否相等。
    • 如果不相等,则将 s l o w slow slow 右移一位,将 f a s t fast fast 指向位置的元素复制到 s l o w slow slow 位置上。
  4. f a s t fast fast 右移 1 1 1 位。
  5. 重复上述 3 ∼ 4 3 \sim 4 34 步,直到 f a s t fast fast 等于数组长度。
  6. 返回 s l o w + 1 slow + 1 slow+1 即为新数组长度。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

4. 分离双指针

分离双指针:两个指针分别属于不同的数组,两个指针分别在两个数组中移动。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第4张图片

4.1 分离双指针求解步骤

  1. 使用两个指针 l e f t ‾ 1 left\underline{}1 left1 l e f t ‾ 2 left\underline{}2 left2 l e f t ‾ 1 left\underline{}1 left1 指向第一个数组的第一个元素,即: l e f t ‾ 1 = 0 left\underline{}1 = 0 left1=0 l e f t ‾ 2 left\underline{}2 left2 指向第二个数组的第一个元素,即: l e f t ‾ 2 = 0 left\underline{}2 = 0 left2=0
  2. 当满足一定条件时,两个指针同时右移,即 l e f t ‾ 1 + = 1 left\underline{}1 += 1 left1+=1 l e f t ‾ 2 + = 1 left\underline{}2 += 1 left2+=1
  3. 当满足另外一定条件时,将 l e f t ‾ 1 left\underline{}1 left1 指针右移,即 l e f t ‾ 1 + = 1 left\underline{}1 += 1 left1+=1
  4. 当满足其他一定条件时,将 l e f t ‾ 2 left\underline{}2 left2 指针右移,即 l e f t ‾ 2 + = 1 left\underline{}2 += 1 left2+=1
  5. 当其中一个数组遍历完时或者满足其他特殊条件时跳出循环体。

4.2 分离双指针伪代码模板

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

4.3 分离双指针使用范围

分离双指针一般用于处理有序数组合并,求交集、并集问题。

下面我们根据具体例子来讲解如何使用分离双指针来解决问题。

4.4 两个数组的交集

4.4.1 题目链接
  • 349. 两个数组的交集 - 力扣(LeetCode)
4.4.2 题目大意

描述:给定两个数组 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2

要求:返回两个数组的交集。重复元素只计算一次。

说明

  • 1 ≤ n u m s 1. l e n g t h , n u m s 2. l e n g t h ≤ 1000 1 \le nums1.length, nums2.length \le 1000 1nums1.length,nums2.length1000
  • 0 ≤ n u m s 1 [ i ] , n u m s 2 [ i ] ≤ 1000 0 \le nums1[i], nums2[i] \le 1000 0nums1[i],nums2[i]1000

示例

  • 示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2
  • 示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
4.4.3 解题思路
思路 1:分离双指针
  1. 对数组 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2 先排序。
  2. 使用两个指针 l e f t ‾ 1 left\underline{}1 left1 l e f t ‾ 2 left\underline{}2 left2 l e f t ‾ 1 left\underline{}1 left1 指向第一个数组的第一个元素,即: l e f t ‾ 1 = 0 left\underline{}1 = 0 left1=0 l e f t ‾ 2 left\underline{}2 left2 指向第二个数组的第一个元素,即: l e f t ‾ 2 = 0 left\underline{}2 = 0 left2=0
  3. 如果 n u m s 1 [ l e f t ‾ 1 ] = = n u m s 2 [ l e f t ‾ 2 ] nums1[left\underline{}1] == nums2[left\underline{}2] nums1[left1]==nums2[left2],则将其加入答案数组(注意去重),并将 l e f t ‾ 1 left\underline{}1 left1 l e f t ‾ 2 left\underline{}2 left2 右移。
  4. 如果 n u m s 1 [ l e f t ‾ 1 ] < n u m s 2 [ l e f t ‾ 2 ] nums1[left\underline{}1] < nums2[left\underline{}2] nums1[left1]<nums2[left2],则将 l e f t ‾ 1 left\underline{}1 left1 右移。
  5. 如果 n u m s 1 [ l e f t ‾ 1 ] > n u m s 2 [ l e f t ‾ 2 ] nums1[left\underline{}1] > nums2[left\underline{}2] nums1[left1]>nums2[left2],则将 l e f t ‾ 2 left\underline{}2 left2 右移。
  6. 最后返回答案数组。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

5. 双指针总结

双指针分为「对撞指针」、「快慢指针」、「分离双指针」。

  • 对撞指针:两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题、字符串反转问题。
  • 快慢指针:两个指针方向相同。适合解决数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。
  • 分离双指针:两个指针分别属于不同的数组 / 链表。适合解决有序数组合并,求交集、并集问题。

二. 双指针练习题目

1. 0344. 反转字符串

1.1 题目大意

描述:给定一个字符数组 s

要求:将其反转。

说明

  • 不能使用额外的数组空间,必须原地修改输入数组、使用 O ( 1 ) O(1) O(1) 的额外空间解决问题。
  • 1 ≤ s . l e n g t h ≤ 1 0 5 1 \le s.length \le 10^5 1s.length105
  • 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"]

1.2 解题思路

思路 1:对撞指针

  1. 使用两个指针 leftrightleft 指向字符数组开始位置,right 指向字符数组结束位置。
  2. 交换 s[left]s[right],将 left 右移、right 左移。
  3. 如果遇到 left == right,跳出循环。

思路 1:代码

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

思路 1:复杂度分析

  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

2. 0015. 三数之和

2.1 题目大意

描述:给定一个整数数组 nums

要求:判断 nums 中是否存在三个元素 abc,满足 a + b + c == 0。要求找出所有满足要求的不重复的三元组。

说明

  • 3 ≤ n u m s . l e n g t h ≤ 3000 3 \le nums.length \le 3000 3nums.length3000
  • − 1 0 5 ≤ n u m s [ i ] ≤ 1 0 5 -10^5 \le nums[i] \le 10^5 105nums[i]105

示例

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]


输入:nums = [0,1,1]
输出:[]

2.2 解题思路

思路 1:对撞指针

直接三重遍历查找 abc 的时间复杂度是: O ( n 3 ) O(n^3) O(n3)。我们可以通过一些操作来降低复杂度。

先将数组进行排序,以保证按顺序查找 abc 时,元素值为升序,从而保证所找到的三个元素是不重复的。同时也方便下一步使用双指针减少一重遍历。时间复杂度为: O ( n l o g n ) O(nlogn) O(nlogn)

第一重循环遍历 a,对于每个 a 元素,从 a 元素的下一个位置开始,使用对撞指针 leftrightleft 指向 a 元素的下一个位置,right 指向末尾位置。先将 left 右移、right 左移去除重复元素,再进行下边的判断。

  1. 如果 nums[a] + nums[left] + nums[right] = 0,则得到一个解,将其加入答案数组中,并继续将 left 右移,right 左移;
  2. 如果 nums[a] + nums[left] + nums[right] > 0,说明 nums[right] 值太大,将 right 向左移;
  3. 如果 nums[a] + nums[left] + nums[right] < 0,说明 nums[left] 值太小,将 left 右移。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度 O ( n ) O(n) O(n)

3. 0080. 删除有序数组中的重复项 II

3.1 题目大意

描述:给定一个有序数组 nums

要求:在原数组空间基础上删除重复出现 2 次以上的元素,并返回删除后数组的新长度。

说明

  • 1 ≤ n u m s . l e n g t h ≤ 3 ∗ 1 0 4 1 \le nums.length \le 3 * 10^4 1nums.length3104
  • − 1 0 4 ≤ n u m s [ i ] ≤ 1 0 4 -10^4 \le nums[i] \le 10^4 104nums[i]104
  • 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 。 不需要考虑数组中超出新长度后面的元素。

3.2 解题思路

思路 1:快慢指针

因为数组是有序的,所以重复元素必定是连续的。可以使用快慢指针来解决。具体做法如下:

  1. 使用两个指针 slowfastslow 指针指向即将放置元素的位置,fast 指针指向当前待处理元素。
  2. 本题要求相同元素最多出现 2 次,并且 slow - 2 是上上次放置了元素的位置。则应该检查 nums[slow - 2] 和当前待处理元素 nums[fast] 是否相同。
    1. 如果 nums[slow - 2] == nums[fast] 时,此时必有 nums[slow - 2] == nums[slow - 1] == nums[fast],则当前 nums[fast] 不保留,直接向右移动快指针 fast
    2. 如果 nums[slow - 2] != nums[fast] 时,则保留 nums[fast]。将 nums[fast] 赋值给 nums[slow] ,同时将 slow 右移。然后再向右移动快指针 fast
  3. 这样 slow 指针左边均为处理好的数组元素,而从 slow 指针指向的位置开始, fast 指针左边都为舍弃的重复元素。
  4. 遍历结束之后,此时 slow 就是新数组的长度。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

4. 0283. 移动零

4.1 题目大意

描述:给定一个数组 nums

要求:将所有 0 移动到末尾,并保持原有的非 0 数字的相对顺序。

说明

  • 只能在原数组上进行操作。
  • 1 ≤ n u m s . l e n g t h ≤ 1 0 4 1 \le nums.length \le 10^4 1nums.length104
  • − 2 31 ≤ n u m s [ i ] ≤ 2 31 − 1 -2^{31} \le nums[i] \le 2^{31} - 1 231nums[i]2311

示例

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]


输入: nums = [0]
输出: [0]

4.2 题目解析

思路 1:快慢指针
  1. 使用两个指针 slowfastslow 指向处理好的非 0 数字数组的尾部,fast 指针指向当前待处理元素。
  2. 不断向右移动 fast 指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 slow 右移。
  3. 此时,slow 指针左侧均为处理好的非零数,而从 slow 指针指向的位置开始, fast 指针左边为止都为 0

遍历结束之后,则所有 0 都移动到了右侧,且保持了非零数的相对位置。

思路 1:代码
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

5. 0075. 颜色分类

5.1 题目大意

描述:给定一个数组 nums,元素值只有 012,分别代表红色、白色、蓝色。

要求:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。

说明

  • 要求不使用标准库函数,同时仅用常数空间,一趟扫描解决。
  • n = = n u m s . l e n g t h n == nums.length n==nums.length
  • 1 ≤ n ≤ 300 1 \le n \le 300 1n300
  • nums[i]012

示例

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]


输入:nums = [2,0,1]
输出:[0,1,2]

5.2 题目解析

思路 1:双指针 + 快速排序思想

快速排序算法中的 partition 过程,利用双指针,将序列中比基准数 pivot 大的元素移动到了基准数右侧,将比基准数 pivot 小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。

这道题我们也可以借鉴快速排序算法中的 partition 过程,将 1 作为基准数 pivot,然后将序列分为三部分:0(即比 1 小的部分)、等于 1 的部分、2(即比 1 大的部分)。具体步骤如下:

  1. 使用两个指针 leftright,分别指向数组的头尾。left 表示当前处理好红色元素的尾部,right 表示当前处理好蓝色的头部。
  2. 再使用一个下标 index 遍历数组,如果遇到 nums[index] == 0,就交换 nums[index]nums[left],同时将 left 右移。如果遇到 nums[index] == 2,就交换 nums[index]nums[right],同时将 right 左移。
  3. 直到 index 移动到 right 位置之后,停止遍历。遍历结束之后,此时 left 左侧都是红色,right 右侧都是蓝色。

注意:移动的时候需要判断 indexleft 的位置,因为 left 左侧是已经处理好的数组,所以需要判断 index 的位置是否小于 left,小于的话,需要更新 index 位置。

思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

6. 0080. 删除有序数组中的重复项 II

6.1 题目大意

描述:给定一个有序数组 nums

要求:在原数组空间基础上删除重复出现 2 次以上的元素,并返回删除后数组的新长度。

说明

  • 1 ≤ n u m s . l e n g t h ≤ 3 ∗ 1 0 4 1 \le nums.length \le 3 * 10^4 1nums.length3104
  • − 1 0 4 ≤ n u m s [ i ] ≤ 1 0 4 -10^4 \le nums[i] \le 10^4 104nums[i]104
  • 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 。 不需要考虑数组中超出新长度后面的元素。

6.2 题目解析

思路 1:快慢指针

因为数组是有序的,所以重复元素必定是连续的。可以使用快慢指针来解决。具体做法如下:

  1. 使用两个指针 slowfastslow 指针指向即将放置元素的位置,fast 指针指向当前待处理元素。
  2. 本题要求相同元素最多出现 2 次,并且 slow - 2 是上上次放置了元素的位置。则应该检查 nums[slow - 2] 和当前待处理元素 nums[fast] 是否相同。
    1. 如果 nums[slow - 2] == nums[fast] 时,此时必有 nums[slow - 2] == nums[slow - 1] == nums[fast],则当前 nums[fast] 不保留,直接向右移动快指针 fast
    2. 如果 nums[slow - 2] != nums[fast] 时,则保留 nums[fast]。将 nums[fast] 赋值给 nums[slow] ,同时将 slow 右移。然后再向右移动快指针 fast
  3. 这样 slow 指针左边均为处理好的数组元素,而从 slow 指针指向的位置开始, fast 指针左边都为舍弃的重复元素。
  4. 遍历结束之后,此时 slow 就是新数组的长度。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

三. 双指针题目

对撞指针题目

题号 标题 题解 标签 难度
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 链接 数学、字符串、模拟 简单

四. 滑动窗口基础知识

1. 滑动窗口算法介绍

在计算机网络中,滑动窗口协议(Sliding Window Protocol)是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。我们所要讲解的滑动窗口算法也是利用了同样的特性。

滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个固定长度或不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。

  • 滑动操作:窗口可按照一定方向进行移动。最常见的是向右侧移动。
  • 缩放操作:对于不定长度的窗口,可以从左侧缩小窗口长度,也可以从右侧增大窗口长度。

滑动窗口利用了双指针中的快慢指针技巧,我们可以将滑动窗口看做是快慢指针两个指针中间的区间,也可以将滑动窗口看做是快慢指针的一种特殊形式。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第5张图片

2. 滑动窗口适用范围

滑动窗口算法一般用来解决一些查找满足一定条件的连续区间的性质(长度等)的问题。该算法可以将一部分问题中的嵌套循环转变为一个单循环,因此它可以减少时间复杂度。

按照窗口长度的固定情况,我们可以将滑动窗口题目分为以下两种:

  • 固定长度窗口:窗口大小是固定的。
  • 不定长度窗口:窗口大小是不固定的。
    • 求解最大的满足条件的窗口。
    • 求解最小的满足条件的窗口。

下面来分别讲解一下这两种类型题目。

3. 固定长度滑动窗口

固定长度滑动窗口算法(Fixed Length Sliding Window):在给定数组 / 字符串上维护一个固定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第6张图片

3.1 固定长度滑动窗口算法步骤

假设窗口的固定大小为 w i n d o w ‾ s i z e window\underline{}size windowsize

  1. 使用两个指针 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 = 0 left = 0 left=0 r i g h t = 0 right = 0 right=0,区间 [ l e f t , r i g h t ] [left, right] [left,right] 被称为一个「窗口」。
  2. 当窗口未达到 w i n d o w ‾ s i z e window\underline{}size windowsize 大小时,不断移动 r i g h t right right,先将数组前 w i n d o w ‾ s i z e window\underline{}size windowsize 个元素填入窗口中,即 window.append(nums[right])
  3. 当窗口达到 w i n d o w ‾ s i z e window\underline{}size windowsize 大小时,即满足 right - left + 1 >= window_size 时,判断窗口内的连续元素是否满足题目限定的条件。
    1. 如果满足,再根据要求更新最优解。
    2. 然后向右移动 l e f t left left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 w i n d o w ‾ s i z e window\underline{}size windowsize
  4. 向右移动 r i g h t right right,将元素填入窗口中,即 window.append(nums[right])
  5. 重复 2 ∼ 4 2 \sim 4 24 步,直到 r i g h t right right 到达数组末尾。

3.2 固定长度滑动窗口代码模板

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

下面我们根据具体例子来讲解一下如何使用固定窗口大小的滑动窗口来解决问题。

3.3 大小为 K 且平均值大于等于阈值的子数组数目

3.3.1 题目链接
  • 1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)
3.3.2 题目大意

描述:给定一个整数数组 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 的子数组数目。

说明

  • 1 ≤ a r r . l e n g t h ≤ 1 0 5 1 \le arr.length \le 10^5 1arr.length105
  • 1 ≤ a r r [ i ] ≤ 1 0 4 1 \le arr[i] \le 10^4 1arr[i]104
  • 1 ≤ k ≤ a r r . l e n g t h 1 \le k \le arr.length 1karr.length
  • 0 ≤ t h r e s h o l d ≤ 1 0 4 0 \le threshold \le 10^4 0threshold104

示例

  • 示例 1:
输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4
输出:3
解释:子数组 [2,5,5],[5,5,5][5,5,8] 的平均值分别为 456 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)
  • 示例 2:
输入:arr = [11,13,17,23,29,31,7,5,2,3], k = 3, threshold = 5
输出:6
解释:前 6 个长度为 3 的子数组平均值都大于 5 。注意平均值不是整数。
3.3.3 解题思路
思路 1:滑动窗口(固定长度)

这道题目是典型的固定窗口大小的滑动窗口题目。窗口大小为 k k k。具体做法如下:

  1. a n s ans ans 用来维护答案数目。 w i n d o w ‾ s u m window\underline{}sum windowsum 用来维护窗口中元素的和。
  2. l e f t left left r i g h t right right 都指向序列的第一个元素,即: l e f t = 0 left = 0 left=0 r i g h t = 0 right = 0 right=0
  3. 向右移动 r i g h t right right,先将 k k k 个元素填入窗口中,即 window_sum += arr[right]
  4. 当窗口元素个数为 k k k 时,即满足 right - left + 1 >= k 时,判断窗口内的元素和平均值是否大于等于阈值 t h r e s h o l d threshold threshold
    1. 如果满足,则答案数目加 1 1 1
    2. 然后向右移动 l e f t left left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 k k k
  5. 重复 3 ∼ 4 3 \sim 4 34 步,直到 r i g h t right right 到达数组末尾。
  6. 最后输出答案数目。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n ) O(n) O(n)

4. 不定长度滑动窗口

不定长度滑动窗口算法(Sliding Window):在给定数组 / 字符串上维护一个不定长度的窗口。可以对窗口进行滑动操作、缩放操作,以及维护最优解操作。

Leetcode 01-算法入门与数组-⑤数组双指针&滑动窗口_第7张图片

4.1 不定长度滑动窗口算法步骤

  1. 使用两个指针 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 = 0 left = 0 left=0 r i g h t = 0 right = 0 right=0,区间 [ l e f t , r i g h t ] [left, right] [left,right] 被称为一个「窗口」。
  2. 将区间最右侧元素添加入窗口中,即 window.add(s[right])
  3. 然后向右移动 r i g h t right right,从而增大窗口长度,即 right += 1。直到窗口中的连续元素满足要求。
  4. 此时,停止增加窗口大小。转向不断将左侧元素移出窗口,即 window.popleft(s[left])
  5. 然后向右移动 l e f t left left,从而缩小窗口长度,即 left += 1。直到窗口中的连续元素不再满足要求。
  6. 重复 2 ~ 5 步,直到 r i g h t right right 到达序列末尾。

4.2 不定长度滑动窗口代码模板

left = 0
right = 0

while right < len(nums):
    window.append(nums[right])
    
    while 窗口需要缩小:
        # ... 可维护答案
        window.popleft()
        left += 1
    
    # 向右侧增大窗口
    right += 1

4.3 无重复字符的最长子串

4.3.1 题目链接
  • 3. 无重复字符的最长子串 - 力扣(LeetCode)
4.3.2 题目大意

描述:给定一个字符串 s s s

要求:找出其中不含有重复字符的最长子串的长度。

说明

  • 0 ≤ s . l e n g t h ≤ 5 ∗ 1 0 4 0 \le s.length \le 5 * 10^4 0s.length5104
  • s s s 由英文字母、数字、符号和空格组成。

示例

  • 示例 1:
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3
  • 示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1
4.3.3 解题思路
思路 1:滑动窗口(不定长度)

用滑动窗口 w i n d o w window window 来记录不重复的字符个数, w i n d o w window window 为哈希表类型。

  1. 设定两个指针: l e f t left left r i g h t right right,分别指向滑动窗口的左右边界,保证窗口中没有重复字符。
  2. 一开始, l e f t left left r i g h t right right 都指向 0 0 0
  3. 向右移动 r i g h t right right,将最右侧字符 s [ r i g h t ] s[right] s[right] 加入当前窗口 w i n d o w window window 中,记录该字符个数。
  4. 如果该窗口中该字符的个数多于 1 1 1 个,即 w i n d o w [ s [ r i g h t ] ] > 1 window[s[right]] > 1 window[s[right]]>1,则不断右移 l e f t left left,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 w i n d o w [ s [ r i g h t ] ] ≤ 1 window[s[right]] \le 1 window[s[right]]1
  5. 维护更新无重复字符的最长子串长度。然后继续右移 r i g h t right right,直到 r i g h t ≥ l e n ( n u m s ) right \ge len(nums) rightlen(nums) 结束。
  6. 输出无重复字符的最长子串长度。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( ∣ ∑ ∣ ) O(| \sum |) O()。其中 ∑ \sum 表示字符集, ∣ ∑ ∣ | \sum | 表示字符集的大小。

4.4 长度最小的子数组

4.4.1 题目链接
  • 209. 长度最小的子数组 - 力扣(LeetCode)
4.4.2 题目大意

描述:给定一个只包含正整数的数组 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

说明

  • 1 ≤ t a r g e t ≤ 1 0 9 1 \le target \le 10^9 1target109
  • 1 ≤ n u m s . l e n g t h ≤ 1 0 5 1 \le nums.length \le 10^5 1nums.length105
  • 1 ≤ n u m s [ i ] ≤ 1 0 5 1 \le nums[i] \le 10^5 1nums[i]105

示例

  • 示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
  • 示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
4.4.3 解题思路
思路 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

  1. 一开始, l e f t left left r i g h t right right 都指向 0 0 0
  2. 向右移动 r i g h t right right,将最右侧元素加入当前窗口和 w i n d o w ‾ s u m window\underline{}sum windowsum 中。
  3. 如果 w i n d o w ‾ s u m ≥ t a r g e t window\underline{}sum \ge target windowsumtarget,则不断右移 l e f t left left,缩小滑动窗口长度,并更新窗口和的最小值,直到 w i n d o w ‾ s u m < t a r g e t window\underline{}sum < target windowsum<target
  4. 然后继续右移 r i g h t right right,直到 r i g h t ≥ l e n ( n u m s ) right \ge len(nums) rightlen(nums) 结束。
  5. 输出窗口和的最小值作为答案。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

4.5 乘积小于K的子数组

4.5.1 题目链接
  • 713. 乘积小于K的子数组 - 力扣(LeetCode)
4.5.2 题目大意

描述:给定一个正整数数组 n u m s nums nums 和整数 k k k

要求:找出该数组内乘积小于 k k k 的连续的子数组的个数。

说明

  • 1 ≤ n u m s . l e n g t h ≤ 3 ∗ 1 0 4 1 \le nums.length \le 3 * 10^4 1nums.length3104
  • 1 ≤ n u m s [ i ] ≤ 1000 1 \le nums[i] \le 1000 1nums[i]1000
  • 0 ≤ k ≤ 1 0 6 0 \le k \le 10^6 0k106

示例

  • 示例 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
4.5.3 解题思路
思路 1:滑动窗口(不定长度)
  1. 设定两个指针: l e f t left left r i g h t right right,分别指向滑动窗口的左右边界,保证窗口内所有数的乘积 w i n d o w ‾ p r o d u c t window\underline{}product windowproduct 都小于 k k k。使用 w i n d o w ‾ p r o d u c t window\underline{}product windowproduct 记录窗口中的乘积值,使用 c o u n t count count 记录符合要求的子数组个数。
  2. 一开始, l e f t left left r i g h t right right 都指向 0 0 0
  3. 向右移动 r i g h t right right,将最右侧元素加入当前子数组乘积 w i n d o w ‾ p r o d u c t window\underline{}product windowproduct 中。
  4. 如果 w i n d o w ‾ p r o d u c t ≥ k window\underline{}product \ge k windowproductk,则不断右移 l e f t left left,缩小滑动窗口长度,并更新当前乘积值 w i n d o w ‾ p r o d u c t window\underline{}product windowproduct 直到 w i n d o w ‾ p r o d u c t < k window\underline{}product < k windowproduct<k
  5. 记录累积答案个数加 1 1 1,继续右移 r i g h t right right,直到 r i g h t ≥ l e n ( n u m s ) right \ge len(nums) rightlen(nums) 结束。
  6. 输出累积答案个数。
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

五. 滑动窗口练习题目

1. 0674. 最长连续递增序列

1.1 题目大意

描述:给定一个未经排序的数组 nums

要求:找到最长且连续递增的子序列,并返回该序列的长度。

说明

  • 连续递增的子序列:可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
  • 1 ≤ n u m s . l e n g t h ≤ 1 0 4 1 \le nums.length \le 10^4 1nums.length104
  • − 1 0 9 ≤ n u m s [ i ] ≤ 1 0 9 -10^9 \le nums[i] \le 10^9 109nums[i]109

示例

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为 3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 57 在原数组里被 4 隔开。 

1.2 题目解析

思路 1:动态规划
1. 定义状态

定义状态 dp[i] 表示为:以 nums[i] 结尾的最长且连续递增的子序列长度。

2. 状态转移方程

因为求解的是连续子序列,所以只需要考察相邻元素的状态转移方程。

如果一个较小的数右侧相邻元素为一个较大的数,则会形成一个更长的递增子序列。

对于相邻的数组元素 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] + 1nums[i - 1] < nums[i]

3. 初始条件

默认状态下,把数组中的每个元素都作为长度为 1 的最长且连续递增的子序列长度。即 dp[i] = 1

4. 最终结果

根据我们之前定义的状态,dp[i] 表示为:以 nums[i] 结尾的最长且连续递增的子序列长度。则为了计算出最大值,则需要再遍历一遍 dp 数组,求出最大值即为最终结果。

思路 1:动态规划代码
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)
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)。一重循环遍历的时间复杂度为 O ( n ) O(n) O(n),最后求最大值的时间复杂度是 O ( n ) O(n) O(n),所以总体时间复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n ) O(n) O(n)。用到了一维数组保存状态,所以总体空间复杂度为 O ( n ) O(n) O(n)
思路 2:滑动窗口(不定长度)
  1. 设定两个指针:leftright,分别指向滑动窗口的左右边界,保证窗口内为连续递增序列。使用 window_len 存储当前窗口大小,使用 max_len 维护最大窗口长度。
  2. 一开始,leftright 都指向 0
  3. 将最右侧元素 nums[right] 加入当前连续递增序列中,即当前窗口长度加 1window_len += 1)。
  4. 判断当前元素 nums[right] 是否满足连续递增序列。
  5. 如果 right > 0 并且 nums[right - 1] >= nums[right] ,说明不满足连续递增序列,则将 left 移动到窗口最右侧,重置当前窗口长度为 1window_len = 1)。
  6. 记录当前连续递增序列的长度,并更新最长连续递增序列的长度。
  7. 继续右移 right,直到 right >= len(nums) 结束。
  8. 输出最长连续递增序列的长度 max_len
思路 2:代码
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
思路 2:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

2. 1004. 最大连续1的个数 III

2.1 题目大意

描述:给定一个由 01 组成的数组 nums,再给定一个整数 k。最多可以将 k 个值从 0 变到 1

要求:返回仅包含 1 的最长连续子数组的长度。

说明

  • 1 ≤ n u m s . l e n g t h ≤ 1 0 5 1 \le nums.length \le 10^5 1nums.length105
  • nums[i] 不是 0 就是 1
  • 0 ≤ k ≤ n u m s . l e n g t h 0 \le k \le nums.length 0knums.length

示例

输入: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

2.2 题目解析

思路 1:滑动窗口(不定长度)
  1. 使用两个指针 leftright 指向数组开始位置。使用 max_count 来维护仅包含 1 的最长连续子数组的长度。
  2. 不断右移 right 指针,扩大滑动窗口范围,并统计窗口内 0 元素的个数。
  3. 直到 0 元素的个数超过 k 时将 left 右移,缩小滑动窗口范围,并减小 0 元素的个数,同时维护 max_count
  4. 最后输出最长连续子数组的长度 max_count
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

3. 0220. 存在重复元素 III

3.1 题目大意

描述:给定一个整数数组 nums,以及两个整数 kt

要求:判断数组中是否存在两个不同下标的 ij,其对应元素满足 abs(nums[i] - nums[j]) <= t,同时满足 abs(i - j) <= k。如果满足条件则返回 True,不满足条件返回 False

说明

  • 0 ≤ n u m s . l e n g t h ≤ 2 ∗ 1 0 4 0 \le nums.length \le 2 * 10^4 0nums.length2104
  • − 2 31 ≤ n u m s [ i ] ≤ 2 31 − 1 -2^{31} \le nums[i] \le 2^{31} - 1 231nums[i]2311
  • 0 ≤ k ≤ 1 0 4 0 \le k \le 10^4 0k104
  • 0 ≤ t ≤ 2 31 − 1 0 \le t \le 2^{31} - 1 0t2311

示例

输入:nums = [1,2,3,1], k = 3, t = 0
输出:True


输入:nums = [1,0,1,1], k = 1, t = 2
输出:True

3.2 题目解析

题目中需要满足两个要求,一个是元素值的要求(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 的方法。有两种思路:「桶排序」和「滑动窗口(固定长度)」。

思路 1:桶排序
  1. 利用桶排序的思想,将桶的大小设置为 t + 1。只需要使用一重循环遍历位置 i,然后根据 nums[i] // (t + 1),从而决定将 nums[i] 放入哪个桶中。
  2. 这样在同一个桶内各个元素之间的差值绝对值都小于等于 t。而相邻桶之间的元素,只需要校验一下两个桶之间的差值是否不超过 t。这样就可以以 O ( 1 ) O(1) O(1) 的时间复杂度检测相邻 2 * k 个元素是否满足 abs(nums[i] - nums[j]) <= t
  3. abs(i - j) <= k 条件则可以通过在一重循环遍历时,将超出范围的 nums[i - k] 从对应桶中删除,从而保证桶中元素一定满足 abs(i - j) <= k

具体步骤如下:

  1. 将每个桶的大小设置为 t + 1。我们将元素按照大小依次放入不同的桶中。
  2. 遍历数组 nums 中的元素,对于元素 nums[i]
    1. 如果 nums[i] 放入桶之前桶里已经有元素了,那么这两个元素必然满足 abs(nums[i] - nums[j]) <= t
    2. 如果之前桶里没有元素,那么就将 nums[i] 放入对应桶中。
    3. 再判断左右桶的左右两侧桶中是否有元素满足 abs(nums[i] - nums[j]) <= t
    4. 然后将 nums[i - k] 之前的桶清空,因为这些桶中的元素与 nums[i] 已经不满足 abs(i - j) <= k 了。
  3. 最后上述满足条件的情况就返回 True,最终遍历完仍不满足条件就返回 False
思路 1:代码
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
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n) n n n 是给定数组长度。
  • 空间复杂度 O ( m i n ( n , k ) ) O(min(n, k)) O(min(n,k))。桶中最多包含 m i n ( n , k + 1 ) min(n, k + 1) min(n,k+1) 个元素。
思路 2:滑动窗口(固定长度)
  1. 使用一个长度为 k 的滑动窗口,每次遍历到 nums[right] 时,滑动窗口内最多包含 nums[right] 之前最多 k 个元素。只需要检查前 k 个元素是否在 [nums[right] - t, nums[right] + t] 区间内即可。
  2. 检查 k 个元素是否在 [nums[right] - t, nums[right] + t] 区间,可以借助保证有序的数据结构(比如 SortedList)+ 二分查找来解决,从而减少时间复杂度。

具体步骤如下:

  1. 使用有序数组类 window 维护一个长度为 k 的窗口,满足数组内元素有序,且支持增加和删除操作。
  2. leftright 都指向序列的第一个元素。即:left = 0right = 0
  3. 将当前元素填入窗口中,即 window.add(nums[right])
  4. 当窗口元素大于 k 个时,即 right - left > k,移除窗口最左侧元素,并向右移动 left
  5. 当窗口元素小于等于 k 个时:
    1. 使用二分查找算法,查找 nums[right]window 中的位置 idx
    2. 判断 window[idx] 与相邻位置上元素差值绝对值,若果满足 abs(window[idx] - window[idx - 1]) <= t 或者 abs(window[idx + 1] - window[idx]) <= t 时返回 True
  6. 向右移动 right
  7. 重复 3 ~ 6 步,直到 right 到达数组末尾,如果还没找到满足条件的情况,则返回 False
思路 2:代码
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
思路 2:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 ( m i n ( n , k ) ) ) O(n \times \log_2(min(n, k))) O(n×log2(min(n,k)))
  • 空间复杂度 O ( m i n ( n , k ) ) O(min(n, k)) O(min(n,k))

六. 滑动窗口题目

固定长度窗口题目

题号 标题 题解 标签 难度
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 绝对差不超过限制的最长连续子数组 队列、数组、有序集合、滑动窗口、单调队列、堆(优先队列) 中等

你可能感兴趣的:(LeetCode,算法,leetcode,python,数据结构)