目录
前言
碎碎念:轻薄,低调,干净
本系列《绝境求生》记录转码算法筑基过程,以代码随想录为纲学习,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。请你返回所有不重复的三元组,且答案中不可以包含重复的三元组。关键规则:
- 索引
i、j、k互不相同(但值可以重复,比如[-1,-1,2]是合法的);- 三元组本身不能重复(比如
[-1,0,1]和[0,-1,1]视为重复,只能保留一个);- 无需考虑三元组的顺序(最终返回的三元组内部元素通常按升序排列)。
示例
- 示例 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=1(0): k=2(1)→ 和为0 → 收集[-1,0,1] k=3(2)→ 和为1 → 跳过 k=4(-1)→ 和为-2 → 跳过 k=5(-4)→ 和为-5 → 跳过 j=2(1): ...(无符合条件的k) j=4(-1): k=3(2)→ 和为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=5(2)→ 和=-4+(-1)+2=-3 < 0 → left=2 left=2(-1),right=5(2)→ 和=-4+(-1)+2=-3 < 0 → left=3 left=3(0),right=5(2)→ 和=-4+0+2=-2 < 0 → left=4 left=4(1),right=5(2)→ 和=-4+1+2=-1 < 0 → left=5(left>=right,结束) i=1(nums[i]=-1): left=2(-1),right=5(2)→ 和=-1+(-1)+2=0 → 收集[-1,-1,2] 跳过重复left:left=2(-1)重复,left=3 left=3(0),right=5(2)→ 和=-1+0+2=1 > 0 → right=4 left=3(0),right=4(1)→ 和=-1+0+1=0 → 收集[-1,0,1] 跳过重复left/right:left=3→4,right=4→3(left>=right,结束) i=2(nums[i]=-1):与i=1的值重复,直接跳过 i=3(nums[i]=0):0>0不成立,但后续left=4,right=5 → 和=0+1+2=3>0,无符合条件的组合 i=4/5:nums[i]>0,直接终止遍历 最终结果:[[-1,-1,2],[-1,0,1]]
4.4、代码逻辑?
-
if n < 3: return []边界条件 数组长度不足 3,无法构成三元组,直接返回空 必须优先处理,避免后续索引越界 nums.sort()排序数组 排序后元素升序排列,便于双指针调整和去重 排序是双指针法的基础,不可省略 result = []结果初始化 存储最终不重复的三元组 初始为空,符合无结果的情况 for i in range(n):遍历第一个数 i 是三元组第一个数的索引 遍历范围是整个数组,但会提前剪枝 if nums[i] > 0: break剪枝优化 排序后 nums [i]>0,后续数都≥nums [i],和不可能为 0 break 而非 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,请你找出其中不含有重复字符的最长子串的长度。关键规则:
- 「子串」是字符串中连续的字符序列(区别于子序列);
- 子串中不能有重复字符(比如
"abc"合法,"abca"不合法);- 若字符串为空,返回 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且索引0≥left=0 → left=0+1=1 → 更新char_index["a"]=3 → 窗口[1,3],长度3 → max_len=3 right=4(字符b): b在char_index且索引1≥left=1 → left=1+1=2 → 更新char_index["b"]=4 → 窗口[2,4],长度3 → max_len=3 right=5(字符c): c在char_index且索引2≥left=2 → left=2+1=3 → 更新char_index["c"]=5 → 窗口[3,5],长度3 → max_len=3 right=6(字符b): b在char_index且索引4≥left=3 → left=4+1=5 → 更新char_index["b"]=6 → 窗口[5,6],长度2 → max_len=3 right=7(字符b): b在char_index且索引6≥left=5 → left=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
右指针一直往前装新字符,一旦发现装的字符在窗口里重复了,就把左指针直接挪动到重复字符的下一位