不同容器常见操作的时间复杂度表格(平均 O(1)都是介于 O(1)和 O(n)之间):
操作类型 | 列表 (list ) | 元组 (tuple ) | 集合 (set ) | 字典 (dict ) | 字符串 (str ) |
---|---|---|---|---|---|
索引访问 | O(1) | O(1) | N/A | 平均 O(1) | O(1) |
赋值 | O(1) | 不支持 | N/A | 平均 O(1) | 不支持 |
追加元素 | 平均 O(1) | 不支持 | 平均 O(1) | 平均 O(1) | 不支持 |
插入元素 | O(n) | 不支持 | N/A | N/A | 不支持 |
删除元素 | O(n) | 不支持 | 平均 O(1) | 平均 O(1) | 不支持 |
弹出最后一个元素 | O(1) | 不支持 | N/A | N/A | 不支持 |
弹出指定位置元素 | O(n) | 不支持 | N/A | N/A | 不支持 |
查找元素 | O(n) | O(n) | 平均 O(1) | 平均 O(1) | O(n*m) |
获取长度 | O(1) | O(1) | O(1) | O(1) | O(1) |
切片操作 | O(b-a) | O(b-a) | N/A | N/A | O(b-a) |
集合操作 | N/A | N/A | 见下注释 | N/A | N/A |
连接字符串 | N/A | N/A | N/A | N/A | O(total_length) |
集合操作(set
)详细时间复杂度:
- 交集:O(min(len(set), len(other_set)))
- 并集:O(len(set) + len(other_set))
- 差集:O(len(set))
- 对称差集:O(len(set) + len(other_set))
说明:
- N/A: 表示该操作不适用于该数据结构。
- 平均时间复杂度 和 最坏时间复杂度:对于基于哈希的数据结构(如集合和字典),通常情况下操作是常数时间复杂度 O(1),但在极端情况下(如所有元素都发生哈希冲突),时间复杂度可能退化为 O(n)。
- 切片操作:对于列表和元组,切片操作的时间复杂度取决于切片的长度(即
b-a
),而不是整个容器的大小。 - 查找子串:对于字符串,查找子串的时间复杂度为 O(n*m),其中 n 是主串长度,m 是子串长度。
这个表格提供了更简洁的信息,帮助你快速了解不同数据结构的主要操作时间复杂度。如果你有更多问题或需要进一步的帮助,请随时告诉我!
理清楚 python 传递对象的方式
python 传递对象的方式很神奇,叫做 按对象传递
其实呢就是传递地址,或者说是把一个带地址的 class 封装好了传递过去,也就是说当你将一个对象作为函数参数传递过去时,不像 C/C++ 一样默认复制一份 按值传递,python 则是 默认按引用传递
对于一个参数(param)来说
传参方式 | Python | C/C++ |
---|---|---|
按 值 传递 | fun(param.copy())/(fun(param [:] ))(仅适用列表) | fun(param)(默认) |
按 引用 传递 | fun(param)(默认) | fun(& param) |
一个简单的例子
path = []
def dfs(depth, path):
# 当 depth = 6 时退出深搜
if depth == 6:
return
path.append(depth)
# dfs(depth+1, path)
# > 这里是左值(有地址的对象), 所以是按引用传递
# >>> print 结果为 [0,1,2,3,4,5]
dfs(depth+1, path.copy())
# > 这里是 copy(), 所以是按值传递
# >>> print 结果为 [0,1]
if depth == 1:
print(path)
# 如果按值传递会 print [0,1] 如果按引用传递会 print [0,1,2,3,4,5]
dfs(0, [])
函数写为对象(类)的好处
可以起到 函数声明 的作用
一个简单的例子来说明,类的方法 可以对 全局 起作用,而 函数 只能对其 下方 的作用域起作用
类的方法写法
class Solution:
def preorderTraversal(self, root: TreeNode) -> List [int]:
result = []
# 可以将 preorder 函数在下方声明, 上方调用
self.preorder(root, result)
return result
def preorder(self, node, result):
if node:
result.append(node.val)
self.preorder(node.left, result)
self.preorder(node.right, result)
注意:不要写成子方法!
class Solution:
def preorderTraversal(self, root: TreeNode) -> List [int]:
result = []
# 可以将 preorder 函数在下方声明, 上方调用
self.preorder(root, result)
return result
# 这样缩进就错了!
def preorder(self, node, result):
if node:
result.append(node.val)
self.preorder(node.left, result)
self.preorder(node.right, result)
子函数写法
class Solution:
def preorderTraversal(self, root: TreeNode) -> List [int]:
result = []
preorder(root, result)
return result
def preorder(node, result):
if node:
result.append(node.val)
preorder(node.left, result)
preorder(node.right, result)
# 如果想要这么写, 则会弹出报错'NameError: name 'preorder' is not defined. '
# 说明 preorder 不定义为类的方法的话, 定义在下方的函数就只能对更下方的作用域起作用
NameError: name 'preorder' is not defined. Did you mean: 'byteorder'?
^^^^^^^^
preorder(root, result)
Line 4 in preorderTraversal (Solution.py)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ret = Solution().preorderTraversal(param_1)
Line 38 in _driver (Solution.py)
_driver()
Line 53 in <module> (Solution.py)
说明 preorder 不定义为 类的方法 的话, 定义在下方的函数 就只能对 更下方的作用域 起作用
Leetcode hot100 题
1. 两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
一开始我的解法(太慢了)
1866ms 击败 15.41%
class Solution:
def twoSum(self, nums: List [int], target: int) -> List [int]:
n = len(nums)
for i in range(n-1):
for j in range(i+1, n):
if nums [i] + nums [j] == target:
return [i, j]
return []
现在用的解法(双指针法)
8ms 击败 44.35%
class Solution:
def twoSum(self, nums: List [int], target: int) -> List [int]:
# 构造一个含 index 的数组,同时对其进行排序
matrix = list(enumerate(nums))
matrix.sort(key = lambda x: x [1])
# 构造两个 lambda 函数方便理解
value = lambda x: matrix [x][1]
index = lambda x: matrix [x][0]
# 初始化两个指针
left = 0
right = len(nums)-1
# 循环结束条件为:两指针不相等
while left != right:
if value(left) + value(right) > target:
right-= 1
elif value(left) + value(right) < target:
left+= 1
else:
return [index(left), index(right)]
哈希解法
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
val_dict = dict()
for i, num in enumerate(nums):
other_val = target - num
if other_val not in val_dict.keys():
val_dict[num] = i
else:
return [val_dict[other_val], i]
2. 两数相加
正确解法
class ListNode:
def __init__(self, val = 0, next = None):
self.val = val
self.next = next
class Solution:
def addTwoNumbers(self, l1, l2):
dummy = ListNode() # 创建一个哑节点,方便处理结果链表
cur = dummy
carry = 0
while l1 or l2 or carry:
# 将 carry 作为总数值
if l1:
carry += l1.val
l1 = l1.next
if l2:
carry += l2.val
l2 = l2.next
cur.val = ListNode(carry%10)
cur = cur.next
carry//= 10
return dummy.next # 返回哑节点的下一个节点作为结果链表的头
3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
哈希表(set/dict)维护指针法
15ms 击败 91.72%
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
n = len(s)
ans = 0
left = 0
charSet = set() # 使用集合来存储当前窗口内的字符
for right in range(n): # 遍历字符串,right 作为滑动窗口的右边界
while s [right] in charSet: # 如果当前字符已经在集合中,移动左边界直到没有重复字符
charSet.remove(s [left]) # 移除左边界的字符
left += 1
charSet.add(s [right]) # 将当前字符加入集合
ans = max(ans, right - left + 1) # 更新答案
return ans
0x3f 做法
from collections import Counter
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
ans = 0
# 使用 Counter 来记录当前窗口中每个字符的出现次数
cnt = Counter()
left = 0
# 遍历字符串 s,right 作为右指针,c 为当前字符
for right, c in enumerate(s):
cnt [c] += 1
while cnt [c] > 1:
# 减少左指针所指向字符的计数,并将左指针向右移动,缩小窗口
cnt [s[left]] -= 1
left += 1
# 当前窗口的长度是(right - left + 1)
ans = max(ans, right - left + 1)
return ans
维护指针法
23ms 击败 57.96%
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if not s:
return 0
n = len(s)
ans = 0
left = 0
right = 0
while right <= n-1:
if s [right] not in s [left: right]:
ans = max(ans, right - left + 1)
right+= 1
else:
left+= 1
return ans
8. 字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s)
函数,使其能将字符串转换成一个 32 位有符号整数。
函数 myAtoi(string s)
的算法如下:
- 空格: 读入字符串并丢弃无用的前导空格(
" "
) - 符号: 检查下一个字符(假设还未到字符末尾)为
'-'
还是'+'
。如果两者都不存在,则假定结果为正。 - 转换: 通过跳过前置零来读取该整数,直到遇到非数字字符或到达字符串的结尾。如果没有读取数字,则结果为 0。
- 舍入: 如果整数数超过 32 位有符号整数范围
[−231, 231 − 1]
,需要截断这个整数,使其保持在这个范围内。具体来说,小于−231
的整数应该被舍入为−231
,大于231 − 1
的整数应该被舍入为231 − 1
。
返回整数作为最终结果。
官方解法(利用状态机求解)
INT_MAX = 2 ** 31 - 1
INT_MIN = -2 ** 31
class Automaton:
def __init__(self):
self.state = 'start'
self.sign = 1
self.ans = 0
self.table = {
'start': ['start', 'signed', 'in_number', 'end'],
'signed': ['end', 'end', 'in_number', 'end'],
'in_number': ['end', 'end', 'in_number', 'end'],
'end': ['end', 'end', 'end', 'end'],
}
def get_col(self, c):
if c.isspace():
return 0
if c == '+' or c == '-':
return 1
if c.isdigit():
return 2
return 3
def get(self, c):
self.state = self.table [self.state][self.get_col(c)]
if self.state == 'in_number':
self.ans = self.ans * 10 + int(c)
self.ans = min(self.ans, INT_MAX) if self.sign == 1 else min(self.ans, -INT_MIN)
elif self.state == 'signed':
self.sign = 1 if c == '+' else -1
class Solution:
def myAtoi(self, str: str) -> int:
automaton = Automaton()
for c in str:
automaton.get(c)
return automaton.sign * automaton.ans
我的解法
我的解法是状态机的变体, 但是只能利用在每个状态 顺序已知 的情况下, 不适合 变通 的情况
class Solution:
def myAtoi(self, s: str) -> int:
ture_num = 0
index = 0
negFlag = False
while index < len(s):
while index < len(s) and s [index] == ' ':
index+= 1
while index < len(s) and s [index] in "+-":
if s [index] == '-':
negFlag = True
index+= 1
break
while index < len(s) and (s[index] > = '0' and s [index] <= '9'):
ture_num*= 10
ture_num+= int(s [index])
index+= 1
break
ture_num = -ture_num if negFlag else ture_num
if ture_num > 2**31-1:
ture_num = 2**31-1
elif ture_num < -2**31:
ture_num = -2**31
return ture_num
9.回文数
快速解法
class Solution:
def isPalindrome(self, x: int) -> bool:
return str(x)==str(x)[::-1] #运用了Python的str函数直接转置int数字为字符串,同时利用[::-1]写法直接进行reverse操作
我的解法(没有考虑到 python 自带字符串转置函数)
class Solution:
def isPalindrome(self, x: int) -> bool:
if x < 0:
return False
s = ""
while x:
cur_bit = x%10
s+= str(cur_bit)
x//= 10
while len(s)> 1:
if s [0] != s [-1]:
return False
s = s [1:-1]
return True
10. 正则表达式匹配
给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'
和 '*'
的正则表达式匹配。
'.'
匹配任意单个字符'*'
匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s
的,而不是部分字符串。
示例 1:
输入:s = "aa", p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:s = "aa", p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
提示:
1 <= s.length <= 20
1 <= p.length <= 20
s
只包含从a-z
的小写字母。p
只包含从a-z
的小写字母,以及字符.
和*
。- 保证每次出现字符
*
时,前面都匹配到有效的字符
C++写法
class Solution {
public:
bool isMatch(string s, string p) {
// dp 递推
// 记忆化搜索
int n = s.length();
int m = p.length();
s.insert(s.begin(),'0');
p.insert(p.begin(),'0');
vector <vector<bool> > f(n+1, vector <bool>(m+1, false));
// f [i][j] 表示 s 串前 i 个字符与 p 串前 j 个字符是否匹配
// 答案为 f [n][m]
f [0][0] = true;
for(int j = 1; j <= m; j++){
if(p [j] == '*') f [0][j] = f [0][j-1];
else if (j + 1 > m || p [j+1] != '*') break; // f [0][j] = false
else f [0][j] = true;
}
for(int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
if(p [j] == '*'){
f [i][j] = f [i][j-1];
continue;
}
if(j+1 <= m && p [j+1] == '*'){
// 空串
// 1 个 p [j]
// 若干个 p [j]
f [i][j] = f [i][j-1] ||
(f [i-1][j-1] && (p [j] == '.' || s [i] == p [j])) ||
(f [i-1][j] && (p [j] == '.' || s [i] == p [j]));
} else {
f [i][j] = f [i-1][j-1] && (p [j] == '.' || s [i] == p [j]);
}
}
return f [n][m];
}
};
Python 写法
class Solution:
def isMatch(self, s: str, p: str) -> bool:
n = len(s)
m = len(p)
s = '0' + s
p = '0' + p
f = [[False for _ in range(m+1)] for _ in range(n+1)]
f [0][0] = True
for j in range(1, m+1):
if p [j] == '*':
f [0][j] = f [0][j-1]
elif j+1 > m or p [j+1]!='*':
break
else:
f [0][j] = True
for i in range(1, n+1):
for j in range(1, m+1):
if p [j] == '*':
f [i][j] = f [i][j-1]
continue
if j+1 <= m and p [j+1] == '*':
f [i][j] = f [i][j-1] or\
(f [i-1][j-1] and (p [j] == '.' or s [i] == p [j])) or\
(f [i-1][j] and (p [j] == '.' or s [i] == p [j]))
else:
f [i][j] = f [i-1][j-1] and (p [j] == '.' or s [i] == p [j])
return f [n][m]
11. 盛最多水的容器
给定一个长度为 n
的整数数组 height
。有 n
条垂线,第 i
条线的两个端点是 (i, 0)
和 (i, height[i])
。
找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明: 你不能倾斜容器。
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
该题思路为 任意情况下 给你两条线,该移动谁可能形成更大的容量,同时注意要节省时间性能,实际上可以观察到
面积 = 底部距离 x 最小值(线段长度 1, 线段长度 2) = 底部距离 x 较短线段长度
底部距离的 单调性: 因为指针是从两边向内靠近, 所以底部距离是在不断缩短的, 呈现单调减的特性
较短线段长度的 单调性:
- 固定较短线段 而 移动较长线段 时: 新的较短线段 <= 老的较短线段,所以也呈现单调减的特性
- 固定较长线段 而 移动较短线段 时: 新的较短线段不确定大小关系,所以单调关系也不确定
则:
- 固定较短线段 而 移动较长线段 时: 面积的单调性 = 单调减 x 单调减,所以也呈现单调减的特性
- 固定较长线段 而 移动较短线段 时: 面积的单调性 = 单调减 x 不确定,所以单调关系也不确定
所以不能 固定较短线段移动较长线段,而是要 固定较长线段移动较短线段 尝试是否有更大值
双指针法
class Solution:
def maxArea(self, height: List [int]) -> int:
ans = 0
left = 0
right = len(height)-1
while left < right:
area = (right-left)*min(height [left], height [right])
ans = max(area, ans)
if height [left] < height [right]:
left+= 1
elif height [left] > height [right]:
right-= 1
else:
left+= 1
return ans
15. 三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
本题解法
class Solution:
def threeSum(self, nums: List [int]) -> List [List[int]]:
# 时间复杂度 O(n^2)
# 空间复杂度 O(1)
nums.sort()
ans = []
n = len(nums)
for i in range(n-2):
# i 跳过重复元素 ##################################
x = nums [i]
if i > 0 and x == nums [i-1]:
continue
# 优化一 #########################################
if x + nums [i+1] + nums [i+2] > 0:
break
# 优化二
if x + nums [-2] + nums [-1] < 0:
continue
#################################################
j = i+1
k = n-1
while j < k:
if nums [j] + nums [k] + x > 0:
k-= 1
elif nums [j] + nums [k] + x < 0:
j+= 1
else:
ans.append([x, nums[j], nums [k]])
# j 跳过重复元素 ##########################
while j < k and nums [j] == nums [j + 1]:
j += 1
# k 跳过重复元素
while j < k and nums [k] == nums [k - 1]:
k -= 1
########################################
j += 1
k -= 1
return ans
18. 四数之和
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且 不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
高效做法(剪枝)
class Solution:
def fourSum(self, nums: List [int], target: int) -> List [List[int]]:
quadruplets = list()
if not nums or len(nums) < 4:
return quadruplets
nums.sort()
length = len(nums)
for i in range(length - 3):
if i > 0 and nums [i] == nums [i - 1]:
continue
if nums [i] + nums [i + 1] + nums [i + 2] + nums [i + 3] > target:
break
if nums [i] + nums [length - 3] + nums [length - 2] + nums [length - 1] < target:
continue
for j in range(i + 1, length - 2):
if j > i + 1 and nums [j] == nums [j - 1]:
continue
if nums [i] + nums [j] + nums [j + 1] + nums [j + 2] > target:
break
if nums [i] + nums [j] + nums [length - 2] + nums [length - 1] < target:
continue
left, right = j + 1, length - 1
while left < right:
total = nums [i] + nums [j] + nums [left] + nums [right]
if total == target:
quadruplets.append([nums[i], nums [j], nums [left], nums [right]])
while left < right and nums [left] == nums [left + 1]:
left += 1
left += 1
while left < right and nums [right] == nums [right - 1]:
right -= 1
right -= 1
elif total < target:
left += 1
else:
right -= 1
return quadruplets
正确做法
from typing import List
class Solution:
def fourSum(self, nums: List [int], target: int) -> List [List[int]]:
nums.sort()
n = len(nums)
ans = []
for i in range(n):
if i > 0 and nums [i] == nums [i - 1]:
continue
for j in range(i + 1, n):
if j > i + 1 and nums [j] == nums [j - 1]:
continue
left, right = j + 1, n - 1
while left < right:
total = nums [i] + nums [j] + nums [left] + nums [right]
if total < target:
left += 1
elif total > target:
right -= 1
else:
ans.append([nums[i], nums [j], 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
return ans
我的做法
class Solution:
def fourSum(self, nums: List [int], target: int) -> List [List[int]]:
n = len(nums)
if n < 4:
return []
nums.sort()
ans = []
for i1 in range(n-3):
x = nums [i1]
if i1 > 0 and nums [i1-1] == x:
continue
for i2 in range(i1+1, n-2):
y = nums [i2]
if i2 > 0 and nums [i2-1] == y:
continue
j = i2+1
k = n-1
while j < k:
if (x+y) + nums [j] + nums [k] > target:
k-= 1
elif (x+y) + nums [j] + nums [k] < target:
j+= 1
else:
ans.append([x, y, nums[j], nums [k]])
# 跳过重复项
while j < k and nums [j+1] == nums [j]:
j+= 1
while j < k and nums [k-1] == nums [k]:
k-= 1
j+= 1
k-= 1
return ans
28. 找出字符串中第一个匹配项的下标
简单
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
朴素解法
class Solution {
public:
int strStr(string haystack, string needle) {
// 方法一: 朴素解法
int n = haystack.size(), m = needle.size();
for(int i = 0; i <= n-m; i++){
int j = i, k = 0;
while(k < m && haystack [j] == needle [k]){
j++;
k++;
}
if(k == m) return i;
}
return -1;
}
};
KMP 解法
class Solution {
public:
int strStr(string haystack, string needle) {
// 方法二: KMP 解法
int n = haystack.size(), m = needle.size();
if(m == 0) return 0;
// 设置哨兵
haystack.insert(haystack.begin(),' ');
needle.insert(needle.begin(),' ');
vector <int> next(m+1);
// 预处理 next 数组
for(int i = 2, j = 0; i <= m; i++){
while(j && needle [i]!= needle [j+1]) j = next [j];
if(needle [i] == needle [j+1]) j++;
next [i] = j;
}
// 匹配过程
for(int i = 1, j = 0; i <= n; i++){
while(j && haystack [i] != needle [j+1]) j = next [j];
if(haystack [i] == needle [j+1]) j++;
if(j == m) return i - m;
}
return -1;
}
};
KMP 优化解法
class Solution {
public:
int strStr(string haystack, string needle) {
// 方法三: KMP 优化解法
int n = haystack.size(), m = needle.size();
if(m == 0) return 0;
// 预处理 next 数组
vector <int> next(m + 1);
for(int i = 2, j = 0; i <= m; i++) {
while(j && needle [i - 1] != needle [j]) j = next [j];
if(needle [i - 1] == needle [j]) j++;
next [i] = j;
}
// 预处理 nextval 数组
vector <int> nextval(m + 1);
for(int i = 2; i <= m; i++) {
if (needle [i] == needle [next[i]])
// 这里必须是 nextval [next[j]]而不是 next [next[j]]
// 因为 nextval 前面的部分在遍历时已经经过处理了, 会指向 "祖先" 点, 而 next 并不会
nextval [i] = nextval [next[i]];
else
nextval [i] = next [i];
}
// 匹配过程
for(int i = 0, j = 0; i < n; i++) {
while(j && haystack [i] != needle [j]) j = nextval [j];
if(haystack [i] == needle [j]) j++;
if(j == m) return i - m + 1;
}
return -1;
}
};
32. 最长有效括号
困难
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
提示:
0 <= s.length <= 3 * 104
s[i]
为'('
或')'
动态规划
我的做法(匹配信息存储到一个数组中, 然后变为 <最长True子串问题>)
11ms 击败 30.65%
class Solution:
def longestValidParentheses(self, s: str) -> int:
# 数组索引括号匹配然后寻找最长连续 True 的部分
n = len(s)
if n < 2:
return 0
valid = [False for _ in range(n)]
stack = []
for i in range(n):
if s [i] == '(':
stack.append(i)
else:
if not stack:
pass
else:
temp_i = stack.pop()
valid [temp_i] = True
valid [i] = True
# 对 valid 数组进行处理 : 找到最长连续 True 串
ans = 0
# 对开头进行特殊处理(因为开头无法判断上升沿)
count = int(valid [0])
count_state = valid [0]
for i in range(1, n):
cur = valid [i]
pre = valid [i-1]
if count_state == True:
if pre == True and cur == False:
# 在遇到下降沿时更新答案
ans = max(count, ans)
count_state = False
count = 0
else:
# 正常计数
count+= 1
else:
if pre == False and cur == True:
# 在遇到上升沿时重新开始计数
count_state = True
count = 1
# 在最后一次循环的末尾也要更新 ans
if i == n-1:
ans = max(count, ans)
return ans
42. 接雨水
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
前缀和法
该题的一种思路是使用 前缀和 和 后缀和 来充当墙体,因为雨水都是被两边的“高墙”所拦截的,对 height 数组遍历得到前缀和就是每个坐标 左边的墙 高度,后缀和就是每个坐标 右边的墙 高度,当前这个点能乘放的雨水量就是当前坐标,左右两墙的较矮墙 的高度减去当前坐标本身的高度,对所有坐标如是操作,则能得到最终乘放的雨水量。
精简版
35ms 击败 21.08%
class Solution:
def trap(self, height: List [int]) -> int:
n = len(height)
pre = [0 for _ in range(n)]
suf = [0 for _ in range(n)]
pre [0] = height [0]
suf [-1] = height [-1]
# 前缀和 后缀和
for i in range(1, n):
pre [i] = max(height [i], pre [i-1])
suf [(n-1)-i] = max(height [(n-1)-i], suf [(n-1)-(i-1)])
return sum(min(pre [i], suf [i]) for i in range(n)) - sum(height)
具体版
39ms 击败 12.19%
class Solution:
def trap(self, height: List [int]) -> int:
n = len(height)
pre_max = [0 for _ in range(n)]
suf_max = [0 for _ in range(n)]
pre_max [0] = height [0]
suf_max [-1] = height [-1]
area = 0
# 前缀和 后缀和
for i in range(1, n):
pre_max [i] = max(height [i], pre_max [i-1])
suf_max [(n-1)-i] = max(height [(n-1)-i], suf_max [(n-1)-(i-1)])
# 遍历计算区域总面积
for val, pre, suf in zip(height, pre_max, suf_max):
area += min(pre, suf) - val
return area
双指针法
11ms 击败 88.01%
另一种思路是利用双指针的方法来优化这个问题。具体思路如下:
- 使用两个指针
left
和right
分别指向数组的两端。 - 维护两个变量
max_left
和max_right
,分别记录从左到右和从右到左的最大高度。 - 每次移动较小的一端指针,并根据当前的最大高度计算可以存水的量。
为什么移动较小的一端?
- 确定存水量的关键:
- 对于每一个位置
i
,它能存储的水量取决于它左边和右边的最大高度中的较小值(即min(max_left, max_right)
)。 - 如果当前柱子的高度小于这个最小值,则可以在该柱子上方存储水。
- 对于每一个位置
- 双指针的工作原理:
- 我们维护两个指针
left
和right
,分别从数组的两端开始向中间靠拢。 - 同时,我们也维护两个变量
max_left
和max_right
,分别记录从左到右和从右到左的最大高度。
- 我们维护两个指针
- 如何决定移动哪个指针:
- 当
max_left <= max_right
时,说明左边的最大高度小于或等于右边的最大高度。这意味着对于left
指针指向的位置,它的最大存水量由max_left
决定。- 因此,我们可以安全地移动
left
指针,并根据max_left
来计算当前位置可以存储的水量。
- 因此,我们可以安全地移动
- 反之,当
max_left > max_right
时,说明右边的最大高度小于左边的最大高度。这意味着对于right
指针指向的位置,它的最大存水量由max_right
决定。- 因此,我们可以安全地移动
right
指针,并根据max_right
来计算当前位置可以存储的水量。
- 因此,我们可以安全地移动
- 当
class Solution:
def trap(self, height: List [int]) -> int:
left = 0
right = len(height) - 1
ans = 0
pre = height [left]
suf = height [right]
while left < right:
if height [left] < height [right]:
left += 1
pre = max(height [left], pre)
ans += pre - height [left]
else:
right -= 1
suf = max(height [right], suf)
ans += suf - height [right]
return ans
46. 全排列
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
提示:
1 <= nums.length <= 6
-10 <= nums[i] <= 10
nums
中的所有整数 互不相同
状态树法
这道题目中状态树做法和深搜做法是一模一样的
class Solution:
def permute(self, nums: List [int]) -> List [List[int]]:
n = len(nums)
s = [False] * n # 用于标记是否已使用过该位置的数
path = [] # 当前构建的排列
ans = [] # 存储所有的排列
def dfs(iterator):
if iterator == n: # 完成一次排列
ans.append(path.copy()) # 将当前排列加入到结果集中
return
for i in range(n):
if not s [i]: # 如果第i个数还没有被使用
s [i] = True # 标记为已使用
path.append(nums [i]) # 将该数添加到当前排列
dfs(iterator + 1) # 递归调用以处理下一个位置
path.pop() # 回溯,撤销上一步的选择
s [i] = False # 取消对该数的使用标记
dfs(0)
return ans
深度搜索法
class Solution:
def permute(self, nums: List [int]) -> List [List[int]]:
n = len(nums)
ans = []
path = []
used = [False for _ in range(n)]
def dfs(iterator):
if iterator == n:
ans.append(path.copy())
return
for i in range(n):
if not used [i]:
used [i] = True
path.append(nums [i])
dfs(iterator+1)
path.pop()
used [i] = False
dfs(0)
return ans
49. 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
提示:
1 <= strs.length <= 104
0 <= strs[i].length <= 100
strs[i]
仅包含小写字母
总结对比表
- N 是字符串数组的长度
- K 是字符串的最大长度
特性/方法 | 方法 1:排序后分组 | 方法 2:质数乘积哈希 | 方法 3:字符计数表 |
---|---|---|---|
时间复杂度 | O(N * K log K) | O(N * K) | O(N * K) |
空间复杂度 | O(N * K) | O(N + 26) ≈ O(N) | O(N + 26) ≈ O(N) |
优点 | 实现简单,易于理解 | 哈希计算快速,避免了排序操作 | 避免了排序操作,且键值对直接反映了字符频率 |
缺点 | 排序操作增加了时间复杂度 | 质数乘积可能导致数值溢出,特别是对于较长的字符串 | 对于每个字符串都需要一个固定大小的计数表 |
适用场景 | 字符串较短或数量较少的情况 | 字符串较长但数量适中,且不担心整数溢出 | 字符串长度适中,且希望避免排序操作 |
稳定性 | 稳定(相同字母异位词始终生成相同的键) | 不稳定(依赖于质数乘积的结果,可能因整数溢出导致冲突) | 稳定(相同字母异位词始终生成相同的键) |
字典排序法
11ms 击败 92.18%
class Solution:
def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
if len(strs)< 2:
return [strs]
ans = {}
for s in strs:
temp = "".join(sorted(s))
ans [temp] = ans.get(temp, []) + [s]
return list(ans.values())
哈希质数相乘法
15ms 击败 54.53%
prime_number = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101]
numalpha_table = {i: j for i, j in zip("abcdefghijklmnopqrstuvwxyz", prime_number)}
class Solution:
def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
if len(strs)< 2:
return [strs]
def hash_mul(string):
result = 1
for char in string:
result*= numalpha_table [char]
return result
ans = {}
for s in strs:
temp = hash_mul(s)
ans [temp] = ans.get(temp, []) + [s]
return list(ans.values())
哈希字典计数法
23ms 击败 18.44%
class Solution:
def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
if len(strs) < 2:
return [strs]
result = {}
for s in strs:
count_table = [0]*26
for c in s:
count_table [ord(c)-ord('a')] += 1
key = tuple(count_table)
result [key] = result.get(key, [])+[s]
return list(result.values())
我的字典计数法(超时)
超出时间限制
111 / 126 个通过的测试用例
class Solution:
def groupAnagrams(self, strs: List [str]) -> List [List[str]]:
if len(strs)< 2:
return [strs]
ans = {}
ans_dict = {}
for i, string in enumerate(strs):
temp_dict = Counter(string)
if not ans:
ans [string] = [string]
ans_dict [string] = temp_dict
# 从头遍历所有已知的异位词字典, 如果不在, 将自己作为新的 key 加入
else:
Flag_without = True
for key in ans_dict:
if temp_dict == ans_dict [key]:
ans [key].append(string)
Flag_without = False
break
# 遍历完如果不在
if Flag_without:
ans [string] = [string]
ans_dict [string] = temp_dict
return list(ans.values())
53. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
进阶: 如果你已经实现复杂度为 O(n)
的解法,尝试使用更为精妙的 分治法 求解。
这道题的所有方法总结如下
总结对比表
方法 | 时间复杂度 | 空间复杂度 | 备注 |
---|---|---|---|
动态规划 | O(n) | O(n) / O(1) | 可优化至 O(1) |
Kadane 算法 | O(n) | O(1) | 最优选择之一 |
前缀和方法 | O(n) | O(n) | 较多额外空间 |
双循环暴力法 | O(n^2) | O(1) | 不推荐用于大规模数据 |
分治法 | O(n log n) | O(log n) | 适用于特定场景 |
Kadane 算法(类似于动态规划, 但是需要的空间更小, 一遍下来只维护两个最大值)
63ms 击败 78.78%
# 时间复杂度 O(n)
# 空间复杂度 O(1)
class Solution:
def maxSubArray(self, nums: List [int]) -> int:
max_current = max_global = nums [0]
for num in nums [1:]:
max_current = max(num, max_current + num)
if max_current > max_global:
max_global = max_current
return max_global
动态规划
147ms 击败 8.87%
# 时间复杂度 O(n)
# 空间复杂度 O(n)
class Solution:
def maxSubArray(self, nums: List [int]) -> int:
dp = [-float('inf') for _ in range(len(nums))]
dp [0] = nums [0]
for i in range(1, len(nums)):
dp [i] = max(dp [i-1]+nums [i], nums [i])
return max(dp)
0x3f 的前缀和(想到了)
101ms 击败 36.14%
# 时间复杂度 O(n)
# 空间复杂度 O(n)
class Solution:
def maxSubArray(self, nums: List [int]) -> int:
ans = -inf
min_pre_sum = pre_sum = 0
for x in nums:
pre_sum += x # 当前的前缀和
ans = max(ans, pre_sum - min_pre_sum) # 减去前缀和的最小值
min_pre_sum = min(min_pre_sum, pre_sum) # 维护前缀和的最小值
return ans
双循环暴力法
超出时间限制
202 / 210 个通过的测试用例
# 时间复杂度 O(n^2)
# 空间复杂度 O(1)
class Solution:
def maxSubArray(self, nums: List [int]) -> int:
# 初始化
n = len(nums)
ans = -float('inf') #或者 ans = min(nums)
# 暴力法双重循环(超时 202 / 210 个通过的测试用例)
for i in range(n):
pre = 0
for j in range(i, n):
pre+= nums [j]
if pre > ans:
ans = pre
return ans
分治法(太难想了, 不强求)
56. 合并区间
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
提示:
1 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104
执行用时分布
我的做法
3ms 击败 93.57%
class Solution:
def merge(self, intervals: List [List[int]]) -> List [List[int]]:
if len(intervals) <= 1:
return intervals
start = lambda x: x [0]
end = lambda x: x [1]
# 首先排序确定开始时间顺序
intervals.sort(key = lambda x: start(x))
result = [intervals[0]]
for i in range(1, len(intervals)):
# 大于等于都要包含, 否则会错漏 [1,4][4,5] 这样的情况
if end(result [-1]) >= start(intervals [i]):
temp_result = result [-1]
result.pop()
# 结束时间不定, 所以取最大值即可
temp_end = max(end(intervals [i]), end(temp_result))
result.append([start(temp_result), temp_end])
else:
result.append(intervals [i])
return result
简洁做法
3ms
class Solution:
def merge(self, intervals: List [List[int]]) -> List [List[int]]:
if len(intervals)< 2:
return intervals
result = []
intervals.sort(key = lambda x: x [0])
for interval in intervals:
if len(result) == 0 or interval [0] > result [-1][1]:
result.append(interval)
else:
result [-1][1] = max(result [-1][1], interval [1])
return result
57. 插入区间
给你一个 无重叠的 , 按照区间起始端点排序的区间列表 intervals
,其中 intervals[i] = [starti, endi]
表示第 i
个区间的开始和结束,并且 intervals
按照 starti
升序排列。同样给定一个区间 newInterval = [start, end]
表示另一个区间的开始和结束。
在 intervals
中插入区间 newInterval
,使得 intervals
依然按照 starti
升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。
返回插入之后的 intervals
。
注意 你不需要原地修改 intervals
。你可以创建一个新数组然后返回它。
示例 1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:
输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
提示:
0 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 105
intervals
根据starti
按 升序 排列newInterval.length == 2
0 <= start <= end <= 105
合并区间改编过来的做法
class Solution:
def insert(self, intervals: List [List[int]], newInterval: List [int]) -> List [List[int]]:
# 改动一: intervals 列表边界条件
if len(intervals) == 0:
return [newInterval]
start = lambda x: x [0]
end = lambda x: x [1]
# 首先排序确定开始时间顺序
# 改动二: 将 newInterval 插入 intervals 开始作为合并整体遍历
intervals.append(newInterval)
intervals.sort(key = lambda x: start(x))
result = [intervals[0]]
for i in range(1, len(intervals)):
# 大于等于都要包含, 否则会错漏 [1,4][4,5] 这样的情况
if end(result [-1]) >= start(intervals [i]):
temp_result = result [-1]
result.pop()
# 结束时间不定, 所以取最大值即可
temp_end = max(end(intervals [i]), end(temp_result))
result.append([start(temp_result), temp_end])
else:
result.append(intervals [i])
return result
70. 爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
动态规划
class Solution:
def climbStairs(self, n: int) -> int:
# 方法二
if n < 2:
return 1
dp = [0 for _ in range(n+1)]
dp [0] = dp [1] = 1
for i in range(2, n+1):
dp [i] = dp [i-1] + dp [i-2]
return dp [-1]
# 方法一
# if n < 2:
# return dp [n]
# dp = [1,1]
# for i in range(2, n+1):
# dp.append(dp [-1] + dp [-2])
# return dp [-1]
77. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
class Solution:
def combine(self, n: int, k: int) -> List [List[int]]:
if n == 0 or k > n:
return []
elif n == k:
return [list(range(1, n+1))]
# 回溯函数(组合类型)
# params: 待挑选数组长度
# params: 每个组合需要的数组长度
# params: 待挑选的数组开始索引(即迭代器 iterator, 用迭代器普适性更强)
# params: 挑选过程中的路径 path
def backtracking(n, k, begin, path):
if len(path) == k:
ans.append(path.copy())
return
for i in range(begin, n+1):
path.append(i)
# 注意传递的是 i+1, 而不是 begin+1
# 意思就是从当前挑选的数往后挑选, 不会回头看
# begin 表示的不是当前的数, 而是这一轮 dfs 开始的数
backtracking(n, k, i+1, path)
path.pop()
ans = []
backtracking(n, k,1, [])
return ans
78. 子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums
中的所有元素 互不相同
class Solution:
def subsets(self, nums: List [int]) -> List [List[int]]:
# 每次选择拿不拿, 迭代器一直往前走, 迭代器结束时将 path 加入 ans 并 return
ans = []
path = []
# 回溯函数(子集类型)
# params: 待挑选的集合
# params: 迭代器 iterator
# params: 挑选过程中的路径 path
def backtracking(nums, iterator, path):
# 当 iterator 到底时加入 path
if iterator == len(nums):
ans.append(path.copy())
return
# 加入当前 iterator 的数
path.append(nums [iterator])
backtracking(nums, iterator+1, path)
path.pop()
# backtracking(nums, iterator+1, path+[nums[iterator]])
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>> 此处为一个右值副本, 不是左值所以不是按引用传递
# 不加入当前 iterator 的数
backtracking(nums, iterator+1, path)
# >>>>>>>>>>>>>>>>>>>>>>>>>>> 这里是左值, 所以是按引用传递
backtracking(nums,0, [])
return ans
90. 子集 II
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
class Solution:
def subsetsWithDup(self, nums: List [int]) -> List [List[int]]:
# 首先进行排序获得以便利用顺序特性: 该题中的特性为, 按顺序来取的话子集中也只会是按顺序排列, 那么元组就会自动过滤相同的子集, 如果不排序, cnt 相同的元组可能会因为顺序不同不能过滤掉, 所以要先进行排列
nums.sort()
# 该题与子集 I 的不同点就在于需要去重:
# 1.利用 set 过滤不同的元组(之所以是元组(tuple)是因为列表(list)不具有 hash 性不能加入 set)
# 2.利用排序过滤 cnt 相同而顺序不同的数组
ans_set = set()
path = []
def backtracking(nums, iterator, path):
if iterator == len(nums):
ans_set.add(tuple(path))
return
backtracking(nums, iterator+1, path)
backtracking(nums, iterator+1, path+[nums[iterator]])
backtracking(nums,0, [])
return list(ans_set)
94. 二叉树的中序遍历
给定一个二叉树的根节点 root
,返回 它的 * 中序 * 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
- 树中节点数目在范围
[0, 100]
内 -100 <= Node.val <= 100
二叉树定义
# Definition for a binary tree node.
class TreeNode:
def __init__(self, val = 0, left = None, right = None):
self.val = val
self.left = left
self.right = right
迭代法(需要记住, 手动维护一个栈)
class Solution:
def inorderTraversal(self, root: Optional [TreeNode]) -> List [int]:
stack = []
ans = []
cur = root
while cur != None or stack != []:
# 如果上一个节点存在子树则向左子树深入
if cur != None:
stack.append(cur)
cur = cur.left
# 如果不存在子树则回到上一个节点, 并将上一个节点加入答案, 向其右子树走一个节点
else:
cur = stack.pop()
ans.append(cur.val)
cur = cur.right
return ans
递归法(简单)
class Solution:
def inorderTraversal(self, root: Optional [TreeNode]) -> List [int]:
def inorderTraversalHandler(node):
# 退出条件
if node == None:
return
# 如果本题是先序遍历 #################
# ans.append(node.val)
####################################
inorderTraversalHandler(node.left)
ans.append(node.val)
inorderTraversalHandler(node.right)
# 如果本题是后序遍历 #################
# ans.append(node.val)
####################################
ans = []
inorderTraversalHandler(root)
return ans
102. 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -1000 <= Node.val <= 1000
简练做法(用 深度搜索+先序遍历 实现 层序遍历)
class Solution:
def levelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
def dfs(root, depth, ans):
if root == None:
return
if depth >= len(ans):
ans.append([])
ans [depth].append(root.val)
dfs(root.left, depth+1 , ans)
dfs(root.right, depth+1, ans)
ans = []
dfs(root, 0, ans)
return ans
我的做法
import queue
class Solution:
def levelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
if root == None:
return []
elif root.left == None and root.right == None:
return [[root.val]]
my_queue = queue.Queue()
my_queue.put(root)
# 加入第一层标识符
my_queue.put('-')
path = []
ans = [[root.val]]
while not my_queue.empty():
cur = my_queue.get()
# 每一层结尾检测到标识符将 path 加入 ans, 清除之前的 path, 并重置标识位
if cur == '-':
if pre == '-':
break
if path:
ans.append(path.copy())
path = []
my_queue.put('-')
pre = cur
continue
# 如果 cur 存在左右子树就将子树加入队列, 子树的值加入 path
if cur.left:
my_queue.put(cur.left)
path.append(cur.left.val)
if cur.right:
my_queue.put(cur.right)
path.append(cur.right.val)
pre = cur
return ans
103. 二叉树的锯齿形层序遍历
给你二叉树的根节点 root
,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[20,9],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -100 <= Node.val <= 100
class Solution:
def zigzagLevelOrder(self, root: Optional [TreeNode]) -> List [List[int]]:
def dfs(root, depth, ans):
if root == None:
return
if depth >= len(ans):
ans.append([])
if depth%2 == 0:
ans [depth].append(root.val)
else:
ans [depth].insert(0, root.val)
dfs(root.left, depth+1 , ans)
dfs(root.right, depth+1, ans)
ans = []
dfs(root, 0, ans)
return ans
144. 二叉树的前序遍历
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
示例 1:
输入: root = [1, null,2,3]
输出: [1,2,3]
解释:
示例 2:
输入: root = [1,2,3,4,5, null,8, null, null,6,7,9]
输出: [1,2,4,5,6,7,3,8,9]
解释:
示例 3:
输入: root = []
输出: []
示例 4:
输入: root = [1]
输出: [1]
提示:
- 树中节点数目在范围
[0, 100]
内 -100 <= Node.val <= 100
以前写的递归写法
class TreeNode:
def __init__(self, val = 0, left = None, right = None):
self.val = val
self.left = left
self.right = right
class Solution:
def preorderTraversal(self, root: TreeNode) -> List [int]:
result = []
self.preorder(root, result)
return result
def preorder(self, node, result):
if node:
result.append(node.val)
self.preorder(node.left, result)
self.preorder(node.right, result)
200. 岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j]
的值为'0'
或'1'
DFS 解法
237ms 击败 92.72%
class Solution:
def numIslands(self, grid: List [List[str]]) -> int:
rows = len(grid)
cols = len(grid [0])
ans = 0
def dfs(row, col):
if row < 0 or row > rows-1 or col < 0 or col > cols-1 or grid [row][col] == "0":
return
else:
grid [row][col] = "0" # grid [row][col] = 0 一开始写成具体数值 0, 错误了
dfs(row+1, col)
dfs(row-1, col)
dfs(row, col+1)
dfs(row, col-1)
for i in range(rows):
for j in range(cols):
if grid [i][j] == "1":
ans+= 1
dfs(i, j)
return ans
209. 长度最小的子数组
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度 。 如果不存在符合条件的子数组,返回 0
。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 104
进阶:
- 如果你已经实现
O(n)
时间复杂度的解法, 请尝试设计一个O(n log(n))
时间复杂度的解法。
自己的写法(18/21 用例)
class Solution:
def minSubArrayLen(self, target: int, nums: List [int]) -> int:
n = len(nums)
left = 0
right = 0
ans = float('inf')
# [left: right] 为左闭右开区间
#当 right 小于数组长度时一直前进, 如果到了而且 sum(nums [left: right])小于 target 就可以退出了
while not (right >= n and sum(nums [left: right])< target):
if sum(nums [left: right])< target:
right+= 1
else:
ans = min(right-left, ans)
left+= 1
return ans if ans < 65536 else 0
问题
- 重复计算
sum(nums[left:right])
:在每次循环中都调用sum(nums[left:right])
会导致性能瓶颈,因为这个操作的时间复杂度是 O(n),其中 n 是子数组的长度。 - 终止条件的问题:当前的终止条件
while not (right >= n and sum(nums[left:right])<target)
可能会导致逻辑错误,特别是在处理边界情况时。 - 返回值检查
ans if ans<65536 else 0
:这种做法虽然可以工作,但不如直接比较ans
是否仍为初始值float('inf')
来判断是否有符合条件的子数组直观。
改进建议
为了提高效率并修正逻辑错误,我们可以使用滑动窗口的方法来避免重复计算子数组的和,并优化终止条件。下面是改进后的代码示例:
通义千问写法
from typing import List
class Solution:
def minSubArrayLen(self, target: int, nums: List [int]) -> int:
n = len(nums)
left = 0
total = 0
min_len = float('inf')
for right in range(n):
total += nums [right] # 增加右指针指向的元素到总和
# 当总和大于等于目标值时,尝试缩小窗口大小
while total >= target:
min_len = min(min_len, right - left + 1) # 更新最小长度
total -= nums [left] # 减去左指针指向的元素从总和
left += 1 # 移动左指针
return min_len if min_len != float('inf') else 0
关键点解释
- 滑动窗口方法:通过维护一个窗口(由
left
和right
两个指针定义),我们可以在遍历数组的同时动态调整窗口大小,以找到满足条件的最小子数组。 - 避免重复计算:通过在每一步更新窗口的总和,而不是每次都重新计算整个子数组的和,大大提高了效率。
- 终止条件:新的实现不再需要复杂的终止条件,而是自然地通过遍历数组完成任务。
- 结果检查:如果
min_len
仍为初始值float('inf')
,说明没有找到符合条件的子数组,因此返回 0。
这种方法的时间复杂度是 O(n),每个元素最多被访问两次(一次通过右指针,一次通过左指针),空间复杂度是 O(1)。
407. 接雨水 II
给你一个 m x n
的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
示例 1:
输入: heightMap = [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]
输出: 4
解释: 下雨后,雨水将会被上图蓝色的方块中。总的接雨水量为1+2+1=4。
示例 2:
输入: heightMap = [[3,3,3,3,3],[3,2,2,2,3],[3,2,1,2,3],[3,2,2,2,3],[3,3,3,3,3]]
输出: 10
提示:
m == heightMap.length
n == heightMap[i].length
1 <= m, n <= 200
0 <= heightMap[i][j] <= 2 * 104
我的解法( 19 / 42 )
class Solution:
def trapRainWater(self, heightMap: List [List[int]]) -> int:
rows = len(heightMap)
cols = len(heightMap [0])
ans = 0
pre_left = [[0 for _ in range(cols)] for _ in range(rows)]
suf_right = [[0 for _ in range(cols)] for _ in range(rows)]
pre_up = [[0 for _ in range(cols)] for _ in range(rows)]
suf_down = [[0 for _ in range(cols)] for _ in range(rows)]
for i in range(rows):
pre_left [i][0] = heightMap [i][0]
suf_right [i][-1] = heightMap [i][-1]
for j in range(cols):
pre_up [0][j] = heightMap [0][j]
suf_down [-1][j] = heightMap [-1][j]
for i in range(rows):
for j in range(1, cols):
pre_left [i][j] = max(heightMap [i][j], pre_left [i][j-1])
suf_right [i][(cols-1)-j] = max(heightMap [i][(cols-1)-j], suf_right [i][(cols-1)-(j-1)])
for j in range(cols):
for i in range(1, rows):
pre_up [i][j] = max(heightMap [i][j], pre_up [i-1][j])
suf_down [(rows-1)-i][j] = max(heightMap [(rows-1)-i][j], suf_down [(rows-1)-(i-1)][j])
for i in range(rows):
for j in range(cols):
ans += min(pre_left [i][j], suf_right [i][j], pre_up [i][j], suf_down [i][j]) - heightMap [i][j]
return ans
713. 乘积小于 K 的子数组
给你一个整数数组 nums
和一个整数 k
,请你返回子数组内所有元素的乘积严格 小于 k
的连续子数组的数目。
示例 1:
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2:
输入:nums = [1,2,3], k = 0
输出:0
提示:
1 <= nums.length <= 3 * 104
1 <= nums[i] <= 1000
0 <= k <= 106
我的写法
class Solution:
def numSubarrayProductLessThanK(self, nums: List [int], k: int) -> int:
n = len(nums)
left = 0
ans = 0
mul = 1
for right in range(n):
mul *= nums [right]
temp_left = left
temp_mul = mul
while tmep_mul/nums [temp_left] >= k: //and left < right
temp_mul/= nums [temp_left]
temp_left+= 1
ans += right - temp_left
return ans
主要问题
- 缩进不一致:Python 对缩进非常敏感,必须使用一致的缩进方式(全部使用空格或全部使用制表符)。你当前的代码在不同的地方混用了制表符和空格,导致了
TabError: inconsistent use of tabs and spaces in indentation
错误。 - 变量名拼写错误:
tmep_mul
应该是temp_mul
。 - 逻辑错误:
- 在更新
ans
时,应该基于当前有效的子数组长度。 - 当
mul >= k
时,需要移动左指针来减少乘积,而不是通过临时变量来进行调整。
- 在更新
修正后的代码
通义千问写法
from typing import List
class Solution:
def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
if k <= 1:
return 0
n = len(nums)
left = 0
ans = 0
mul = 1
for right in range(n):
mul *= nums[right]
# 移动左指针以保证乘积小于k
while mul >= k and left <= right:
mul /= nums[left]
left += 1
# 当前有效子数组的数量是(right - left + 1)
ans += right - left + 1
return ans
关键点解释
- 滑动窗口方法:我们使用两个指针
left
和right
来表示当前窗口的边界。当窗口内所有元素的乘积大于等于k
时,我们通过移动左指针来缩小窗口,直到乘积小于k
。 - 计算有效子数组数量:每当右指针向右移动一位时,所有以当前
right
结尾且乘积小于k
的子数组数量为(right - left + 1)
。这是因为从left
到right
之间的每一个起点都构成一个有效的子数组。 - 处理特殊情况:如果
k <= 1
,那么没有任何子数组的乘积可以小于k
,因此直接返回 0。