数组双指针、滑动窗口

学习资料来源:

【算法通关手册】:https://algo.itcharge.cn/

【datawhale组队学习】https://github.com/datawhalechina/team-learning

大多是对这次组队学习的学习资料的学习笔记,非原创~

另外时间原因滑动窗口部分暂时还没有时间写博客,之后会补上~

数组双指针

双指针简介

双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。

  • 对撞时针:两个指针方向相反
  • 快慢指针:两个指针方向相同
  • 分离双指针:两个指针分别属于不同的数组 / 链表

在数组的区间问题上,暴力算法的时间复杂度往往是 O ( n 2 ) O(n^2) O(n2)

而双指针利用了区间「单调性」的性质,从而将时间复杂度降到了 O ( n ) O(n) O(n)

对撞指针

指的是两个指针 left、right 分别指向序列第一个元素和最后一个元素,然后 left 指针不断递增,right 不断递减,直到两个指针的值相撞(即 left == right),或者满足其他要求的特殊条件为止。

对撞指针求解步骤

  • 两个指针left,rightleft指向序列第一个元素,即:left = 0right指向序列最后一个元素,即:right = len(nums) - 1
  • 在循环体中将左右指针相向移动,当满足一定条件时,将左指针右移,left += 1。当满足另外一定条件时,将右指针左移,right -= 1
  • 知道两指针相撞(即:left == right),或者满足其他要求的特殊条件时,跳出循环体。

对撞指针伪代码模板

left = 0
right = len(nums) - 1

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

对撞指针适用范围

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

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

两数之和Ⅱ-输入有序数组

题目链接

  • https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/

题目大意

给定一个升序排列的整数数组:numbers和一个目标值target

要求:从数组中找出满足相加之和等于target的两个数,并返回两个数在数组下的标值。

注意:数组下标从1开始计数。

解题思路

  1. 暴力求解 O ( n 2 ) O(n^2) O(n2):
