【灵神高频面试题合集01-03】相向双指针、滑动窗口

基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- GitHub

视频:灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频


相向双指针

01 两数之和 三数之和

课程讲解

167. 两数之和 II - 输入有序数组

暴力:O(n^2)的时间复杂度,没有利用到数组已经排好序的性质

双指针:O(n)的时间复杂度

暴力法是找两个数加起来和target比较,花费O(1)的时间知道O(1)的信息。

而优化后的做法是把当前剩下的最小的数和最大的数加起来和target比较,比完之后就知道其中一个数和其他任何一个数相加都是小于target或大于target的,花费O(1)的时间知道O(n)的信息

python 复制代码
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left, right = 0, len(numbers)-1
        while left < right:
            if numbers[left] + numbers[right] == target:
                break
            elif numbers[left] + numbers[right] > target:
                right -= 1
            else:
                left += 1
        return [left + 1, right + 1]
  • 时间O(n),空间O(1)
15. 三数之和

经过上题的启发,如果数组有序,就可以使用相向双指针。故本题先对数组排序

  • 三元组的顺序并不重要,那么可以 i < j < k
  • 答案中不可以包含重复的三元组,那么只要当前枚举的这个数和上一个数是相同的,就跳过
  • nums[i] + nums[j] + nums[k] == 0,转变一下,nums[j] + nums[k] == -nums[i],就跟上题一样了
python 复制代码
class Solution:
    def threeSum(self, nums: list[int]) -> list[list[int]]:
        nums.sort()
        res = []
        n = len(nums)
        for i in range(n-2):   # 后面留n-1, n-2位置给j和k
            x = nums[i]
            if i > 0 and x == nums[i-1]:   # 去重
                continue
            j, k = i+1, n-1
            # 下面跟上题一样
            while j < k:
                s = x + nums[j] + nums[k]
                if s > 0:
                    k -= 1
                elif s < 0:
                    j += 1
                else:
                    res.append([x, nums[j], nums[k]])
                    j += 1
                    while j < k and nums[j] == nums[j-1]:   # 去重
                        j += 1
                    k -= 1
                    while k > j and nums[k] == nums[k+1]:   # 去重
                        k -= 1
        return res

优化:

python 复制代码
            # 剪枝优化
            if x + nums[i+1] + nums[i+2] > 0:
                break
            if x + nums[-1] + nums[-2] < 0:
                continue
  • 时间O(n^2)
    • 排序O(nlogn)
    • for循环里嵌套while循环O(n^2)
  • 空间O(1)

课后作业

2824. 统计和小于目标的下标对数目

16. 最接近的三数之和

18. 四数之和

611. 有效三角形的个数


02 接雨水 前后缀分解

课程讲解

11. 盛最多水的容器

容器的高度取决于短的这条线,容器的宽度取决于这两条线的距离(下标的差)

看图中短的那条红色的线,

  • 如果中间的线比它短,那么面积的宽度和高度都变小
  • 如果中间的线比它长,那么面积的宽度变小,高度不变
  • 中间的任何一条线都无法跟它构成容量更大的容器

换句话说,如果要容纳更多的水,肯定不会包含短的那条红色的线

python 复制代码
class Solution:
    def maxArea(self, height: List[int]) -> int:
        res = 0
        left, right = 0, len(height) - 1
        while left < right:
            area = (right - left) * min(height[left], height[right])
            res = max(res, area)
            # 哪条线短就移动哪条
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return res
  • 时间O(n),空间O(1)
42. 接雨水

假设每个位置都有一个宽度为1的桶,计算能接多少水,就需要计算左边这块木板的高度(取决于左边的max)和右边这块木板的高度(取决于右边的max),取两者的min

前后缀分解

需要额外两个数组

  • 第一个数组存储从最左边到第i个位置(前缀)的最大高度,为前缀的最大值
  • 第二个数组存储从最右边到第i个位置(后缀)的最大高度,为后缀的最大值

