leetcode_day11_筑基期_《绝境求生》

89 阅读14分钟

目录


前言

碎碎念:轻薄,低调,干净

本系列《绝境求生》记录转码算法筑基过程,以代码随想录为纲学习,leetcode_hot_100练手,在此记录思考过程,方便过后复现。内容比较粗糙仅便于笔者厘清思路,复盘总结。


提示:以下是本篇文章正文内容

双指针&滑动窗口

双指针

解决数组和字符串的遍历问题,目标是把On^2变成On。 通过移动同向或相向的两个指针减少重复计算。用两个指针覆盖所有的有效组合

反向指针:根据当前的结果调整指针(先排序数组,太小了,左指针向右移,太大了右指针向左移动)

同向指针:慢指针记录有效位置,快指针遍历筛选元素(快指针找到目标元素就和慢指针交换位置)

滑动窗口

同向双指针特殊形式。用两个指针维护一个连续的窗口区间(有点像毛毛虫爬行),通过扩展右指针,收缩左指针让窗口满足特定的条件(比如无重复字符和>target)。让窗口内的元素是当前处理的有效范围。

仅针对连续子串 / 子数组问题

一、15 三数之和

1、题目描述

给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ≠ j ≠ k(索引互不相同)且 nums[i] + nums[j] + nums[k] = 0。请你返回所有不重复的三元组,且答案中不可以包含重复的三元组。

关键规则

  1. 索引 i、j、k 互不相同(但值可以重复,比如 [-1,-1,2] 是合法的);
  2. 三元组本身不能重复(比如 [-1,0,1] 和 [0,-1,1] 视为重复,只能保留一个);
  3. 无需考虑三元组的顺序(最终返回的三元组内部元素通常按升序排列)。

示例

  • 示例 1:输入 nums = [-1,0,1,2,-1,-4] → 输出 [[-1,-1,2],[-1,0,1]]
  • 示例 2:输入 nums = [] → 输出 []
  • 示例 3:输入 nums = [0] → 输出 []
  • 示例 4:输入 nums = [0,0,0] → 输出 [[0,0,0]]

提示

  • 0 <= nums.length <= 3000
  • -10^5 <= nums[i] <= 10^5

2、简单理解?

3、暴力法

  • 暴力法(O (n³),超时但理解逻辑):三层循环枚举所有三元组;

3.1、能不能用图示意?

nums = [-1,0,1,2,-1,-4] 为例

三层循环枚举:
i=0(-1):
  j=10):
    k=21)→ 和为0 → 收集[-1,0,1]
    k=32)→ 和为1 → 跳过
    k=4(-1)→ 和为-2 → 跳过
    k=5(-4)→ 和为-5 → 跳过
  j=21):
    ...(无符合条件的k)
  j=4(-1):
    k=32)→ 和为0 → 收集[-1,-1,2]
...(后续循环无新的符合条件的组合)
去重后结果:[[-1,-1,2],[-1,0,1]]

3.3、边界条件?

        # 边界条件:数组长度<3,直接返回空
        if n < 3:
            return []

3.4、代码逻辑?

3.5、之前见过但没注意到的?

list(set( tuple(i) for i in result))

[list(t) for t in result]

 # 去重:转成集合(需将列表转元组,因为列表不可哈希),再转回列表
        result = list(set(tuple(t) for t in result))
        # 转回列表的列表格式
        result = [list(t) for t in result]

3.6、疑惑点/新知识 ?

3.7、python 代码

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        # 边界条件:数组长度<3,直接返回空
        if n < 3:
            return []
        
        # 初始化结果列表
        result = []
        # 三层循环枚举所有i<j<k的组合(保证索引不同)
        for i in range(n):
            for j in range(i+1, n):
                for k in range(j+1, n):
                    # 检查三数之和是否为0
                    if nums[i] + nums[j] + nums[k] == 0:
                        # 收集三元组并排序(方便后续去重)
                        triplet = sorted([nums[i], nums[j], nums[k]])
                        result.append(triplet)
        
        # 去重:转成集合(需将列表转元组,因为列表不可哈希),再转回列表
        result = list(set(tuple(t) for t in result))
        # 转回列表的列表格式
        result = [list(t) for t in result]
        
        return result

4、优化法

for i in range(n) 固定一个数,另外两个数用双指针

 4.1、能不能用图示意?

