数组双指针和数组窗口

0. 内容说明

最近在自己编写一些小的算法的时候,深感自己的算法过于臃肿。碰巧Datawhale在新的一期组队学习中组织了数据结构与算法的课程学习。于是就参加了,再次感谢Datawhale~~

首先跟大家分享一下两个自己感觉比较好的学习资料,一个是 算法通关手册 ,也是Datawhale在本次组队学习中的学习资料;一个是B站上的视频 【北京大学】数据结构与算法Python版(完整版),老师讲的特别棒(也难得有Python版的数据结构课程,哈哈~)。

需要指出的是:本次博客的内容更像是对上述两个资料做的笔记,很多都是资料上的原内容,并非原创。

1. 双指针简介

双指针(Two Pointers):指的是在遍历元素的过程中,不是使用单个指针进行访问,而是使用两个指针进行访问,从而达到相应的目的。如果两个指针方向相反,则称为对撞时针。如果两个指针方向相同,则称为快慢指针。如果两个指针分别属于不同的数组 / 链表,则称为分离双指针。在数组的区间问题上,暴力算法的时间复杂度往往是 O ( n 2 ) O(n^2) O(n2)。而双指针利用了区间「单调性」的性质,从而将时间复杂度降到了 O ( n ) O(n) O(n)

1.1 对撞指针

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

例题1

  • 题目

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

  • 要求

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

  • 思路

    使用两个指针 left,right。left 指向数组第一个值最小的元素位置,right 指向数组值最大元素位置。判断两个位置上的元素的和与目标值的关系。如果元素和等于目标值,则返回两个元素位置。如果元素和大于目标值,则让 right 左移,继续检测。如果元素和小于目标值,则让 left 右移,继续检测。直到 left 和 right 移动到相同位置停止检测。如果最终仍没找到,则返回 [-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]

例题2

  • 题目

    验证回文串。

  • 要求

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

  • 思路

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

代码:

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

例题3

  • 题目

    给定 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 条垂直线,垂直线 i 的两个端点分别为 ( i , a i ) (i,a_i) (i,ai) ( i , 0 ) (i,0) (i,0)

  • 要求

    找出其中的两条垂直线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

  • 思路

    使用两个指针 left,right。left 指向数组开始位置,right 指向数组结束位置。计算 left 和 right 所构成的面积值,同时维护更新最大面积值。判断 left 和 right 的高度值大小。如果 left 指向的直线高度比较低,则将 left 指针右移。如果 right 指向的直线高度比较低,则将 right 指针左移。如果遇到 left == right,跳出循环,最后返回最大的面积。

代码:

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.2 快慢指针

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

例题1

  • 题目

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

  • 要求

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

  • 思路

    定义两个快慢指针 slow,fast。其中 slow 指向去除重复元素后的数组的末尾位置。fast 指向当前元素。令 slow 在后, fast 在前。令 slow = 0,fast = 1。比较 slow 位置上元素值和 fast 位置上元素值是否相等。如果不相等,则将 slow 后移一位,将 fast 指向位置的元素复制到 slow 位置上。将 fast 右移 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.3 分离双指针

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

例题1

  • 题目

    两个数组的交集。

  • 要求

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

  • 思路

    对数组 nums1、nums2 先排序。使用两个指针 left_1、left_2。left_1 指向第一个数组的第一个元素,即:left_1 = 0,left_2 指向第二个数组的第一个元素,即:left_2 = 0。如果 nums1[left_1] 等于 nums2[left_2],则将其加入答案数组(注意去重),并将 left_1 和 left_2 右移。

  • 代码:

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

2. 滑动窗口简介

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

2.1 固定长度的滑动窗口

固定长度的滑动窗口的窗宽是不变的。

例题1

  • 题目

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

  • 要求

    返回长度为 k 且平均值大于等于 threshold 的子数组数目。

  • 思路

    ans 用来维护答案数目。window_sum 用来维护窗口中元素的和。
    left 、right 都指向序列的第一个元素,即:left = 0,right = 0。当窗口元素个数不足 k 个时,不断移动 right,先将 k 个元素填入窗口中。当窗口元素个数为 k 时,即:right - left + 1 >= k 时,判断窗口内的元素和平均值是否大于等于阈值。如果满足,则答案数目 + 1。然后向右移动 left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 k。向右移动 right,将元素填入窗口中。重复 3 ~ 5 步,直到 right 到达数组末尾。

  • 代码:
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

2.2 不定长度的滑动窗口

不定长度的滑动窗口的窗宽是变动的。

例题1

  • 题目

    无重复字符的最长子串 。

  • 要求

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

  • 思路

    一开始,left、right 都指向 0。将最右侧字符 s[right] 加入当前窗口 window 中,记录该字符个数。如果该窗口中该字符的个数多于 1 个,即 window[s[right]] > 1,则不断右移 left,缩小滑动窗口长度,并更新窗口中对应字符的个数,直到 window[s[right]] <= 1。维护更新无重复字符的最长子串长度。然后右移 right,直到 right >= len(nums) 结束。输出无重复字符的最长子串长度。

  • 代码:
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

例题2

  • 题目

    长度最小的子数组。

  • 要求

    给定一个只包含正整数的数组 nums 和一个正整数 target。找出数组中满足和大于等于 target 的长度最小的「连续子数组」,并返回其长度。如果不存在符合条件的子数组,返回 0。

  • 思路

    一开始,left、right 都指向 0。向右移动 right,将最右侧元素加入当前窗口和 window_sum 中。如果 window_sum >= target,则不断右移 left,缩小滑动窗口长度,并更新窗口和的最小值,直到 window_sum < target。然后继续右移 right,直到 right >= len(nums) 结束。输出窗口和的最小值作为答案。

  • 代码:
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

例题3

  • 题目

    乘积小于K的子数组。

  • 要求

    给定一个正整数数组 nums和整数 k 。找出该数组内乘积小于 k 的连续的子数组的个数。

  • 思路

    一开始,left、right 都指向 0。将最右侧元素加入当前子数组乘积 window_product 中。如果 window_product >= k ,则不断右移 left,缩小滑动窗口长度,并更新当前乘积值 window_product 直到 window_product < k。记录累积答案个数 += 1,向右移动 right,直到 right >= len(nums) 结束。输出累积答案个数。

  • 代码:
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

你可能感兴趣的:(数据结构与算法,数据结构,力扣,算法,python)