对于每一个前缀最大值 ,可以用上一个前缀最大值和当前高度比较取max(从左到右算 );对于每一个后缀最大值 ,同上,只是从右到左算

最后,同时遍历高度、前缀最大值和后缀最大值,累加 min(前缀最大值, 后缀最大值) - 高度即为最后结果

python 复制代码
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre_max = [0] * n
        pre_max[0] = height[0]
        for i in range(1, n):
            pre_max[i] = max(pre_max[i-1], height[i])

        suf_max = [0] * n
        suf_max[-1] = height[-1]
        for i in range(n-2, -1, -1):
            suf_max[i] = max(suf_max[i+1], height[i])

        ans = 0
        for h, pre, suf in zip(height, pre_max, suf_max):
            ans += min(pre, suf) - h
        return ans
  • 时空间复杂度都是O(n)
相向双指针

第一种做法在空间复杂度上还可以优化

  • 如果前缀最大值比后缀最大值小,那么左边这个木桶的容量就是前缀最大值,算完之后把它向右扩展
  • 反之,如果后缀最大值比前缀最大值小,那么右边这个木桶的容量就是后缀最大值,算完之后把它向左扩展

用两个指针分别指向最左(0)和最右边(len-1)

python 复制代码
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        ans = 0
        left, right = 0, n-1
        pre_max, suf_max = 0, 0

        while left <= right:
            pre_max = max(pre_max, height[left])
            suf_max = max(suf_max, height[right])
            if pre_max < suf_max:
                ans += pre_max - height[left]
                left += 1
            else:
                ans += suf_max - height[right]
                right -= 1
        return ans
  • 时间O(n),空间O(1)
单调栈

代码随想录思路,详见 单调栈,经典来袭!LeetCode:42.接雨水

单调递增栈(求右边第一个比它大的元素),单调栈里存放的是已经遍历过的元素

  • 中间柱子的下标:栈顶元素(栈里存的是下标)
  • 左边第一个比它高的柱子的下标:把上面栈顶元素弹出后的新栈顶元素
  • 右边~:当前遍历元素的下标(当前遍历元素 > 栈顶元素时)

左右取个min再减去中间柱子的高度为凹槽的高度,之间的距离为凹槽的宽度,由此累加面积

python 复制代码
class Solution:
    def trap(self, height: List[int]) -> int:
        stack = [0]   # 栈里存放下标
        rain = 0
        for i in range(1, len(height)):
            if height[i] < height[stack[-1]]:   # 符合单调递增栈的规则,直接入栈
                stack.append(i)
            elif height[i] == height[stack[-1]]:
                # 先弹出再加入,可以节省一步计算
                stack.pop()
                stack.append(i)
            else:
                while stack != [] and height[i] > height[stack[-1]]:
                    mid = stack[-1]   # 中间柱子的下标为当前栈顶元素
                    stack.pop()
                    if stack != []:
                        left = stack[-1]   # 左边柱子的下标为当前栈顶元素弹出后的栈顶元素
                        right = i   # 右边柱子的下标为当前遍历元素
                        # 计算凹槽的高和宽
                        high = min(height[left], height[right]) - height[mid]
                        width = right - left - 1
                        rain += high * width
                stack.append(i)
        return rain

课后作业

125. 验证回文串

2105. 给植物浇水 II


滑动窗口(同向双指针)

【题单】滑动窗口:https://leetcode.cn/circle/discuss/0viNMK/

03 最短 最长 方案数

课程讲解

在子数组、子串问题中,经常会用到双指针这一技巧。只有满足了单调性才能使用双指针

209. 长度最小的子数组
python 复制代码
class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)
        ans = n+1   # 初始化为一个很大的数,本题答案至多是n(或inf)
        s = 0
        left = 0
        # 枚举右端点
        for right, x in enumerate(nums):   # right: [0, n-1]; x = nums[right]
            s += x
            while s - nums[left] >= target:
                # 缩小窗口,右移左端点
                s -= nums[left]
                left += 1
            if s >= target:
                ans = min(ans, right-left+1)
        return ans if ans <= n else 0