步骤1:排序数组 → nums = [-4,-1,-1,0,1,2]
步骤2:遍历固定第一个数:
i=0(nums[i]=-4):
  left=1(-1),right=52)→ 和=-4+(-1)+2=-3 < 0left=2
  left=2(-1),right=52)→ 和=-4+(-1)+2=-3 < 0left=3
  left=30),right=52)→ 和=-4+0+2=-2 < 0left=4
  left=41),right=52)→ 和=-4+1+2=-1 < 0left=5left>=right,结束)

i=1(nums[i]=-1):
  left=2(-1),right=52)→ 和=-1+(-1)+2=0 → 收集[-1,-1,2]
  跳过重复leftleft=2(-1)重复,left=3
  left=30),right=52)→ 和=-1+0+2=1 > 0right=4
  left=30),right=41)→ 和=-1+0+1=0 → 收集[-1,0,1]
  跳过重复left/rightleft=34right=43left>=right,结束)

i=2(nums[i]=-1):与i=1的值重复,直接跳过

i=3(nums[i]=0):0>0不成立,但后续left=4right=5 → 和=0+1+2=3>0,无符合条件的组合

i=4/5:nums[i]>0,直接终止遍历

最终结果:[[-1,-1,2],[-1,0,1]]

4.4、代码逻辑?

  1. if n < 3: return []边界条件数组长度不足 3,无法构成三元组,直接返回空必须优先处理,避免后续索引越界
    nums.sort()排序数组排序后元素升序排列,便于双指针调整和去重排序是双指针法的基础,不可省略
    result = []结果初始化存储最终不重复的三元组初始为空,符合无结果的情况
    for i in range(n):遍历第一个数i 是三元组第一个数的索引遍历范围是整个数组,但会提前剪枝
    if nums[i] > 0: break剪枝优化排序后 nums [i]>0,后续数都≥nums [i],和不可能为 0break 而非 continue,因为后续 i 更大,nums [i] 只会更大
    if i>0 and nums[i]==nums[i-1]: continue去重第一个数跳过与前一个数重复的 nums [i],避免重复三元组必须判断 i>0,否则 i=0 时 i-1=-1,索引越界
    left = i+1左指针初始化第二个数的索引,必须在 i 右侧(避免重复索引)不能初始化为 i,否则索引重复
    right = n-1右指针初始化第三个数的索引,初始为数组最后一个元素避免索引越界
    while left < right:双指针循环条件左指针必须在右指针左侧,否则无法构成不同索引不能写 left<=right,否则 left=right 时索引重复
    total = nums[i]+nums[left]+nums[right]计算三数之和核心判断条件,决定指针移动方向直接计算总和,避免多次重复计算
    if total <0: left +=1左指针右移和偏小,需要更大的数,左指针右移(排序后右侧数更大)仅移动左指针,右指针不动
    elif total>0: right -=1右指针左移和偏大,需要更小的数,右指针左移(排序后左侧数更小)仅移动右指针,左指针不动
    else: result.append(...)收集三元组和为 0,符合条件,加入结果列表三元组顺序为 [nums [i], nums [left], nums [right]],天然升序
    while left<right and nums[left]==nums[left+1]: left +=1左指针去重跳过重复的第二个数,避免重复三元组必须判断 left<right,否则 left+1 越界
    while left<right and nums[right]==nums[right-1]: right -=1右指针去重跳过重复的第三个数,避免重复三元组必须判断 left<right,否则 right-1 越界
    left +=1; right -=1移动双指针找下一组可能的组合,避免死循环去重后必须移动指针,否则会一直停留在重复值
    return result返回结果遍历结束,返回所有不重复的三元组结果已按升序排列,符合题目要求

4.5、之前见过但没注意到的?

4.6、疑惑点/新知识 ?

4.7、python 代码

