数据结构与算法----复习Part 5(二分查找、双指针、滑动窗口)

这是算法通关手册LeetCode的学习笔记,介绍了三种算法。二分查找通过反复减半查找范围来定位元素;双指针分为对撞、快慢和分离指针,用于不同场景;滑动窗口可进行滑动和缩放操作,有固定和不定长度两种类型,每种算法都配有LeetCode例题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列是算法通关手册LeeCode的学习笔记

算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)

本系列为自用笔记,如有版权问题,请私聊我删除。

目录

一,二分查找(Binary Search Algorithm)

二分查找例题

二分查找总结:

二,双指针(Two Pointers)

1. 对撞指针

  2. 快慢指针       

  3. 分离双指针

双指针总结

三,滑动窗口(Sliding Window)

1. 固定长度滑动窗口

2. 不定长度滑动窗口

滑动窗口总结


一,二分查找(Binary Search Algorithm)

        算法思想:通过确定目标元素所在的区间范围,反复将查找范围减半,直到找到元素或找不到元素为止。

        步骤:

        初始化:确定有序线性表;

        确定查找范围,即左右边界 left, right;

        计算中间位置元素 mid = left + (left + right) // 2;

        比较中间位置元素与目标元素,确定下一次搜索范围; 

        重复直到找到 mid 位置或 left > right

代码实现:

    def binary2Search(self, nums: [int], target: int) -> int:
        left, right = 0, len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            elif target < nums[mid]:
                right = mid -1
            else:
                left = mid + 1
        return -1

        上述代码中,使用查找的思路,如果找到了 mid 则立即返回,如果跑完循环也没找到,则返回 -1,每次查找的搜索区间也不相同,因为已经对 mid 位置元素进行判断,所以可以排除 mid 位置的元素,缩小查找区间。

间接法代码实现:

class Solution:
    def binarySearch(self, nums: [int], target: int) -> int:
        left, right = 0, len(nums) - 1

        while left < right:
            mid = left + (right - left) // 2

            if target > nums[mid]:
                left = mid + 1
            else:
                right = mid
        return left if nums[left] == target else -1

         与图中的过程不同。上述是二分查找的排除法思路,可以看到在运行过程中,我们在不断排除没有目标元素的区间,从而达到不断减少搜索范围的目的,直到 left = right 时才退出循环,判断该位置元素是否与目标元素相等。

        代码中采用 mid = left + (right - left) // 2 的方式,是为了避免整形溢出。

特别的:

array = [1, 2, 2, 2, 2, 3, 4, 5, 5]

def binary1(nums, t):
    left = 0
    right = len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] < t:
            left = mid + 1
        else:
            right = mid
    return left

def binary2(nums, t):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = left + (right - left + 1) // 2
        if nums[mid] > t:
            right = mid - 1
        else:
            left = mid
    return left

print(binary1(array, 2))
print(binary2(array, 2))

# 1
# 4

在binary1中 ,若array长度为 6 ,则 left = 0, right = 5

                        mid = 0 + (5 - 0) // 2 = 2

         mid 位置为中间两个元素靠左的那个

                如果当前位置的元素 nums[mid] < t 则目标位置一定在当前位置的右侧,当前位置已经

        检查过了, left = mid + 1。

                如果当前位置的元素nums[mid] >= t ,则移动右边界

                因为左边界是从 0 位置开始的,所以这样可以保证 left 指向目标元素第一次出现的位置

                如果在等于时也移动左边界,那么 right 会一直停在大于目标元素的位置。

同理在binary2中,left = 0, right = 5

                        mid = 0 + (5 - 0 + 1) // 2 = 3

        mid 位置为中间两个元素靠右的那个

                如果当前位置的元素 nums[mid] > t 则目标位置一定在当前位置的左侧,当前位置已经

        检查过了, right = mid - 1。

                如果当前位置的元素nums[mid] <= t,则移动左边界

                因为右边界是为最右侧开始的,这样可以保证 left 指向目标元素最后一次出现的位置。

 可见例题

                34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

二分查找例题

1,33. 搜索旋转排序数组 - 力扣(LeetCode)

对于旋转数组问题,取得中间位置元素后,有两种情况

第一种:

nums[mid] > nums[left]

此时若 target < nums[left] 或 target > nums[mid]

则不在左侧, left = mid + 1

否则 right = mid

第二种:

nums[mid] < nums[left]

此时若 target < nums[mid] 或 target > nums[right] 

则不在右侧,right = mid

否则 left = mid + 1

通过代码如下:

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

2,278. 第一个错误的版本 - 力扣(LeetCode)

第二题是一道经典的二分查找