# 这里还有另一种写法
class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)
        ans = n+1   # 或inf
        s = 0
        left = 0
        for right, x in enumerate(nums):   # right: [0, n-1]; x = nums[right]
            s += x
            while s >= target:
                ans = min(ans, right-left+1)
                s -= nums[left]
                left += 1      
        return ans if ans <= n else 0
  • 时间O(n),空间O(1)
  • 注意不要以为for里套着while就是O(n^2)的时间复杂度,因为left和right都是至多移动到n的
713. 乘积小于 K 的子数组

while条件还可以从不满足要求变成满足要求

python 复制代码
class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        if k <= 1:
            return 0
        ans = 0
        prod = 1
        left = 0
        for right, x in enumerate(nums):
            prod *= x
            while prod >= k:
                prod = prod / nums[left]
                left += 1
            # [l, r] 的乘积 < k
            # 那么 [l, r] [l+1, r] ... [r, r] 的乘积一定 < k
            # 符合的子数组数目其实就是 r-l+1
            ans += right-left+1
        return ans
3. 无重复字符的最长子串

由于每次都是接到一个没有重复元素的子串后面,所以这个重复的元素一定来自接入的这个字符

怎么判断是否有重复字符呢?可以用一个哈希表记录字符的出现次数

  • cnt = Counter() 会创建一个空的计数器对象 ,类似于空字典 {},但专门用于计数
python 复制代码
from collections import Counter

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans = 0   # 求的是最大值,因此初始化为0
        cnt = Counter()   # hashmap,记录每个字符的出现次数
        left = 0
        # 枚举右端点
        for right, c in enumerate(s):
            cnt[c] += 1   # 字符c出现次数+1
            while cnt[c] > 1:
                # 将左端点右移
                cnt[s[left]] -= 1
                left += 1
            ans = max(ans, right-left+1)  # 字符个数
        return ans
  • 时间O(n)
  • 空间O(128)或O(1),因为s由英文字母、数字、符号和空格组成,可以当做是一个ASCII字符(最多有128个)。也可以理解成:s里有多少个不同的字符,O(len(set(s)))

课后作业

3090. 每个字符最多出现两次的最长子字符串

2958. 最多 K 个重复元素的最长子数组

2730. 找到最长的半重复子字符串

1004. 最大连续 1 的个数 III

2962. 统计最大元素出现至少 K 次的子数组

2302. 统计得分小于 K 的子数组数目

1658. 将 x 减到 0 的最小操作数

3795. 不同元素和至少为 K 的最短子数组长度

76. 最小覆盖子串

相关推荐
leoufung2 小时前
LeetCode 42:接雨水 —— 从“矩形法”到双指针的完整思考过程
java·算法·leetcode
WHS-_-20222 小时前
Rank-Revealing Bayesian Block-Term Tensor Completion With Graph Information
人工智能·python·机器学习
技术钱2 小时前
Modal组件及使用技巧
python
Ulyanov2 小时前
《现代 Python 桌面应用架构实战:PySide6 + QML 从入门到工程化》:QML 声明式语法与霓虹按钮 —— 当 Python 遇见现代美学
开发语言·python·ui·qml·系统仿真·雷达电子对抗仿真
zh路西法2 小时前
【RDKX5多摄像头模型推理】USB带宽限制与ROS2话题零拷贝转发
linux·c++·python·深度学习
码界筑梦坊2 小时前
113-基于Python的国际超市电商销售数据可视化分析系统
开发语言·python·信息可视化·毕业设计·fastapi
千寻girling3 小时前
五一劳动节快乐 [特殊字符][特殊字符][特殊字符]
java·c++·git·python·学习·github·php
_日拱一卒3 小时前
LeetCode:543二叉树的直径
算法·leetcode·职场和发展
jieyucx3 小时前
Go 数据结构入门:线性表、顺序表、链表
数据结构·链表·golang