from typing import List

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        # 步骤1:边界条件处理(数组长度<3,直接返回空)
        if n < 3:
            return []
        
        # 步骤2:排序数组(核心预处理,便于双指针+去重)
        nums.sort()
        # 步骤3:初始化结果列表
        result = []
        
        # 步骤4:遍历固定第一个数(i为第一个数的索引)
        for i in range(n):
            # 剪枝:排序后第一个数>0,后续都是正数,和不可能为0,直接终止
            if nums[i] > 0:
                break
            
            # 去重:跳过重复的第一个数(避免重复三元组)
            if i > 0 and nums[i] == nums[i-1]:
                continue
            
            # 步骤5:初始化双指针(left=i+1:第二个数;right=n-1:第三个数)
            left = i + 1
            right = n - 1
            
            # 步骤6:双指针遍历,找和为-nums[i]的两个数
            while left < right:
                # 计算三数之和
                total = nums[i] + nums[left] + nums[right]
                
                if total < 0:
                    # 和偏小:左指针右移,增大和
                    left += 1
                elif total > 0:
                    # 和偏大:右指针左移,减小和
                    right -= 1
                else:
                    # 和为0:收集三元组
                    result.append([nums[i], nums[left], nums[right]])
                    
                    # 去重:跳过重复的左指针值(避免重复三元组)
                    while left < right and nums[left] == nums[left+1]:
                        left += 1
                    # 去重:跳过重复的右指针值(避免重复三元组)
                    while left < right and nums[right] == nums[right-1]:
                        right -= 1
                    
                    # 双指针同时移动,找下一组可能的组合
                    left += 1
                    right -= 1
        
        # 步骤7:返回结果
        return result

 一、3无重复字符的最长子串

1、题目描述

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

关键规则

  1. 「子串」是字符串中连续的字符序列(区别于子序列);
  2. 子串中不能有重复字符(比如 "abc" 合法,"abca" 不合法);
  3. 若字符串为空,返回 0;若字符串所有字符都重复(如 "bbbbb"),返回 1。

示例

  • 示例 1:输入 s = "abcabcbb" → 输出 3

    • 解释:最长无重复子串是 "abc",长度为 3;
  • 示例 2:输入 s = "bbbbb" → 输出 1

    • 解释:最长无重复子串是 "b",长度为 1;
  • 示例 3:输入 s = "pwwkew" → 输出 3

    • 解释:最长无重复子串是 "wke" 或 "kew",长度为 3;
  • 示例 4:输入 s = "" → 输出 0;输入 s = " " → 输出 1;输入 s = "abba" → 输出 2

提示

  • 0 <= s.length <= 5 * 10^4
  • s 由英文字母、数字、符号和空格组成

2、简单理解?

用最少个完全平方数的和组成target,(可以使用重复的完全平方数)

3、暴力法

3.1、能不能用图示意?

# 初始化dp数组(长度13,索引0~12)
dp[0] = 0
dp[1] = dp[1-1²] +1 = dp[0]+1=1
dp[2] = dp[2-1²] +1 = dp[1]+1=2
dp[3] = dp[3-1²] +1 = dp[2]+1=3
dp[4] = min(dp[4-1²]+1, dp[4-2²]+1) = min(dp[3]+1=4, dp[0]+1=1) → 1
dp[5] = min(dp[5-1²]+1, dp[5-2²]+1) = min(dp[4]+1=2, dp[1]+1=2) → 2
dp[6] = min(dp[6-1²]+1, dp[6-2²]+1) = min(dp[5]+1=3, dp[2]+1=3) → 3
dp[7] = min(dp[7-1²]+1, dp[7-2²]+1) = min(dp[6]+1=4, dp[3]+1=4) → 4
dp[8] = min(dp[8-1²]+1, dp[8-2²]+1) = min(dp[7]+1=5, dp[4]+1=2) → 2
dp[9] = min(dp[9-1²]+1, dp[9-2²]+1, dp[9-3²]+1) = min(5,3,1) →1
dp[10] = min(dp[10-1²]+1, dp[10-2²]+1, dp[10-3²]+1) = min(2,3,2) →2
dp[11] = min(dp[11-1²]+1, dp[11-2²]+1, dp[11-3²]+1) = min(3,4,3) →3
dp[12] = min(dp[12-1²]+1, dp[12-2²]+1, dp[12-3²]+1) = min(dp[11]+1=4, dp[8]+1=3, dp[3]+1=4) →3

3.2、初始化条件?

dp[0]=0, dp[1]=1

3.3、边界条件?

剪枝 target=0 or 1 的情况

3.4、代码逻辑?

状态定义dp[i ]表示 组成正整数i的最少完全平方个数

dp数组下标代表target。 对应的值代表实现平方和target的最少数