# The isBadVersion API is already defined for you.
# def isBadVersion(version: int) -> bool:

class Solution:
    def firstBadVersion(self, n: int) -> int:
        left = 0
        right = n
        while left < right:
            mid = left + (right - left) // 2
            if not isBadVersion(mid):
                left = mid + 1
            else:
                right = mid
        return left
        

3,852. 山脉数组的峰顶索引 - 力扣(LeetCode)

4,74. 搜索二维矩阵 - 力扣(LeetCode) 

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        def binarySearch(nums, target: int) -> bool:
            print(nums)
            left = -1
            right = len(nums)
            while left + 1 < right:
                mid = left + (right - left) // 2
                if nums[mid] < target:
                    left = mid
                else:
                    right = mid
            return nums[right] == target

        for length in matrix:
            print(length)
            print(length[0], length[-1])
            # 如果target 在该行范围内,二分搜索
            if length[0] <= target and length[-1] >= target:
                return binarySearch(length, target)
        return False

二分查找总结:

        二分查找的思想非常简单,但在使用时会遇到各种各样的问题,最容易遇到的问题时陷入死循环或是对边界的处理,在这里我强烈推荐up的视频

二分查找为什么总是写错?_哔哩哔哩_bilibili

        而在确保语法正确的前提下,二分查找的难点是 大于号、小于号的判断条件。

二,双指针(Two Pointers)

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

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

1. 对撞指针

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

        步骤:

        left = 0 左指针指向第一个元素, right = len(nums) - 1 右指针指向最后一个元素;

        满足条件时 left +=1 或 right -= 1;

        直到两指针相撞或其他条件时,跳出循环。

伪代码模板:

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

while left < right:
    if 满足条件:
        return 符合要求的答案
    elif 条件1:
        left += 1
    elif 条件2:
        right -= 1

return 无满足条件的值

例题:

1.   167. 两数之和 II - 输入有序数组 - 力扣(LeetCode)

        可以使用对撞指针,因为数组是有序的,所以先将 left = 0,指向最小元素       

        将 right = len(nums) - 1,指向最大元素;

        如果两数之和大于目标值,则将右指针减一;

        如果两数之和小于目标值,则将左指针加一;

        返回结果

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0
        right = len(numbers) - 1
        while left < right:
            ans = numbers[left] + numbers[right]
            if ans == target:
                return [left + 1, right + 1]
            elif ans < target:
                left += 1
            else:
                right -= 1
        return [-1, -1]

  2. 125. 验证回文串 - 力扣(LeetCode)          

        可以使用对撞指针,如果当前元素不是字母,则跳过

        

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. 11. 盛最多水的容器 - 力扣(LeetCode)                          

        因为盛水的多少取决于较短的板,因此在 left 较低时,移动 left 可能会使容量变大,而移动right 如果新的 right - 1 的值大于 right,则容量取决于 left,若新的 right - 1 的值小于 left,容量变小,因此移动的条件是,指向高度较短时,移动。

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left = 0
        right = len(height) - 1
        ans = 0
        while left < right:
            h = min(height[left], height[right])
            v = h * (right - left)
            ans = max(ans, v)
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return ans

  2. 快慢指针       

        两个指针从同一侧开始遍历序列,且移动的步长一个快一个慢,移动快的为 fast,移动慢的为 slow。两个指针以不同速度,不同策略移动,直到快指针移动到数组尾端或两指针相交。

        步骤:

        slow = 0,慢指针指向序列第一个元素,fast = 1,快指针指向序列第二个元素;

        在循环体中依据相应策略移动快慢指针;

        满足条件时,跳出循环体。

伪代码模板:

slow = 0
fast = 1

while 没有遍历完:
    if 满足条件:
        slow += 1
    fast += 1
return 结果

例题:

1. 26. 删除有序数组中的重复项 - 力扣(LeetCode)                                                                                 fast 用于检查是否与前一个元素相同,slow 记录修改元素的位置

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if len(nums) == 1:
            return 1
        slow = 1    # 指向第一个可能被修改的位置
        fast = 1
        while fast < len(nums):
            if nums[fast - 1] != nums[fast]:
                nums[slow] = nums[fast]
                slow += 1
            fast += 1
        return slow

  3. 分离双指针

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

        步骤:

        使用两个指针,left1 = 0,指向第一个数组起始元素,left2 = 0 指向第二个数组起始元素;

        满足一定条件时,两个指针同时右移;

        满足某条件1,left1 += 1;

        满足某条件2,right += 1;

        其中一个数组遍历完或满足条件时退出。

伪代码模板:

left_1 = 0
left_2 = 0

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