# 超时
def twoSum(self, numbers, target):
    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. 对撞指针
    有序数组——双指针
  • 使用两个指针left,rightleft指向数组第一个值最小的元素位置,right指向数组值最大元素的位置。
  • 判断两个位置上的元素的和与目标值的关系。
    • 如果元素和等于目标值,则返回两个元素位置。
    • 如果元素和大于目标值,则让 right 左移,继续检测。
    • 如果元素和小于目标值,则让 left 右移,继续检测。
  • 直到 leftright 移动到相同位置停止检测。
  • 如果最终仍没找到,则返回 [-1, -1]
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]
        '''

        '''
        有序数组——对撞指针
        首尾相加——考虑能让两数之和变化最小幅度的移动方向
            最小变大:右移首
            最大变小:左移尾
        因为是两数之和,不能2*自己,所以left=right是一定要退出循环了
        这次的数据全都可以找见对应的两个数,所以最后不用写return [-1, -1]也行
        '''
        left = 0
        right = len(numbers) - 1
        while left < right:
            if numbers[left] + numbers[right] == target:
                return [left + 1,right + 1]
            elif numbers[left] + numbers[right] < target:
                left += 1
            else:
                right -= 1
        return [-1, -1]

验证回文串

题目链接

  • 125.验证回文串:https://leetcode-cn.com/problems/valid-palindrome/

题目大意

给定一个字符串s

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

  • 回文串:正着读和反着读都一样的字符串。

解题思路

使用对撞指针求解。具体步骤如下:

  • 使用两个指针 leftrightleft 指向字符串开始位置,right 指向字符串结束位置。
  • 判断两个指针对应字符是否是字母或数字。 通过 left 右移、right 左移的方式过滤掉字母和数字以外的字符。
  • 然后判断 s[start] 是否和 s[end] 相等(注意大小写)。
  • 如果相等,则将 left 右移、right 左移,继续进行下一次过滤和判断。
  • 如果不相等,则说明不是回文串,直接返回 False
  • 如果遇到 left == right,跳出循环,则说明该字符串是回文串,返回 True

代码

class Solution:
    def isPalindrome(self, s: str) -> bool:
        '''
            正好用对撞指针挨个判断即可
            注意点:
                只考虑字母和数字字符————忽略空格和标点符号.isalnum()。
                忽略字母的大小写————统一.upper()或者.lower()
            1. 首先判断left和right是否是数字和字母,是的话再判断
            2. 只剩一个元素不用影响对称,进入循环条件写为:left < right

        '''
        left = 0
        right = len(s) - 1
        # s = s.upper()
        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
True

盛最多水的容器

题目链接

    1. 盛最多水的容器https://leetcode-cn.com/problems/container-with-most-water/

题目大意

给定 n个非负整数 α 1 , α 2 , . . . , α n \alpha_1,\alpha_2,...,\alpha_n α1,α2,...,αn ,每个数代表坐标中的一个点 ( i , α i ) (i,\alpha_i) (i,αi)。在坐标内画 n条垂直线,垂直线 i的两个端点分别为 ( i , α i ) (i,\alpha_i) (i,αi) ( i , 0 ) (i,0) (i,0)

要求:找出其中的两条垂直线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
在这里插入图片描述

解题思路

  • 容量=左右两端直线中较低直线的高度 * 两端直线之间的距离——目标是较低直线的高度尽可能高
    可以使用双指针求解。移动较低直线所在的指针位置,从而得到不同的高度和面积,最终获取其中最大的面积。
  1. 使用两个指针 leftrightleft 指向数组开始位置,right 指向数组结束位置。
  2. 计算 leftright 所构成的面积值,同时维护更新最大面积值。
  3. 判断 leftright 的高度值大小。
  4. 如果 left 指向的直线高度比较低,则将 left 指针右移。
  5. 如果 right 指向的直线高度比较低,则将 right 指针左移。
  6. 如果遇到 left == right,跳出循环,最后返回最大的面积。

代码

class Solution:
    def maxArea(self, height: List[int]) -> int:
        '''
            面积 = 左右两端直线中较低直线的高度 * 两端直线之间的距离
            1. 从最大距离开始
            2. 存一个暂时量ans,以免移动后面积更小了。
            3. 移动较短直线
                因为目的是找最大面积
                移动短线可能会使面积变大(高度>=<原来,距离<原来)
                但是移动长线面积一定变小(高度<=原来,距离<原来)

        '''
        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)」。两个指针以不同速度、不同策略移动,直到快指针移动到数组尾端,或者两指针相交,或者满足其他特殊条件时为止。

快慢指针求解步骤

  • 使用两个指针slowfase
    1. slow一般指向序列第一个元素,即:slow = 0
    2. fast一般指向序列第二个元素,即:fast = 1
  • 这里需要注意的是左右指针移动方向都是一样的【向右】
    1. 当满足一定条件时,将慢指针右移,即 slow += 1
    2. 当满足另外一定条件时(也可能不需要满足条件),将快指针右移,即 fast += 1
  • 到快指针移动到数组尾端(即 fast == len(nums) - 1),或者两指针相交,或者满足其他特殊条件时跳出循环体。

快慢指针伪代码模板

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

快慢指针适用范围

  • 移动元素
  • 删除元素
  • 判断链表中是否有环
  • 判断链表的长度问题

删除有序数组中的重复项

题目链接

  • https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/

题目大意

给定一个有序数组nums

  • 要求:删除nums中的重复元素,使每个元素只出现一次。并输出去除重复元素之后数组的长度。
  • 注意:不能使用额外的数组空间,在原地修改数组,并在使用 额外空间的条件下完成。

解题思路

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

  • 删除重复元素,实际上就是将不重复的元素移到数组左侧。

  • 具体做法:

    1. 定义两个快慢指针 slowfast。其中 slow 指向去除重复元素后的数组的末尾位置。fast 指向当前元素。
    2. slow 在后, fast 在前。令 slow = 0fast = 1
    3. 比较 slow 位置上元素值和 fast 位置上元素值是否相等。
      • 如果不相等,则将 slow 后移一位,将 fast 指向位置的元素复制到 slow位置上。
    4. fast 右移1 位。
    • 重复3~4,直到fast等于数组长度。
    • 返回slow + 1即为新数组长度。

代码

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        '''
            思路就是:
                if 有重复:
                    1. 慢指针先按兵不动
                    2. 快指针冲到重复结束(新元素出现的地方)
                        此时慢指针往前走一步(跨过一个重复元素,搞后面的)
                        重复的元素被新元素赋值

                        总之————slow就等着新元素出现再往前迈一步收了新元素
        '''
        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_1left_2
    1. left_1 指向第一个数组 / 链表的第一个元素,即:left_1 = 0
    2. left_2 指向第二个数组 / 链表的第一个元素,即:left_2 = 0
  • 当满足一定条件时,两个指针同时右移,即 left_1 += 1left_2 += 1
  • 当满足另外一定条件时,将 left_1 指针右移,即 left_1 += 1
  • 当满足其他一定条件时,将 left_2 指针右移,即 left_2 += 1
  • 当其中一个数组 / 链表遍历完时或者满足其他特殊条件时跳出循环体。

分离双指针伪代码模板

left_1 = 0
left_2 = 0

while left_1 < len(nums1) and left_2 < len(nums2):
    if 一定条件 1:
        left_1 += 1
        left_2 += 2
    elif 一定条件 2:
        left_1 += 1
    elif 一定条件 3:
        left_2 += 1

分离双指针使用范围

  • 有序数组合并
  • 求交集、并集问题

两个数组的交集

题目链接

  • https://leetcode-cn.com/problems/intersection-of-two-arrays/

题目大意

给定两个数组 nums1nums2

要求:编写一个函数来计算它们的交集。重复元素只计算一次。

解题思路

使用分离双指针求解,具体步骤如下:

  • 对数组 nums1nums2 先排序。
  • 使用两个指针 left_1left_2
    1. left_1 指向第一个数组的第一个元素,即:left_1 = 0
    2. left_2 指向第二个数组的第一个元素,即:left_2 = 0
  • 如果 nums1[left_1] 等于 nums2[left_2],则将其加入答案数组(注意去重),并将 left_1left_2 右移。
  • 如果 nums1[left_1] 小于 nums2[left_2],则将 left_1 右移。
  • 如果 nums1[left_1] 大于 nums2[left_2],则将 left_2 右移。
  • 最后输出答案数组。

代码

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        '''
            list.sort()——对列表从小到大进行排序
            res——用来存放答案数组
            if nums1[left_1] not in res——注意 `not in` 的用法
        '''
        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

双指针总结

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值