对于i,遍历所有不大于i的完全平方数j^2,dp[i]=min(dp[i-j^2]+1)   (+1 是加上当前选的 j^2

外层循环,从1-n 逐个计算每个数的最少个数

内层循环,遍历所有不大于 i 的完全平方数,避免无效运算

3.5、之前见过但没注意到的?

5的平方不能直接用5^2表示。最好是square=5*5

!!! dp = [float('inf')] * (n + 1):初始值设为极大值,因为一开始不知道每个 i 的最少个数,后续通过转移方程更新;

3.6、疑惑点/新知识 ?

搞清楚索引和数组长度分别定义什么?

3.7、python 代码

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n = len(s)
        # 初始化最大长度为0(兼容空字符串)
        max_len = 0
        
        # 外层循环:枚举所有子串起始位置i
        for i in range(n):
            # 初始化集合,记录当前子串的字符(去重)
            char_set = set()
            # 初始化当前子串长度
            current_len = 0
            
            # 内层循环:从i开始扩展子串,直到遇到重复字符
            for j in range(i, n):
                # 若字符已存在,终止内层循环
                if s[j] in char_set:
                    break
                # 字符不存在,加入集合,当前长度+1
                char_set.add(s[j])
                current_len += 1
            
            # 更新全局最大长度
            max_len = max(max_len, current_len)
        
        return max_len

4、优化法

 4.1、能不能用图示意?

以 s = "abcabcbb" 为例

初始化:left=0,max_len=0,char_index={}
right=0(字符a):
  a不在char_index → char_index={"a":0} → 窗口[0,0],长度1 → max_len=1
right=1(字符b):
  b不在char_index → char_index={"a":0,"b":1} → 窗口[0,1],长度2 → max_len=2
right=2(字符c):
  c不在char_index → char_index={"a":0,"b":1,"c":2} → 窗口[0,2],长度3 → max_len=3
right=3(字符a):
  a在char_index且索引0left=0left=0+1=1 → 更新char_index["a"]=3 → 窗口[1,3],长度3 → max_len=3
right=4(字符b):
  b在char_index且索引1left=1left=1+1=2 → 更新char_index["b"]=4 → 窗口[2,4],长度3 → max_len=3
right=5(字符c):
  c在char_index且索引2left=2left=2+1=3 → 更新char_index["c"]=5 → 窗口[3,5],长度3 → max_len=3
right=6(字符b):
  b在char_index且索引4left=3left=4+1=5 → 更新char_index["b"]=6 → 窗口[5,6],长度2 → max_len=3
right=7(字符b):
  b在char_index且索引6left=5left=6+1=7 → 更新char_index["b"]=7 → 窗口[7,7],长度1 → max_len=3
最终max_len=3

4.6、疑惑点/新知识 ?

问题 1:为什么判断条件要加 char_index[char] >= left

比如遍历到 s="abba" 的 right=3(char='a')时:

  • char_index['a']=0,但此时left=2(因为之前处理重复的 'b',left 已经挪到 2 了);
  • 0 < 2 说明 'a' 虽然出现过,但不在当前窗口 [2,3] 里,所以不用挪 left —— 这个条件是为了避免 “把窗口外的重复字符算进来”。
问题 2:为什么每次都要更新char_index[char] = right

哈希表要永远记录字符「最后一次出现的位置」,如果不更新,下次遇到这个字符时,会用旧的索引挪 left,导致错误。比如第四次循环后,'a' 的索引从 0 改成 3,下次再遇到 'a' 时,就会用 3 来判断,而不是 0。

4.7、python 代码

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 步骤1:初始化核心变量
        char_index = {}  # 哈希表:字符 → 最后出现的索引
        max_len = 0      # 记录最长无重复子串长度
        left = 0         # 滑动窗口左指针(窗口起始位置)
        
        # 步骤2:右指针遍历每个字符(窗口结束位置)
        for right, char in enumerate(s):
            # 步骤3:关键判断:字符在哈希表中 且 最后出现的索引≥左指针(说明在当前窗口内重复)
            if char in char_index and char_index[char] >= left:
                # 左指针移动到重复字符的下一位,收缩窗口
                left = char_index[char] + 1
            
            # 步骤4:更新哈希表中当前字符的最新索引
            char_index[char] = right
            
            # 步骤5:计算当前窗口长度,更新最大长度
            current_len = right - left + 1
            max_len = max(max_len, current_len)
        
        # 步骤6:返回结果
        return max_len

右指针一直往前装新字符,一旦发现装的字符在窗口里重复了,就把左指针直接挪动到重复字符的下一位