性感100题
着重解决一下hot100题,下面的实现都以python进行。
哈希表
哈希表算是最常用的数据结构之一,对于python而言,哈希表的选择有很多,最简单的是dict()
,初次之外还有Collections
中的OrderedDict
, Counter
等等,当然,这些都是会用到的,只是具体的情况不同。
首先对于dict()
其就是一个简单的字典,算是最小的一个hash类别,其在简单场景下可以轻松的使用,并可以使用字典推导式来简洁写法。OrderedDict
使用场景较少,需要用到的时候再说。Counter
算是对可迭代数据的一个处理,对可迭代的数据进行一个计数统计。接下来就是三道哈希表的
1. 两数之和
- 从一个无序数组中找到两个数字使得和等于target
49. 字母异位词分组
- 将一个列表中的不同字符串,根据是否包含完全一样的字符串分到不同的数组中
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
128. 最长连续序列
- 找到最长的连续的序列长度
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
双指针
283. 移动零
- 原地把0放到数组末尾
11. 盛最多水的容器
- 计算两个竖线中间容纳水的最大值
15. 三数之和
- 找到三个数字的和为0
42. 接雨水
- 接雨水
滑动窗口
需要单调性
3. 无重复字符的最长子串
- 找到没有重复字符的子串
438. 找到字符串中所有字母异位词
- 前面异位词的简单升级版
子串
560. 和为 K 的子数组
- 找到和为k的连续数组
239. 滑动窗口最大值
- 返回序列中窗口的最大值列表
76. 最小覆盖子串
- 找到覆盖子串的最小长度子串
普通数组
53. 最大子数组和
- 找到最大的连续的子数组的和
56. 合并区间
- 合并区间
189. 轮转数组
- 数组右移k位
- 给你一个数组,对每个元素计算$prod(nums)/nums[i]$ 但是不能除法
41. 缺失的第一个正数
- 找到一个数组里缺失的第一个正数
矩阵
73. 矩阵置零
- 将0所在的行列置0
54. 螺旋矩阵
- 螺旋输出数组
48. 旋转图像
- 旋转一下矩阵90度
240. 搜索二维矩阵 II
- 行列均是升序,高效查找到某个target值
链表
160. 相交链表
- 两个列表找到相交的节点
206. 反转链表
- 反转链表
234. 回文链表
- 判断链表是不是回文的
141. 环形链表
- 看看链表是否有环
142. 环形链表 II
- 找到环的位置
21. 合并两个有序链表
- 合并有序链表
2. 两数相加
- 把链表表示的数值求和
19. 删除链表的倒数第 N 个结点
- 删除链表的倒数第 N 个结点
24. 两两交换链表中的节点
- 两两交换链表中的节点
25. K 个一组翻转链表
- 以k个单位为一组反转链表
树
94. 二叉树的中序遍历
- 二叉树中序遍历
104. 二叉树的最大深度
- 二叉树的最大深度
226. 翻转二叉树
- 翻转兄弟节点
101. 对称二叉树
- 判断是否关于root结点轴对称
543. 二叉树的直径
- 找到最长的路径
102. 二叉树的层序遍历
- 层序遍历二叉树
108. 将有序数组转换为二叉搜索树
- 将有序数组转换为二叉搜索树
98. 验证二叉搜索树
- 判断是不是一个二叉搜索树
230. 二叉搜索树中第 K 小的元素
- 找二叉搜索树中第k小的元素
199. 二叉树的右视图
- 想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
114. 二叉树展开为链表
- 将一个二叉树前序遍历展开成链表
105. 从前序与中序遍历序列构造二叉树
- 根据先序和中序遍历构造二叉树
437. 路径总和 III
- 求二叉树里节点值之和等于
sum
的路径数量
236. 二叉树的最近公共祖先
- 给两个节点找祖先
图论
200. 岛屿数量
- 计算岛屿的数量
994. 腐烂的橘子
- 腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂
207. 课程表
- 单项依赖的课程能否学完
回溯
回溯最经典的题目就是N皇后问题了,对于回溯问题之前并没有详细的刷过,因此这次得重新理解一下了。首先,对于python是有原生的permute函数的:
from itertools import permutations
return list(permutations(nums))
但是这一点只是应用,关键是如何更好的理解这个过程。从名字就可以看出来,这类题目的核心就是构建一棵树并逐渐添加节点,最后枚举叶子节点即可,因此通常来说并不是特别复杂吗?显然不是,对于简单的题目可以直接permute,但是对于复杂的场景,往往还需要去重等操作,也就是接下来要学习的了。
46. 全排列
- 返回数组的全排列列表
78. 子集
- 返回数组所有不相同的子集
17. 电话号码的字母组合
- 返回每个数字表示的字符所能组成的单词可能
39. 组合总和
- 返回能够得到某个数的数字所有数字集合(找零钱)
22. 括号生成
- 给定n生成符合条件的n个括号的所有可能字符串
79. 单词搜索
- 棋盘中是否包含某个单词可以通过dfs找到
131. 分割回文串
- 将字符串分割成回文串的集合。
51. N 皇后
- 皇后摆放且不能对角线上下左右存在
二分查找
在印象中二分其实是比较常见但是整体来说比较模板化的题目,所以先记下一个模板:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return left
35. 搜索插入位置
- 如果查到数字就返回对应的位置,否则返回应该插入的位置
74. 搜索二维矩阵
- 二维数组从左往右从上往下递增,查找
34. 在排序数组中查找元素的第一个和最后一个位置
- 找target第一个和最后一个位置
33. 搜索旋转排序数组
- 数组根据某个值旋转之后查找到对应的元素
153. 寻找旋转排序数组中的最小值
- 找旋转排序数组最小值
4. 寻找两个正序数组的中位数(not done)
- 找两个升序数组的中位数
栈
20. 有效的括号
- 判断字符串是否是正确的括号
155. 最小栈
- 数据结构题目实现一个最小栈
题解-哈希表
1. 两数之和
- 思路1: 使用排序之后使用双指针扫描即可,复杂度为$O(nlogn)$算是比较高的复杂度了:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
nums=[[j, i] for i,j in enumerate(nums)]
nums=sorted(nums)
print(nums)
ln=len(nums)
l, r = 0, ln-1
while l < r:
if nums[l][0]+nums[r][0] > target:
r -=1
elif nums[l][0]+nums[r][0]<target:
l+=1
else:
return [nums[l][1],nums[r][1]]
- 思路2:既然这一部分是hash的相关内容,那么自然也可以使用hash来解决上述问题,构建一个hash表,从中查询元素,直到两者和等于target
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashtable = dict()
for i, num in enumerate(nums):
if target - num in hashtable:
return [hashtable[target - num], i]
hashtable[nums[i]] = i
return []
相较于官方的题解,自己的思路果然还是复杂了一点。官解在进行遍历时就顺便查找其中的值,使得整体代码更简洁了。
49. 字母异位词分组
- 思路:对字符重排序之后作为字典的键,并存储每个repo的index即可 ,整体来说也不复杂:
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
res = []
d = dict()
ld = 0
for i in strs: # 注意str sorted是对字符进行排序,还需要试用一下join
si = "".join(sorted(i))
if d.get(si, None) is None:
d[si] = ld
res.append([])
res[ld].append(i)
ld += 1
else:
nd = d[si]
res[nd].append(i)
return res
唯一要注意的就是其中的sorted对于字符串实际上是先list后sorted。
128. 最长连续序列
- 思路:利用字典记录一下,之后每次查询+i和-i个元素,并想办法把这些查过的元素删除防止多次计算。这里有两个很坑的地方,首先,对于python,set的查询速度同样为
O(1)
,因此可以考虑使用set直接做in操作查询。其次,使用字典删除元素的复杂度会偏高,导致超时,因此合理的做法是利用set做查询,之后尝试标记防止多次查询。实际的实现更加细节:
#相对来说最完美的
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
if not nums:
return 0
num_set = set(nums) # 转换为集合,去重
max_length = 0
for num in num_set:
# 只有当 num-1 不存在时,才开始计算序列
# 这确保了每个序列只被计算一次
if num - 1 not in num_set:
current_num = num
current_length = 1
# 向后查找连续序列
while current_num + 1 in num_set:
current_num += 1
current_length += 1
max_length = max(max_length, current_length)
return max_length
核心在于num-1不存在时的序列计算。另一种比较好想但是不优雅的方式如下:
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
d = set(nums)
max_len = 0
while d:
# print(d)
nc = d.pop()
cur_len = 1
# forward
for i in range(1, 100_000):
if nc + i not in d:
break
else:
cur_len += 1
d.remove(nc+i)
# backward
for i in range(1, 100_000):
if nc - i not in d:
break
else:
cur_len += 1
d.remove(nc-i)
max_len = max(max_len, cur_len)
return max_len
每次pop出来一个元素之后查询,查到一个就删除一个,当然也可以使用额外数组来保存。
题解-双指针
283. 移动零
- 思路:其实如果不限制原地和额外空间的话思路很多,什么栈、队列、排序基本上都可以。但是题目说了原地置换和$O(1)$空间,所以就只能用双指针了,代码如下:
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
left = right = 0
while right < n:
if nums[right] != 0:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right += 1
11. 盛最多水的容器
- 思路:正常来求解的话肯定是枚举所有的列可能性,这种情况下复杂度直接$O(n^2)$所以肯定不能采取,所以核心在于如何消除多余的状态。其实不难想到的就是对于左右指针每次逼近最大的,从而满足贪心的情况,从某个角度来看该题目也不算特别复杂。
class Solution:
def maxArea(self, height: List[int]) -> int:
left, right = 0, len(height) - 1
max_square = min(height[left], height[right]) * (right - left)
while left < right:
if height[left] < height[right]:
left += 1
else:
right -= 1
max_square = max(max_square, min(height[left], height[right]) * (right - left))
return max_square
15. 三数之和
- 思路:常规来说其实就是$O(n^3)$但是这样肯定超时,所以可能的手段就是先排序然后用双指针变成两数之和问题。取中间一个数开始,两个方向搜负数即可。但是理论上是这样,题目难的地方其实在于去重的步骤。首先,需要对mid去重,即去除和之前相同的情况,其次在最后还需要分别对左值和右值去重。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums = sorted(nums)
res = []
len_n = len(nums)
for mid in range(len_n-2):
# 第一步去重
if mid > 0 and nums[mid] == nums[mid-1]:
continue
l , r = mid + 1, len_n - 1
mid_v = nums[mid]
while l < r:
if nums[l] + nums[r] > - mid_v:
r -= 1
elif nums[l] + nums[r] < - mid_v:
l += 1
else:
res.append([nums[l], mid_v, nums[r]])
# 跳过重复的左值
while l < r and nums[l] == nums[l+1]:
l += 1
# 跳过重复的右值
while l < r and nums[r] == nums[r-1]:
r -= 1
r -= 1
l += 1
return res
42. 接雨水
- 思路:其实这道题算是相当印象深刻的题目了,甚至能够一行写出来了已经。这道题思路也很简单,类似其他dp的题目,先从头往后利用双指针过一遍,每一遍开始时,左指针为当前的状态,右指针不断向右,如果遇到大于左指针的情况,就计算一下积水数量(在这个过程中是已经算上去的),上述一遍得到的是从左往右递增的情况,同理获取一遍从右往左递增的情况即可。
class Solution:
def trap(self, height: List[int]) -> int:
ln = len(height)
water = []
c_max = height[0]
res = 0
for i in range(ln):
if height[i] > c_max:
c_max = height[i]
water.append(0)
elif height[i] == c_max:
water.append(0)
else:
water.append(c_max-height[i])
a_water = []
height = height[::-1]
c_max = height[0]
for i in range(ln):
if height[i] > c_max:
c_max = height[i]
a_water.append(0)
elif height[i] == c_max:
a_water.append(0)
else:
a_water.append(c_max-height[i])
for i, j in zip(water, a_water[::-1]):
res += min(i, j)
return res
# 还有一个极端版本
class Solution:
def trap(self, height: List[int]) -> int:
return 0 if len(set(height)) == 1 else sum([min(max(height[:i+1]), max(height[i:])) - height[i] for i in range(len(height))])
题解-滑动窗口
3. 无重复字符的最长子串
- 思路:说实话我感觉滑动窗口就是双指针的另一种形式罢了,如果没有重复的r+=1,否则l+=1直到没有重复。至于存储方式,用哈希表就好了。稍微借鉴一下前面set的使用方式了。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if len(s) == 0:
return 0
l, r = 0, 0
hasht = set()
ls = len(s)
ml = 1
while r < ls:
if s[r] not in hasht:
hasht.add(s[r])
r += 1
else:
ml = max(ml , r - l)
hasht -= set(s[l])
l += 1
return max(ml, r-l)
438. 找到字符串中所有字母异位词
- 思路1:很简单我都不想多说:
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
sp = "".join(sorted(p))
lsp, ls = len(sp), len(s)
res = []
for i in range(ls - lsp + 1):
if "".join(sorted(s[i:i+lsp])) == sp:
res.append(i)
return res
- 思路2:这个就复杂一点了,字符中是否出现的次数,如果超了则l+=1。
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
ans = []
cnt = Counter(p) # 统计 p 的每种字母的出现次数
left = 0
for right, c in enumerate(s):
cnt[c] -= 1 # 右端点字母进入窗口
while cnt[c] < 0: # 字母 c 太多了
cnt[s[left]] += 1 # 左端点字母离开窗口
left += 1
if right - left + 1 == len(p): # s' 和 p 的每种字母的出现次数都相同
ans.append(left) # s' 左端点下标加入答案
return ans
题解-子串
560. 和为 K 的子数组
- 思路:注意题目,这道题不能排序做!从这个角度看的话其实就是更加复杂的两数之和问题了。由于是连续子串,所以最先想到的其实就是前缀和,在这种情况下,取差即可得到对应的结果:
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
ln = len(nums)
sum_n = [0] + [sum(nums[:i]) for i in range(1, ln+1)]
lsum_n = len(sum_n)
res = 0
for i in range(lsum_n-1):
for j in range(i+1, lsum_n):
if sum_n[j] - sum_n[i] == k:
res += 1
return res
尽管上述方法很简答,但是问题在于上述解答会超时。但是这种情况并不是因为前缀和的方法导致的,而是就差一步了,在这种情况下得考虑一下优化手段了。一下能想到的方案有两种:1、由于已经是前缀了,所以可以做一下排序之后只计算idx更大的即可。2、尝试保存一下其中的一些值。
这里尝试用第二种方案来解决,同时这种方案也是比较常用的手法。首先计算出前缀和数组s
,之后会存在$s[j]-s[i] = k (i<j)$我们换一下顺序就好理解了,存在$s[j]-k = s[i]$,因此根据上式,计算$s[i]$的个数,等价于计算在 $s[j]$ 左边的 $s[j]−k$ 的个数。示例代码如下:
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
ln = len(nums)
sum_n = [0] * (len(nums) + 1)
for i, x in enumerate(nums):
sum_n[i + 1] = sum_n[i] + x
res_d = defaultdict(int)
ans = 0
for i in sum_n:
ans += res_d[i - k]
res_d[i] += 1
return ans
从代码的角度分析,首先sum_n
还是刚才设计的内容。关键在于res_d的设计,但是还是太复杂了,我觉得可以从另一个角度再次简单理解一下。res_d保存的是之前的s[i]值,如果存在的话就使得ans+= res_d[i-k]。整体设计的非常巧妙!
for i in sum_n:
ans += res_d[i - k]
res_d[i] += 1
实际核心就是这两行,一个保证j在i后面,一个用来保存之前的值,分开都不行。
239. 滑动窗口最大值
- 思路:这种还是双指针+滑动窗口滑着就把结果搞出来了。但是由于最大值的获取是$O(N)$复杂度,所以很容易就超时了,首先是一个简单版本的滑动窗口:
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if len(nums) <= 1 or k == 1:
return nums
r = k
res = []
cmax = max(nums[:r])
while r < len(nums):
res.append(cmax)
if nums[r] > cmax:
cmax = nums[r]
r += 1
if nums[r-k-1] == cmax:
cmax = max(nums[r-k:r])
res.append(cmax)
return res
这个倒是不难思考,但是问题在于,超时了!但是身为有着较强算法思维的我们而言,其实最好的方法就是在滑动窗口的值中维护一个优先队列,此外还有一种实现就是维护一个双端队列,这里把两种都写一下。当然,具体的数据结构就不用我们自己写了,
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
ans = []
q = deque() # 双端队列
for i, x in enumerate(nums):
# 1. 入
while q and nums[q[-1]] <= x:
q.pop() # 维护 q 的单调性
q.append(i) # 入队
# 2. 出
if i - q[0] >= k: # 队首已经离开窗口了
q.popleft()
# 3. 记录答案
if i >= k - 1:
# 由于队首到队尾单调递减,所以窗口最大值就是队首
ans.append(nums[q[0]])
return ans
## 双端队列
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
# 注意 Python 默认的优先队列是小根堆
q = [(-nums[i], i) for i in range(k)]
heapq.heapify(q)
ans = [-q[0][0]]
for i in range(k, n):
heapq.heappush(q, (-nums[i], i))
while q[0][1] <= i - k:
heapq.heappop(q)
ans.append(-q[0][0])
return ans
## 最大堆
当然,从具体实现上来看,我还是更倾向于使用双端队列。
76. 最小覆盖子串
- 思路:思路倒是不复杂,一个动长的滑动窗口,先把每个遇到的字符添加到字典里,如果遇到最近的就修改字典,整体思路这样,但是要是写出来还是有难度的,考虑到题目的难度,这里暂且先看一下Counter的一个用法:
class Counter():
...
def __le__(self, other):
'True if all counts in self are a subset of those in other.'
if not isinstance(other, Counter):
return NotImplemented
return all(self[e] <= other[e] for c in (self, other) for e in c)
我们可以看到,Counter实际上是自带大小比较的功能,而且实现也非常好理解。所以可以利用这个方便的和结果的子串进行比较:
cnt_s = Counter()
cnt_t = Counter(t)
...
if cnt_s < cnt_t:
...
所以整体的解决方案为:
class Solution:
def minWindow(self, s: str, t: str) -> str:
ans_left, ans_right = -1, len(s)
cnt_s = Counter() # s 子串字母的出现次数
cnt_t = Counter(t) # t 中字母的出现次数
left = 0
for right, c in enumerate(s): # 移动子串右端点
cnt_s[c] += 1 # 右端点字母移入子串
while cnt_s >= cnt_t: # 涵盖
if right - left < ans_right - ans_left: # 找到更短的子串
ans_left, ans_right = left, right # 记录此时的左右端点
cnt_s[s[left]] -= 1 # 左端点字母移出子串
left += 1
return "" if ans_left < 0 else s[ans_left: ans_right + 1]
题解-普通数组
53. 最大子数组和
- 思路:这题一眼就是dp,根本和普通数组没啥关系,但是既然这样搞了咱也得会做。首先,最简单粗暴的思路自然就是一整个$O(n^2)$的循环,但是包超时的。对于DP的思路,其实也很简单,跟当前最长的比即可。由于其是连续的最大,对于一个样例
[-2, 1, -3]
显然如果前面的值小于0,则肯定不会连续上去。因此根据这个列一下条件转移方程试试,长度可以直接用一个变量保存,需要保存的状态应该是之前的连续和的最大值,无非就是自己和前一个的和的比较,所以代码如下:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
c_max = -inf
ln = len(nums)
res = [-inf for i in range(ln+1)]
for i in range(1, ln+1):
res[i] = max(res[i-1]+nums[i-1], nums[i-1])
c_max = max(res[i], c_max)
# print(res)
return c_max
我真厉害嘿嘿。
56. 合并区间
- 思路:这个看数组长度,可以直接用$O(n^2)$解决,所以直接暴力拿下:
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
ci = intervals.pop(0)
res = []
while intervals:
new = intervals.pop(0)
if new[0] <= ci[1]:
if new[1] > ci[1]:
ci[1] = new[1]
else:
res.append(ci)
ci = new
res.append(ci)
return res
其实用了一个$O(n)$的pop操作,理论上也可以不用的,只是这里为了简化实现所以这样用了。这种题倒是有很多类似的题型。
189. 轮转数组
- 思路:如果不考虑原地置换的话应该秒杀了,但是现在高阶也建议了用$O(1)$所以不能直接拷贝空间解决。
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
nums[:] = nums[-k % n:] + nums[:-k % n]
这种解法算是python的语法糖,但是实际上应该使用了额外空间的。另一种很巧的思路如下:
轮转k个元素等于前n-k个反转之后后k个反转最后再一整个反转(这个有点类似以前做的矩阵旋转),直接开始写:
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
k = k % n
nums[:n-k] = nums[:n-k][::-1]
nums[n-k:] = nums[n-k:][::-1]
nums[:] = nums[::-1]
238. 除自身以外数组的乘积
- 思路:本来很简单的,但是不能做除法而且复杂度$O(n)$。那就直接不能一个一个暴力计算了。这道题算是比较巧妙地一种解法,核心在于
前缀
,想到这一点的话会发现nums[i]部分的结果就是<i和>i的数值的相乘,所以开两个空间算一下试试:
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
pre = [1] * n
post = [1] * n
for i in range(1, n):
pre[i] = nums[i-1] * pre[i-1]
for i in range(n-2, -1, -1):
post[i] = post[i+1] * nums[i+1]
return [p*s for p,s in zip(pre, post)]
那能不能直接用$O(1)$来解,其实也可以,只要pre在向前的时候顺便计算一下就好了:
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
suf = [1] * n
for i in range(n - 2, -1, -1):
suf[i] = suf[i + 1] * nums[i + 1]
pre = 1
for i, x in enumerate(nums):
# 此时 pre 为 nums[0] 到 nums[i-1] 的乘积,直接乘到 suf[i] 中
suf[i] *= pre
pre *= x
return suf
41. 缺失的第一个正数
- 思路:如果用这个代码解决的话,已经相当接近最终的方法了但是实际上并不正确:
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
maxn = max(nums) + 1
space_min = inf
not1 = 1
for i in nums:
if i <= 0 :
continue
elif i == not1:
not1 += 1 # 已经很接近了
else :
if (i - 1) < space_min and (i - 1) != not1:
space_min = i - 1
else:
maxn = i + 1
return min(space_min, maxn, not1)
因为没有充分考虑有些分隔的问题,而这道题正确的求解方法是原地哈希
,其核心就是把n个数字放到n-1个位置直到找到不对劲的,从理论上来说有点像排序,但是由于很多数字不满足情况,所以是会踢出去的。所以试一下看看效果:
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1
题解-矩阵
73. 矩阵置零
- 思路:使用$O(mn)$和$O(m+n)$思路都不太行,毕竟最好还是$O(1)$空间而且既然可以就往这个方向想。原地的话一般都是使用原本的数据结构做数据存储,这道题也不例外,我们可以把其中的第一个出现0的行和列作为标记并处理即可,因此代码如下:
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
l_row, l_col = len(matrix), len(matrix[0])
sign_row, sign_col = None, None
for i in range(l_row):
for j in range(l_col):
if sign_row is None and matrix[i][j] == 0:
sign_row, sign_col = i, j
matrix[sign_row][sign_col] = None
if sign_row is None:
return
for i in range(l_row):
if i == sign_row:
continue
if 0 in matrix[i]:
matrix[i][sign_col] = None
for i in range(l_col):
if i == sign_col:
continue
for j in range(l_row):
if matrix[j][i] == 0:
matrix[sign_row][i] = None
break
print(matrix)
for i in range(l_row):
if matrix[i][sign_col] is None:
for j in range(l_col):
if matrix[i][j] is None:
continue
else:
matrix[i][j] = 0
for i in range(l_col):
if matrix[sign_row][i] is None:
for j in range(l_row):
if matrix[j][i] is None:
continue
else:
matrix[j][i] = 0
for i in range(l_row):
for j in range(l_col):
if matrix[i][j] is None:
matrix[i][j] = 0
54. 螺旋矩阵
- 思路:最近才做过,用四个指针表示深度即可:
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix:
return []
m, n = len(matrix), len(matrix[0])
res = []
def spiral_layer(sx, sy, rows, cols):
if rows <= 0 or cols <= 0:
return
# 上:从左到右
for i in range(sx, sx + cols):
res.append(matrix[sy][i])
# 右:从上到下(至少两行)
if rows > 1:
for j in range(sy + 1, sy + rows):
res.append(matrix[j][sx + cols - 1])
# 下:从右到左(至少两列)
if cols > 1:
for i in range(sx + cols - 2, sx - 1, -1):
res.append(matrix[sy + rows - 1][i])
# 左:从下到上(至少三行)
if rows > 2:
for j in range(sy + rows - 2, sy, -1):
res.append(matrix[j][sx])
# 递归处理内层
spiral_layer(sx + 1, sy + 1, rows - 2, cols - 2)
spiral_layer(0, 0, m, n)
return res
48. 旋转图像
- 思路:这个也算是比较巧的思路,既然是原地旋转的话肯定还是找个规律做置换了,所以直接先对角线翻转再上下翻转即可
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
mr = int(n//2)
for r in range(mr):
for c in range(n):
matrix[r][c], matrix[n-r-1][c] = matrix[n-r-1][c], matrix[r][c]
for i in range(n):
for j in range(i):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
return matrix
240. 搜索二维矩阵 II
- 思路:不幸的是题目的长度很小所以直接两轮for循环也能解决。
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
l_row, l_cow = len(matrix), len(matrix[0])
for i in range(l_row):
for j in range(l_cow):
if matrix[i][j] == target:
return True
return False
当然,为了追求效率,我们还是尽可能用高效的算法来解决。按常理来说其实更倾向于通过二分来查找(毕竟已经排好序),但是常规的二分对应的是一维数组,这种情况下其实我们考虑对角线?首先,对于斜对角,每个值都是大于前面矩阵的值,所以可以两次二分解决一下,先左上到右下,之后在从左下到右上。
- 其他的思路很多,这里简答拿一张图说明一下:
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
i, j = len(matrix) - 1, 0
while i >= 0 and j < len(matrix[0]):
if matrix[i][j] > target: i -= 1
elif matrix[i][j] < target: j += 1
else: return True
return False
题解-链表
160. 相交链表
- 思路:题目设计的比较畸形,其实就是找相同值,一般来说合适的解法就是先遍历存储然后找节点,但是题目建议$O(m+n)$时间复杂度和$O(1)$空间复杂度。考虑到链表的数据结构,如果有公共部分,则假设公共长度为$a$则两个链表剩下的长度$m-a$和$n-a$,为了对齐,先让长一点的链表走$m-n$个元素,之后直到遇到相同元素就好了。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
len_a, len_b = 0, 0
cur_node = headA
while cur_node:
len_a += 1
cur_node = cur_node.next
cur_node = headB
while cur_node:
len_b += 1
cur_node = cur_node.next
longer = headA if len_a > len_b else headB
shorter = headB if len_a > len_b else headA
for i in range(abs(len_a-len_b)):
longer = longer.next
while longer or shorter:
if id(longer) == id(shorter):
return longer
longer = longer.next
shorter = shorter.next
return None
这道题的一个核心细节是判断相交的情况:if id(longer) == id(shorter)
不能仅仅判断值相等。
206. 反转链表
- 思路:看似简单实则一点都不简单。建议是画个图表示一下:
所以只需要使用三个链表就好了,但是要稍微处理下两个问题:1、开始时的None 2、边界条件。
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
curr = head
prev = None
while curr != None:
next = curr.next
curr.next = prev
prev = curr
curr = next
return prev
234. 回文链表
- 思路:很多思路,但是最好还是按$O(1)$空间复杂度解决。首先链表长度$len$是肯定需要获取的。之后把前$len//2$个元素给反转之后继续双向遍历就好了。
class Solution:
# 876. 链表的中间结点
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# 206. 反转链表
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre, cur = None, head
while cur:
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
return pre
def isPalindrome(self, head: Optional[ListNode]) -> bool:
mid = self.middleNode(head)
head2 = self.reverseList(mid)
while head2:
if head.val != head2.val: # 不是回文链表
return False
head = head.next
head2 = head2.next
return True
141. 环形链表
- 思路:快慢指针秒了
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = fast = head # 乌龟和兔子同时从起点出发
while fast and fast.next:
slow = slow.next # 乌龟走一步
fast = fast.next.next # 兔子走两步
if fast is slow: # 兔子追上乌龟(套圈),说明有环
return True
return False # 访问到了链表末尾,无环
142. 环形链表 II
- 思路:还是快慢指针,但是这次要返回环的位置所以需要一些数学运算了。假设不带环的长度为$L$,假设环前面长度为$a$,后面为$b$:
image-20250219155741783
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if fast is slow:
while slow is not head:
slow = slow.next
head = head.next
return slow
return None
21. 合并两个有序链表
- 思路:主要是编写上的难度
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
cur = dummy = ListNode() # 用哨兵节点简化代码逻辑
while list1 and list2:
if list1.val < list2.val:
cur.next = list1 # 把 list1 加到新链表中
list1 = list1.next
else: # 注:相等的情况加哪个节点都是可以的
cur.next = list2 # 把 list2 加到新链表中
list2 = list2.next
cur = cur.next
cur.next = list1 or list2 # 拼接剩余链表
return dummy.next
这里用了个非常细节的设计cur.next = list1 or list2
,简而言之就是把不为None的添加到尾部,算是一个很新颖的用法了。
2. 两数相加
- 思路:这道题的设计就决定了肯定不会限制空间复杂度,所以随便秒杀
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
l1v, l2v = "", ""
while l1:
l1v += str(l1.val)
l1 = l1.next
while l2:
l2v += str(l2.val)
l2 = l2.next
dummy = ListNode(None)
curr = dummy
for i in str(eval(l1v[::-1] + "+" + l2v[::-1]))[::-1]:
newn = ListNode(int(i))
curr.next = newn
curr = curr.next
curr.next = None
return dummy.next
19. 删除链表的倒数第 N 个结点
思路:由于不限制复杂度,所以遍历两遍就行了,但是最好可以有更好的思路,比如先后指针。先让一个指针走N个即可:
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# 由于可能会删除链表头部,用哨兵节点简化代码
left = right = dummy = ListNode(next=head)
for _ in range(n):
right = right.next # 右指针先向右走 n 步
while right.next:
left = left.next
right = right.next # 左右指针一起走
left.next = left.next.next # 左指针的下一个节点就是倒数第 n 个节点
return dummy.next
### 另一种好写的:
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
ll = 0
while head:
ll += 1
head = head.next
head = dummy
for i in range(ll-n):
head = head.next
head.next = head.next.next
return dummy.next
24. 两两交换链表中的节点
- 思路:其实并不复杂。还是得想办法画个图,此外为了避免一些奇怪的情况,可以考虑创建哨兵节点:
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
node0 = dummy = ListNode(next=head) # 用哨兵节点简化代码逻辑
node1 = head
while node1 and node1.next: # 至少有两个节点
node2 = node1.next
node3 = node2.next
node0.next = node2 # 0 -> 2
node2.next = node1 # 2 -> 1
node1.next = node3 # 1 -> 3
node0 = node1 # 下一轮交换,0 是 1
node1 = node3 # 下一轮交换,1 是 3
return dummy.next # 返回新链表的头节点
25. K 个一组翻转链表
- 思路:反转链表升级版,先获取长度然后对于每k个反转一下即可,可以直接拿之前的code试试:
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
n = 0
cur = head
while cur:
n += 1
cur = cur.next
p0 = dummy = ListNode(next=head)
pre = None
cur = head
while n >= k:
n -= k
for _ in range(k):
nxt = cur.next
cur.next = pre
pre = cur
cur = nxt
nxt = p0.next
nxt.next = cur
p0.next = pre
p0 = nxt
return dummy.next
题解-树
94. 二叉树的中序遍历
- 思路:很简单了,实例代码:
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)
而且这个算是比较通用的实现,对于不同的遍历方式改一下返回值的相加顺序就好了
104. 二叉树的最大深度
- 思路:dfs一下即可,或者层序遍历
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root: return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
226. 翻转二叉树
- 思路:翻转一下兄弟节点(包括None节点):
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if root is None:
return root
root.left, root.right = root.right, root.left
self.invertTree(root.left)
self.invertTree(root.right)
return root
101. 对称二叉树
- 思路:起初感觉有点绕,实际上设置一个isSameTree函数之后每次左右子树对应进去一下就好了
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def isSameTree(ln, rn):
if ln is None or rn is None:
return ln is rn
return ln.val == rn.val and isSameTree(ln.left, rn.right) and isSameTree(ln.right, rn.left)
return isSameTree(root.left, root.right)
543. 二叉树的直径
- 思路:找最长路径其实通常是图论该干的,但是可惜这道题是树,最简单的思路就是拿maxdepth每个节点测一下求最大值,如下:
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root: return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
if not root: return 0
res = self.maxDepth(root.left) + self.maxDepth(root.right)
return max(res, self.diameterOfBinaryTree(root.left), self.diameterOfBinaryTree(root.right))
不幸的是超时了,所以合理的手段其实还是dfs,但是两个子节点之间的路径得减去祖父节点的深度即可。
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
self.ans = 1
def dfs(node):
if not node:
return 0
L = dfs(node.left)
R = dfs(node.right)
self.ans = max(self.ans, L + R + 1)
return max(L, R) + 1
dfs(root)
return self.ans - 1
可恶,其实和求深度差不多,只是稍微多了一行。
102. 二叉树的层序遍历
- 思路:我是数据结构高手所以我知道这道题用队列:
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root: return []
l = [[root, 0]]
res = defaultdict(list)
while l:
cur = l.pop(0)
if cur[0].left is not None:
l.append([cur[0].left, cur[1]+1])
if cur[0].right is not None:
l.append([cur[0].right, cur[1]+1])
res[cur[1]].append(cur[0].val)
return [res[i] for i in res]
秒了,但是看看题解:
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
ans = []
q = deque([root])
while q:
vals = []
for _ in range(len(q)):
node = q.popleft()
vals.append(node.val)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
ans.append(vals)
return ans
从长远的角度来看,其实用真实的队列会更好一点。
108. 将有序数组转换为二叉搜索树
- 思路:逐渐娴熟了,只要每次把中间的数值作为节点并把左右侧数组传入即可。
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
if not nums: return TreeNode(None)
midN = len(nums) // 2
root = TreeNode(nums[midN])
root.left = self.sortedArrayToBST(nums[:midN])
root.right = self.sortedArrayToBST(nums[midN+1:])
return root
98. 验证二叉搜索树
- 思路:很简单,但是这里强推一下这种写法:
class Solution:
def isValidBST(self, root: Optional[TreeNode], left=-inf, right=inf) -> bool:
if root is None:
return True
x = root.val
return left < x < right and \
self.isValidBST(root.left, left, x) and \
self.isValidBST(root.right, x, right)
避免了蛋疼的判断
230. 二叉搜索树中第 K 小的元素
- 思路:中序遍历一下即可(至今不知道黄大人当时的变量为什么找不到)
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
res = []
def innerorder(root):
if not root: return
innerorder(root.left)
res.append(root.val)
innerorder(root.right)
innerorder(root)
return res[k-1]
199. 二叉树的右视图
- 思路:层序遍历输出-1索引的即可(修改一行就好了)
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
if not root: return []
l = [[root, 0]]
res = defaultdict(list)
while l:
cur = l.pop(0)
if cur[0].left is not None:
l.append([cur[0].left, cur[1]+1])
if cur[0].right is not None:
l.append([cur[0].right, cur[1]+1])
res[cur[1]].append(cur[0].val)
return [res[i][-1] for i in res]
114. 二叉树展开为链表
- 思路:如果不看原地更换的话就很简单了。
class Solution:
def flatten(self, root: TreeNode) -> None:
preorderList = list()
def preorderTraversal(root: TreeNode):
if root:
preorderList.append(root)
preorderTraversal(root.left)
preorderTraversal(root.right)
preorderTraversal(root)
size = len(preorderList)
for i in range(1, size):
prev, curr = preorderList[i - 1], preorderList[i]
prev.left = None
prev.right = curr
105. 从前序与中序遍历序列构造二叉树
- 思路:根据先序和中序遍历构造二叉树,每次从前序找到第一个元素,然后从中序中索引一下这个元素,则左子树由中序索引左侧组成,右子树相反。所以主要是编码的难度:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder: # 空节点
return None
left_size = inorder.index(preorder[0]) # 左子树的大小
left = self.buildTree(preorder[1: 1 + left_size], inorder[:left_size])
right = self.buildTree(preorder[1 + left_size:], inorder[1 + left_size:])
return TreeNode(preorder[0], left, right)
上述的算法实际上就已经可以解决了,但是有个细节问题,由于涉及到了index操作,所以会有及$O(N)$时间复杂度,整体的复杂度也会到达$O(N^2)$,所以可以考虑使用字典存储一下索引来优化,这里就不多说明了
437. 路径总和 III
- 思路:可以想到是前缀和的思路,但是相较于数组结构,每个节点得知道之前的所有节点的值,所以这道题的难度变成了一个数据结构的问题:
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
def dfs(root, count, sumption):
if root is None:
return 0
sumption += root.val
res = count[sumption-targetSum]
count[sumption] += 1
res += dfs(root.left, count, sumption)
res += dfs(root.right, count, sumption)
count[sumption] -= 1 # 会干扰左右子树的计算
return res
return dfs(root, Counter({0:1}), 0)
核心其实在于这个 count[sumption] -= 1
,如何不移除之前的节点,会导致在计算左子树的时候使用右子树的值继续计算,当然,基于上述情况也可以把临时的Counter删除了,所以总体答案如下:
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
c = Counter([0])
def dfs(root, sumption):
if root is None:
return 0
sumption += root.val
res = c[sumption-targetSum]
c[sumption] += 1
res += dfs(root.left, sumption)
res += dfs(root.right, sumption)
c[sumption] -= 1 # 会干扰左右子树的计算
return res
return dfs(root, 0)
236. 二叉树的最近公共祖先
- 思路:回溯时的解决问题,首先假设是两棵子树,则每个子树按返回值判断即可,同时也要判断一下在同一枝干上的情况:
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
if root in (None, p, q):
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right: # 左右都找到
return root # 当前节点是最近公共祖先
return left or right
给灵神跪了,简单但是不简约。这种思维确实厉害。
200. 岛屿数量
- 思路:做dfs的时候插旗子,遇到的第一个设置为1即可
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
m, n = len(grid), len(grid[0])
def dfs(i: int, j: int) -> None:
# 出界,或者不是 '1',就不再往下递归
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
return
grid[i][j] = '2' # 插旗!避免来回横跳无限递归
dfs(i, j - 1) # 往左走
dfs(i, j + 1) # 往右走
dfs(i - 1, j) # 往上走
dfs(i + 1, j) # 往下走
ans = 0
for i, row in enumerate(grid):
for j, c in enumerate(row):
if c == '1': # 找到了一个新的岛
dfs(i, j) # 把这个岛插满旗子,这样后面遍历到的 '1' 一定是新的岛
ans += 1
return ans
994. 腐烂的橘子
- 思路:算是前面题目的升级版,但是用两个列表分别保存当前和下一个阶段的即可,但是要考虑的情况可能稍微多了一点
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
lrow, lcol = len(grid), len(grid[0])
s_list, c_list = [], []
for i in range(lrow):
for j in range(lcol):
if grid[i][j] == 2:
s_list.append([i,j])
def rotOrange(i, j):
resl = []
if i + 1 < lrow and grid[i+1][j] == 1:
resl.append([i+1, j])
grid[i+1][j] = -1
if i - 1 >=0 and grid[i-1][j] == 1:
resl.append([i-1, j])
grid[i-1][j] = -1
if j + 1 < lcol and grid[i][j+1] == 1:
resl.append([i, j+1])
grid[i][j+1] = -1
if j - 1 >= 0 and grid[i][j-1] == 1:
resl.append([i, j-1])
grid[i][j-1] = -1
return resl
times = 0
while s_list:
print(s_list)
c_list, s_list = s_list, []
for i in c_list:
grid[i[0]][i[1]] = -1
s_list += rotOrange(i[0], i[1])
if s_list == []:
break
else:
times += 1
for i in range(lrow):
for j in range(lcol):
if grid[i][j] == 1:
return -1
return times
虽然想着很简单但是要完全写对还是相当有难度的,整体的难点在于如何保证s_list去重而且让s_list不存在c_list内容,否则会重复计算。在我的实现里面由于是每次进行BFS,所以在去重时会带来很大的麻烦,最好的方法还是参考一下灵神的:
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
fresh = 0
q = []
for i, row in enumerate(grid):
for j, x in enumerate(row):
if x == 1:
fresh += 1 # 统计新鲜橘子个数
elif x == 2:
q.append((i, j)) # 一开始就腐烂的橘子
ans = 0
while q and fresh:
ans += 1 # 经过一分钟
tmp = q
q = []
for x, y in tmp: # 已经腐烂的橘子
for i, j in (x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1): # 四方向
if 0 <= i < m and 0 <= j < n and grid[i][j] == 1: # 新鲜橘子
fresh -= 1
grid[i][j] = 2 # 变成腐烂橘子
q.append((i, j))
return -1 if fresh else ans
可恶,真厉害
207. 课程表
- 思路:拓扑排序,本来以为很复杂,实际上就是找入度为0的,直到所有课程结束。
from typing import List
from collections import deque
class Solution:
# 思想:该方法的每一步总是输出当前无前趋(即入度为零)的顶点
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
"""
:type numCourses: int 课程门数
:type prerequisites: List[List[int]] 课程与课程之间的关系
:rtype: bool
"""
# 课程的长度
clen = len(prerequisites)
if clen == 0:
# 没有课程,当然可以完成课程的学习
return True
# 步骤1:统计每个顶点的入度
# 入度数组,记录了指向它的结点的个数,一开始全部为 0
in_degrees = [0 for _ in range(numCourses)]
# 邻接表,使用散列表是为了去重
adj = [set() for _ in range(numCourses)]
# 想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
# [0, 1] 表示 1 在先,0 在后
# 注意:邻接表存放的是后继 successor 结点的集合
for second, first in prerequisites:
in_degrees[second] += 1
adj[first].add(second)
# 步骤2:拓扑排序开始之前,先把所有入度为 0 的结点加入到一个队列中
# 首先遍历一遍,把所有入度为 0 的结点都加入队列
queue = deque()
for i in range(numCourses):
if in_degrees[i] == 0:
queue.append(i)
counter = 0
while queue:
top = queue.popleft()
counter += 1
# 步骤3:把这个结点的所有后继结点的入度减去 1,如果发现入度为 0 ,就马上添加到队列中
for successor in adj[top]:
in_degrees[successor] -= 1
if in_degrees[successor] == 0:
queue.append(successor)
return counter == numCourses
灵神还给出了另一种思路:判断是否有环
题解-回溯
46. 全排列
- 思路:最简单的想法其实就是构建这个树之后做dfs即可,但是在简化理解之后实际上连树也不需要构建,这里要用到之前做到的一道题的思想
这道题目核心在于完成一个路径节点的计算之后会恢复回去,因此同样的,我们不需要构建完整的树,只需要做dfs,并保存每一层的状态即可。所以整体的思路如下:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
return list(permutations(nums))
还是写一下正解:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ln = len(nums)
ans = []
path = [0] * ln
exist = [False] * ln
def dfs(depth):
if depth == ln:
ans.append(path.copy())
return
for j, on in enumerate(exist):
if not on:
path[depth] = nums[j]
exist[j] = True
dfs(depth + 1)
exist[j] = False
dfs(0)
return ans
78. 子集
- 思路:聪明的小伙伴应该能想到,直接使用上一道题目的代码即可,但是在添加的部分改一下:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ln = len(nums)
ans = []
path = []
def dfs(depth):
if depth == ln:
ans.append(path.copy())
return
dfs(depth + 1)
path.append(nums[depth])
dfs(depth + 1)
path.pop()
dfs(0)
return ans
即便如此相较于上一道题目还是有相当多的差别的,但是核心思路还是dfs+恢复
- 思路:这个反而很简单,首先把所有的字符表示出来,之后构建树即可(和之前的思路几乎一致,只不过每次的数字编程数组罢了)。感觉理解了前面两道题目,这倒题目就好理解多了。
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if digits is "":
return []
rd = {"2":"abc", "3": "def", "4": "ghi", "5":"jkl", "6": "mno", "7": "pqrs", "8":"tuv", "9": "wxyz"}
all_rd = [rd[i] for i in digits]
ln = len(all_rd)
path = []
res = []
def dfs(depth):
if depth == ln:
res.append("".join(path))
return
for c in all_rd[depth]:
path.append(c)
dfs(depth + 1)
path.pop()
dfs(0)
return res
39. 组合总和
- 思路:还是一样的思路,到目前为止其实都不需要专门做去重的步骤,但是这道题就需要了:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
path = []
ans = []
def dfs(sn):
if sn == target:
ans.append(path.copy())
elif sn > target:
return
for c in candidates:
path.append(c)
dfs(sn + c)
path.pop()
dfs(0)
return ans
# 输出:[[2,2,2,2],[2,3,3],[3,2,3],[3,3,2],[3,5],[5,3]]
# 预期结果:[[2,2,2,2],[2,3,3],[3,5]]
上述思路倒是和题目完全一致,但是问题在于上述思路无法去重,合理的思路是对list做排序之后用set做去重,由于题目说了长度不超过150所以显然是可行的。但是要是想更加优雅地解决的话就得引入去重了(剪枝),当然,灵神的题解也给的非常优秀。举一个简单的例子,如果我们每次按当前的最小值放入,则会存在:[2,2,2,2], [2,2,4], [2,4,2]这样的情况,但是其实只要每次限制使用递增的情况,就能解决这种情况(woc我不会表述了)
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
path = []
ans = []
def dfs(i, left):
if left == 0:
ans.append(path.copy())
return
if i == len(candidates) or left < candidates[i]:
return
dfs(i + 1, left)
path.append(candidates[i])
dfs(i, left - candidates[i])
path.pop()
dfs(0, target)
return ans
这时候代码长这个样子,即优先选择值最大的情况。另一种写法如下:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
ans = []
path = []
def dfs(i: int, left: int) -> None:
if left == 0:
# 找到一个合法组合
ans.append(path.copy())
return
if left < candidates[i]:
return
# 枚举选哪个
for j in range(i, len(candidates)):
path.append(candidates[j])
dfs(j, left - candidates[j])
path.pop() # 恢复现场
dfs(0, target)
return ans
相较于上一种,感觉还是这版代码更好理解,这种情况下出现的数组必然是递增的,因此不会出现重复的情况
22. 括号生成
- 思路:由于最终生成的结果必然是正确的,只需要保证栈不为空即可,如果仅仅只是判断数量的话就是$n!$,可惜这道题要返回所有可能的结果,就得稍微动点脑子了。从另一个角度分析的话,这道题其实就是在现有的情况下插入括号即可。因此另一个可能的解法就是首先得出所有括号的插入方法,之后在结果中生成即可。
- 其实做到目前为止,对于回溯的一个显然的思路就在于如何处理选或不选,全排序是每次都得选,子集是不选选过的,组合总和是选最大的,括号生成就是选择左括号或者小于左括号数量的右括号,所以代码如下:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
res = []
ln = n * 2
path = [""] * ln
def dfs(i: int, open: int) -> None:
if i == ln: # 括号构造完毕
res.append(''.join(path)) # 加入答案
return
if open < n: # 可以填左括号
path[i] = '(' # 直接覆盖
dfs(i + 1, open + 1) # 多了一个左括号
if i - open < open: # 可以填右括号
path[i] = ')' # 直接覆盖
dfs(i + 1, open)
dfs(0, 0)
return res
原本是打算用range做的,结果就是多了很多没用的结果,实际上只要每次dfs的时候判断就好了,反正回溯的时候会实现循环。
79. 单词搜索
- 思路:不想什么优化思路,做dfs即可:
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m, n = len(board), len(board[0])
def dfs(i: int, j: int, k: int) -> bool:
if board[i][j] != word[k]: # 匹配失败
return False
if k == len(word) - 1: # 匹配成功!
return True
board[i][j] = '' # 标记访问过
for x, y in (i, j - 1), (i, j + 1), (i - 1, j), (i + 1, j): # 相邻格子
if 0 <= x < m and 0 <= y < n and dfs(x, y, k + 1):
return True # 搜到了!
board[i][j] = word[k] # 恢复现场
return False # 没搜到
return any(dfs(i, j, 0) for i in range(m) for j in range(n))
呜呜呜学不会这么优化的代码,虽然思路一样但是完全不一样。虽然是回溯但是这道题的回溯思路只在恢复现场那一行出现了,有点可惜。
131. 分割回文串
- 思路:简单点想的话,其实就是找到最长的回文串,之后不断做切分即可(因此得从右往左进行)。但是这种思路有点违背这部分的内容,因此这类题目的另一类思路是,是否要插入逗号。或者说从现在开始的res-n个字符是否是回文串,因此代码如下:
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
ls = len(s)
path = []
def dfs(depth):
if depth == ls:
res.append(path.copy())
return
for i in range(depth, ls):
t = s[depth: i + 1]
if t == t[::-1]:
path.append(t)
dfs(i+1)
path.pop()
dfs(0)
return res
51. N 皇后
- 思路:现在再看n皇后问题其实完全不复杂了(完全想错了),最开始可能认为就是全排列+去掉对角线相同情况,但是实际上完全没那么简单,当然,全排列的代码可以拿过来,但是问题在于如何判断对角线已经存在对应的皇后:
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
conn = [("."*i + "Q" + "."*(n-i-1)) for i in range(n)]
nums = [i for i in range(n)]
ln = len(nums)
ans = []
path = [0] * ln
exist = [False] * ln
def dfs(depth):
if depth == ln:
ans.append([("."*i + "Q" + "."*(n-i-1)) for i in path])
return
for j, on in enumerate(exist):
if not on:
status = True
for idx, check in enumerate(path[:depth]):
if abs(depth - idx) == abs(nums[j] - path[idx]):
status = False
if status == False:
continue
path[depth] = nums[j]
exist[j] = True
dfs(depth + 1)
exist[j] = False
dfs(0)
return ans
本来想看题解,不幸的是一下子做出来了,核心就在于:(看来当天状态很好啊)
status = True
for idx, check in enumerate(path[:depth]):
if abs(depth - idx) == abs(nums[j] - path[idx]):
status = False
if status == False:
continue
题解-二分搜索
35. 搜索插入位置
- 思路:算是最基础的二分搜索了,详细参考这部分的模板代码
74. 搜索二维矩阵
- 思路:跟上一道题目一样,展开就好了:
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
lrow, lcol = len(matrix), len(matrix[0])
left, right = 0, (lrow * lcol) - 1
while left <= right:
mid = ( left + right ) // 2
mrow = mid // lcol
mcol = mid % lcol
if matrix[mrow][mcol] == target:
return True
elif matrix[mrow][mcol] > target:
right = mid - 1
else:
left = mid + 1
return False
34. 在排序数组中查找元素的第一个和最后一个位置
- 思路:毕竟说了二分,思路是稍微有点差距,可以先找到一个分界点,之后根据这个分界点,从左往右搜索和从右往左搜索。且看我丑陋的代码:
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def searchInsert(nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return -1
def searchl(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
right = mid - 1
else:
left = mid + 1
return left
def searchr(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
left = mid + 1
else:
right = mid - 1
return left
f = searchInsert(nums, target)
if f == -1:
return [-1, -1]
l = searchl(nums[:f], target)
r = searchr(nums[f:], target)
return [l, r+f-1]
可以使用库函数直接实现倒是:
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
start = bisect_left(nums, target)
if start == len(nums) or nums[start] != target:
return [-1, -1]
end = bisect_right(nums, target) - 1
return [start, end]
33. 搜索旋转排序数组
- 思路:首先想法是查中间的分割元素然后做二分,但是问题在于如果遍历查找的话是一个$O(N)$的复杂度,就不适合了。核心在于如何使用二分来迅速查找到中间的元素。其实和上一道题思路几乎一样,二分查找找到最小的小于nums[n-1]的元素即可。
class Solution:
def search(self, nums: List[int], target: int) -> int:
def searchmid(nums):
left, right = 0, len(nums) - 1
while left <= right:
mid = ( left + right ) // 2
if nums[mid] <= nums[-1]:
right = mid - 1
elif nums[mid] > nums[-1]:
left = mid + 1
return left
m = searchmid(nums)
def searchInsert(nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid - 1
else:
left = mid + 1
return -1
print(m)
res = [searchInsert(nums[m:], target), searchInsert(nums[:m], target)]
if res[0] != -1:
return res[0] + m
return res[1]
153. 寻找旋转排序数组中的最小值
- 思路:上一道题目的简单版本
class Solution:
def findMin(self, nums: List[int]) -> int:
def searchmid(nums):
left, right = 0, len(nums) - 1
while left <= right:
mid = ( left + right ) // 2
if nums[mid] <= nums[-1]:
right = mid - 1
elif nums[mid] > nums[-1]:
left = mid + 1
return left
m = searchmid(nums)
return nums[m]
4. 寻找两个正序数组的中位数
- 思路:要求了$O(log(M+N))$复杂度所以变为困难题目了。太难了暂且搁置
题解-栈
20. 有效的括号
- 思路:用一个栈维护,如果到最后栈空就正确。
class Solution:
def isValid(self, s: str) -> bool:
stack = []
for i in s:
if i in "([{":
stack.append(i)
else:
if len(stack) == 0:
return False
c = stack.pop()
if c == "{" and i == "}" or c == "[" and i == "]" or c == "(" and i ==")":
continue
else:
return False
if len(stack) != 0:
return False
return True
155. 最小栈
- 思路:数据结构题目实现一个最小栈。用一个栈+一个最小栈就好了
class MinStack:
def __init__(self):
self.list = []
self.minl = [inf]
def push(self, val: int) -> None:
self.list.append(val)
if val <= self.minl[-1]:
self.minl.append(val)
def pop(self) -> None:
val = self.list.pop()
if val == self.minl[-1]:
self.minl.pop()
def top(self) -> int:
return self.list[-1]
def getMin(self) -> int:
return self.minl[-1]