二分查找、双指针、滑动窗口算法技巧总结

文章目录

  • 一、数组二分查找
    • 1.算法介绍
    • 2.算法过程
    • 3.代码模板
    • 4.细节处理
  • 二、数组双指针
    • 1.左右指针
      • 1.1求解步骤
      • 1.2案例实现
    • 2.快慢指针
      • 2.1求解步骤
      • 2.2案例实现
  • 三、滑动窗口
    • 1.算法介绍
    • 2.适用范围
    • 3.固定长度窗口
      • 3.1固定长度窗口求解步骤
      • 3.2案例实现
    • 4.不定长度窗口
      • 4.1 不定长度窗口求解步骤
      • 4.2 案例实现

一、数组二分查找

1.算法介绍

二分查找法(Binary Search)算法,也叫折半查找算法。二分查找针对的是一个有序的数据集合,查找思想有点类似于分治思想。先确定待查找元素所在的区间范围,在逐步缩小范围,直到找到元素或找不到该元素为止。

2.算法过程

二分查找算法的过程如下所示:

  • 每次查找时从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;
  • 如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
  • 如果在某一步骤数组为空,则代表找不到。

3.代码模板

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        l,r = 0,len(nums)-1
        while l <= r:
            mid = (l+r)//2
            if nums[mid] > target:
                r = mid - 1
            elif nums[mid] < target:
                l = mid + 1
            else:
                return mid
        return -1

4.细节处理

区间开闭、mid取值、出界条件问题

区间的左闭右闭、左闭右开指的是初始待查找区间的范围。

  • 左闭右闭:初始化赋值时,left = 0right = len(nums) - 1left 为数组第一个元素位置,right 为数组最后一个元素位置,从而区间 [left, right] 左右边界上的点都能取到。
  • 左闭右开:初始化赋值时,left = 0right = len(nums)left 为数组第一个元素位置,right 为数组最后一个元素的下一个位置,从而区间 [left, right) 左边界点能取到,而右边界上的点不能取到。

最常见的 mid 取值就是 mid = (left + right) // 2 或者 mid = left + (right - left) // 2 。前者是最常见写法,后者是为了防止整型溢出

  • 如果判断语句为 left <= right,且查找的元素不存在,则 while 判断语句出界条件是 left == right + 1,写成区间形式就是 [right + 1, right],此时待查找区间为空,待查找区间中没有元素存在,所以此时终止循环可以直接返回 -1 是正确的。
    • 比如说区间 [3, 2],不可能存在一个元素既大于等于 3 又小于等于 2,此时直接终止循环,返回 -1 即可。
  • 如果判断语句为left < right,且查找的元素不存在,则 while 判断语句出界条件是 left == right,写成区间形式就是 [right, right]。此时区间不为空,待查找区间还有一个元素存在,并不能确定查找的元素不在这个区间中,此时终止循环返回 -1 是错误的。

二、数组双指针

1.左右指针

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

1.1求解步骤

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

1.2案例实现

【LeetCode】11.盛最多水的容器

给定 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i,ai)。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i,ai) 和 (i,0)。

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

二分查找、双指针、滑动窗口算法技巧总结_第1张图片
示例:

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

解题思路

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

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

  • 使用两个指针 leftrightleft 指向数组开始位置,right 指向数组结束位置。
  • 计算 leftright 所构成的面积值,同时维护更新最大面积值。
  • 判断leftright的高度值大小。
    • 如果 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

2.快慢指针

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

2.1求解步骤

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

2.2案例实现

【LeetCode】26.删除有序数组中的重复项

给定一个有序数组 nums

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

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

示例:

输入: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 。不需要考虑数组中超出新长度后面的元素。

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

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

  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 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.算法介绍

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

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

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

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

2.适用范围

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

根据问题,我们可以将滑动窗口分为以下两种:

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

3.固定长度窗口

3.1固定长度窗口求解步骤

假设窗口的固定大小为 window_size

  1. 使用两个指针 leftright。初始时,leftright 都指向序列的第一个元素,即:left = 0right = 0 ,区间 [left, right] 被称为一个「窗口」。
  2. 当窗口未达到 window_size 大小时,不断移动 right,先将 window_size 个元素填入窗口中。
  3. 当窗口达到window_size大小时,判断窗口内的连续元素是否满足题目限定的条件。
    1. 如果满足,再根据要求更新最优解。
    2. 然后向右移动 left,从而缩小窗口长度,即 left += 1,使得窗口大小始终保持为 window_size
  4. 向右移动 right,将元素填入窗口中。
  5. 重复 2 ~ 4 步,直到 right 到达序列末尾。

3.2案例实现

【LeetCode】1343. 大小为 K 且平均值大于等于阈值的子数组数目

给你一个整数数组 arr 和两个整数 kthreshold

请你返回长度为 k 且平均值大于等于 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 的值)。

题解:

class Solution:
    def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
        left = right = 0
        ans = 0
        window_sum = 0

        while right < len(arr):
            window_sum += arr[right]    

            if right - left + 1 >= k:    #判断窗口长度
                if window_sum >= k * threshold:       #直接将窗口内所有数相加去与k和threshold的乘积去比较,便能得到时候大于平均值,同时相等也应该输出
                    ans += 1
                window_sum -= arr[left]
                left += 1     #left加1和right加1便是在将窗口向前移动
            right += 1

        return ans

4.不定长度窗口

4.1 不定长度窗口求解步骤

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

4.2 案例实现

【LeetCode】3. 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 **最长子串 **的长度。

示例:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

题解:

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        left=right = 0
        ans = 0
        window = dict()

        while right<len(s):

            if s[right] not in window:
                window[s[right]] = 1
            else:
                window[s[right]] += 1
            
            while window[s[right]]>1:     #当字符存在2个时,说明右边新加入了一个,那左边就将删去一个,重新保持不重复
                window[s[left]] -= 1
                left += 1
            ans = max(ans,right-left+1)   #取最大的数,最开始是len(s),通过窗口滑动后,找到所有长度中最长的
            right += 1

        return ans

你可能感兴趣的:(刷题记录-解题题解,python,算法)