① leetcode刷题汇总(数组 / 字符串)
一、学习顺序
顺序:“数组 / 字符串 →双指针→滑动窗口→链表→二叉树→动态规划→设计”
二、代码训练 之 数组 / 字符串
这六个题目覆盖了数组 / 字符串的核心技巧:哈希表(快速查找)、双指针(有序结构 / 范围收缩)、滑动窗口(子串问题)、中心扩展(对称问题)。
| 问题类型 | 题目 | 核心思路 | 关键技巧 | 时间复杂度 |
|---|---|---|---|---|
| 数组求和 | 两数之和 | 找两个数的和为 target,返回索引 | 哈希表存「值→索引」,单次遍历查互补数(避免排序破坏索引) | O(n) |
| 三数之和 | 找三个数的和为 0,返回不重复的三元组 | 先排序(方便去重和双指针),固定一个数后用双指针找另外两个数(注意去重) | O(n²) | |
| 字符串子串 | 无重复字符的最长子串 | 找最长的无重复字符子串 | 滑动窗口(左右指针)+ 哈希表(记录字符最后出现的位置,快速收缩左边界) | O(n) |
| 最长回文子串 | 找最长的回文子串(正反读一致) | 中心扩展法(枚举每个中心,分奇偶长度)或动态规划(dp [i][j] 表示子串是否回文) | O(n²) | |
| 字符串公共前缀 | 最长公共前缀 | 找多个字符串的最长公共前缀 | 纵向比较(逐个字符对比所有字符串)或横向比较(两两求公共前缀) | O (mn)(m 为字符串数,n 为最短长度) |
| 数组合并 | 合并两个有序数组 | 将两个有序数组合并为一个有序数组(原地修改) | 双指针从后往前遍历(避免覆盖 nums1 的有效元素) | O(m+n) |
1. 两数之和(哈希表)
- 思路:「哈希表(字典)辅助的单次遍历」思路,核心是通过哈希表快速查找 “互补数”,从而高效找到满足条件的索引对。
from typing import Listclass Solution:def twoSum(self, nums: List[int], target: int) -> List[int]:num_index_dict = {} # 字典:存储 {数字: 索引},用于O(1)时间查找需要的数字if len(nums) < 2: return [] # 提前终止:不足2个元素时无有效解for index, num in enumerate(nums): need_num = target - num # 计算需要的数字(目标与当前数字的差值)if need_num in num_index_dict:return [num_index_dict[need_num], index] # 返回两个数的索引num_index_dict[num] = indexreturn []
注意点:
1. num_index_dict = {} 存储为{数字: 索引} ------ 如果存储为{索引: 数字} 不好寻找目标数字
2. for index, num in enumerate(nums): ------ enumerate(nums) 返回的是 (索引, 元素) 的元组,{0: 3, 1: 2, 2: 4}
15. 三数之和((排序 + 双指针))
class Solution:def threeSum(self, nums: List[int]) -> List[List[int]]:n = len(nums)res =[]if (not nums or n < 3):return []nums.sort() # 因为排序后可以用双指针来优化,并且方便去重# 外层固定循环第一个元素i,左指针L 右指针Rfor i in range(n):# 相同的元素只作为 “固定元素” 处理一次if i > 0 and nums[i] == nums[i-1]: continueL = i + 1R = n - 1while(L<R):if (nums[i] + nums[L] + nums[R] == 0):res.append([nums[i],nums[L],nums[R]])# 作用:把左指针 L 移到「最后一个和当前 nums[L] 相同的元素」位置while(L<R and nums[L]==nums[L+1]):L = L + 1# 作用:把右指针 R 移到「第一个和当前 nums[R] 相同的元素」位置while(L<R and nums[R]==nums[R-1]):R = R - 1# 前面的两个 while 循环,只是把 L 移到 “最后一个重复元素”、R 移到 “最后一个重复元素”,但指针还停在重复元素上(比如 L=2 还在 0,R=3 还在 1)。如果不最后移一步,下一轮还会用这两个重复元素,再次生成相同的三元组 —— 最后移动一次,才能彻底跳出重复元素,避免重复!L = L + 1R = R - 1elif(nums[i]+nums[L]+nums[R]>0):R = R - 1else:L = L + 1return res
注意点:
1. for i in range(n):
# 相同的元素只作为 “固定元素” 处理一次
if i > 0 and nums[i] == nums[i-1]:
continuea. 需要判断i>0 因为如果i=0,则 nums[i-1]就是nums[-1]会越界
b. nums[i] == nums[i-1]: 是将重复的值跳过
2.
while(L<R and nums[L]==nums[L+1]):
L = L + 1
while(L<R and nums[R]==nums[R-1]):
R = R - 1
L = L + 1
R = R - 1前面的两个 while 循环,只是把 L 移到 “最后一个重复元素”、R 移到 “最后一个重复元素”,但指针还停在重复元素上(比如 L=2 还在 0,R=3 还在 1)。如果不最后移一步,下一轮还会用这两个重复元素,再次生成相同的三元组 —— 最后移动一次,才能彻底跳出重复元素,避免重复!
2.
88. 合并两个有序数组
思路:代码采用「双指针从后往前合并」,数字 + 原始索引(不能排序,否则索引失效)
class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:"""Do not return anything, modify nums1 in-place instead."""p1,p2,p = m-1,n-1,m+n-1;# 循环条件:处理完 nums2 所有元素(p2 >= 0)while p2 >= 0:# 注意p1>=0,是为了判断 nums1 数组是否还有剩余的元素未被处理,要不会循环nums1里的数据,因为nums[-1]会取数组最后一个值# 因为nums1[p1]偏大,偏大就要被取出去,所以需要判断取后p1还是否有值if p1 >= 0 and nums1[p1] > nums2[p2]:nums1[p]=nums1[p1]p1-=1;else:nums1[p] = nums2[p2]p2-=1;p-=1;
注意点:原始数据均为有序数据,倒序填充数据到p1
3. 无重复字符的最长子串(“滑动窗口 + 哈希表记录最后位置” )
思路: “滑动窗口 + 哈希表记录最后位置” 的思路,也是解决「子串不重复 / 子数组和」等问题的通用技巧
class Solution(object):def lengthOfLongestSubstring(self, s):""":type s: str:rtype: int"""char_last_index = {} # 记录字符最后出现的索引max_len = 0start = 0 # 滑动窗口左边界(当前窗口的起始位置)for i, char in enumerate(s):# 若字符已在当前窗口内(最后出现的索引 >= start),则移动左边界if char in char_last_index and char_last_index[char] >= start:start = char_last_index[char] + 1# 更新字符最后出现的索引为当前位置char_last_index[char] = i# 计算当前窗口长度,更新最大长度max_len = max(max_len, i - start + 1)return max_len
14. 最长公共前缀
from typing import Listclass Solution:def longestCommonPrefix(self, strs: List[str]) -> str:# 边界条件1:若字符串列表为空,直接返回空if not strs:return ""# 以第一个字符串为基准(基准长度决定了最大可能的前缀长度)base = strs[0]# 遍历基准的每个字符位置 ifor i in range(len(base)):# 检查列表中所有其他字符串for s in strs[1:]:# 两种情况说明当前位置 i 已不是公共前缀:# 1. 某个字符串长度不够 i(比如 s 是 "flow",i=4 时已超出长度)# 2. 某个字符串的 i 位置字符与基准不同if i >= len(s) or s[i] != base[i]:# 返回基准前 i 个字符(0~i-1)return base[:i]# 若基准遍历完所有字符,说明基准就是最长公共前缀return base
5. 最长回文子串(动态规划(DP)解法-具有 “最优子结构”)
a. 回文子串的本质
| 子串长度 | 条件(判断s[i::j])是否为回文 | 结论 |
| 长度=2 | 只需判断 s[i]==s[j] (中间无字符) | 相等则dp[i][j]=True,否则False |
| 长度>2 | 1. s[i]==s[j] (首尾相等) 2. dp[i+1][j-1]== True(中间子串是回文) | 两个条件都满足则True,否则False |
b. dp[i][j]的示例如下
| i\j | 0(b) | 1(a) | 2(b) | 3(a) | 4(d) |
|---|---|---|---|---|---|
| 0(b) | True | False | True | False | False |
| 1(a) | 无效 | True | False | True | False |
| 2(b) | 无效 | 无效 | True | False | False |
| 3(a) | 无效 | 无效 | 无效 | True | False |
| 4(d) | 无效 | 无效 | 无效 | 无效 | True |
class Solution(object):def longestPalindrome(self, s):""":type s: str:rtype: str"""n = len(s)if n < 2: # 处理空串或单字符return s# ✅ 修正1:初始化n×n的二维数组(原代码每行只有1个元素,导致越界)dp = [[False] * n for _ in range(n)] # 5×5的二维数组,所有值初始为Falsemax_len = 1 # 最长回文长度(至少1个字符)start = 0 # 最长回文的起始索引(新增,原代码未定义)# 所有长度为1的子串都是回文for i in range(n):dp[i][i] = True # 对角线设为True# 按子串长度从小到大遍历(2到n)for length in range(2, n + 1): # 原代码正确for i in range(n - length + 1): # 计算起始索引i的合法范围j = i + length - 1 # 结束索引j = i + 长度 - 1# ✅ 修正2:必须同时满足首尾相等 + 中间子串是回文(原代码漏了中间条件)if s[i] == s[j]:if length == 2: # 长度为2,直接是回文(如"bb")dp[i][j] = Trueelse: # 长度>2,依赖中间子串dp[i+1][j-1]dp[i][j] = dp[i+1][j-1] # 核心:中间子串是否是回文else:dp[i][j] = False # 首尾不等,直接非回文# 记录最长回文的起始位置和长度if dp[i][j] and length > max_len: # 原代码漏了dp[i][j]的判断max_len = lengthstart = i# ✅ 修正3:Python切片语法是s[start:end](原代码用了逗号,导致语法错误)return s[start:start + max_len]