例题:

1. 349. 两个数组的交集 - 力扣(LeetCode)

        将两个数组排序后,使用分离指针遍历数组,将符合条件的元素加入到返回数组中

class Solution:
    def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
        res = []
        left1 = 0
        left2 = 0
        nums1.sort()
        nums2.sort()
        while left1 < len(nums1) and left2 < len(nums2):
            if nums1[left1] == nums2[left2]:
                if nums1[left1] not in res:
                    res.append(nums1[left1])
            if nums1[left1] < nums2[left2]:
                left1 += 1
            else:
                left2 += 1
        return res

双指针总结

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

        对撞指针,两个指针方向相反。适合解决查找有序数组中满足某些约束条件的一组元素问题、字符串反转问题。

        快慢指针,两个指针方向相同。适合解决数组中的移动、删除元素问题,或者链表中的判断是否有环、长度问题。

        分离指针,两个指针分别属于不同的数组 ,链表。适合解决有序数组合并,求交集、并集问题。

三,滑动窗口(Sliding Window)

        在给定数组或字符串上,维护一个窗口,可以对窗口进行滑动操作、缩放操作以及维护最优解操作。

        滑动操作,窗口可以按照一定的方向移动。

        缩放操作,可以从左侧缩小窗口长度,也可以从右侧。

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

滑动窗口分为【固定长度滑动窗口】和【不定长度滑动窗口】

1. 固定长度滑动窗口

        在给定数组或字符串上维护一个固定长度的窗口

        步骤:

        使用两个指针 left,right。初始时同时指向第一个元素 left = 0, right = 0;

        当窗口未达到规定大小时,不断移动 right,将元素加入窗口;

        达到规定大小后,判断窗口内元素是否满足条件,若满足,维护最优解;

        移动 left 缩小窗口长度,并移动 right 加入新元素到窗口,以保持窗口大小不变;

伪代码模板:

left = 0
right = 0

while right < len(nums):
    window.append(nums[right])

    if right - left + 1 >- windowSize:
        window.popleft()
        left += 1
    
    right += 1

例题:

 1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)

class Solution:
    def numOfSubarrays(self, arr: List[int], k: int, threshold: int) -> int:
        ans = 0
        target = k * threshold
        left = 0
        right = 0
        window = 0
        while right < len(arr):
            window += arr[right]

            if right - left + 1 == k:
                if window >= target:
                    ans += 1
                window -= arr[left]
                left += 1
            right += 1
        return ans

        因为需要的是窗口的和,因此可以将窗口直接记录为求和。

2. 不定长度滑动窗口

        在给定数组或字符串上维护一个不定长度的窗口

        步骤:

        使用两个指针 left,right。初始时同时指向第一个元素 left = 0, right = 0;

       将区间最右侧的元素加入到窗口中,然后移动 right 增大窗口长度;

        直到窗口中的连续元素满足要求;

        然后移动 left 直到窗口中的连续元素不满足要求

伪代码模板:

left = 0
right = 0

while right < len(nums):
    window.append(nums[right])
    
    while 窗口需要缩小:
        
        window.popleft()
        left += 1

    right += 1

例题:

1. 3. 无重复字符的最长子串 - 力扣(LeetCode)

        使用哈希表来判断新加入的元素是否重复,如果重复则移动 left 直到没有重复元素,并在这个过程中维护最优值

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. 209. 长度最小的子数组 - 力扣(LeetCode)

        注意细节,更新最优值的时机,需要自己模拟几遍

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        left = 0
        right = 0
        window = 0
        flag = False
        res = float('inf')
        while right < len(nums):
            window += nums[right]
            while window >= target:
                flag = True
                window -= nums[left]
                res = min(res, right - left + 1)
                left += 1
            right += 1
        if not flag:
            return 0
        return res

3. 713. 乘积小于 K 的子数组 - 力扣(LeetCode)

        在统计数组数目时,如果 [ i: j ] 符合条件,则 i 到 j 的连乘都符合条件

        即 i * ( i + 1) ;  i * ( i + 1) * ( i + 2) 。。。

        共有 j - i + 1 种

        

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        left = 0
        right = 0
        window = float(1)
        ans = 0
        while right < len(nums):
            window *= nums[right]
            while window >= k and left <= right:
                window /= nums[left]
                left += 1
            ans += right - left + 1
            right += 1
        return ans

滑动窗口总结

        使用滑动窗口时,要注意维护窗口的规模与最优值。

        最优值更新的时机也是该技巧的难点之一

算法通关手册(LeetCode) | 算法通关手册(LeetCode)

原文内容在这里,如有侵权,请联系我删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值