因为某大厂的算法没有撕出来,怒而整理该贴。部分题目有python版本的AC代码。本贴耗时4天呜呜呜
1.哈希
两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
思路
不多说,经典题。哈希表**(字典dict或者直接{})**存储已经访问过的元素和对应下标,对每次遍历的元素num都去寻找target-num是否在哈希表中,在的话就取出并返回答案,否则的话将当前数字和下标存入哈希表。
字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
示例 2:
输入: strs = [“”]
输出: [[“”]]
示例 3:
输入: strs = [“a”]
输出: [[“a”]]
提示:
- 1 <= strs.length <=
10^4
- 0 <= strs[i].length <= 100
- strs[i] 仅包含小写字母
思路
同样也是对每次遍历到的字符串存入哈希表,键为升序字典序的该字符串,值为该字符串及其异位词的列表(注意是列表而不是集合,因为不要求去重)。
例如对于输入
["",""]
我们返回
[["",""]]
而不是
[[""]]
最后就是,返回哈希表的值如果直接用hasttable.values()那么会返回哈希表的值的字典版,需要将其转化为list。
最长连续序列
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
思路
用哈希表+集合优化。
要实现时间复杂度为 O(n) 的算法,关键在于利用哈希表(或集合)快速判断元素的前驱和后继是否存在,避免重复遍历已处理的元素。核心思路如下:
- 将数组元素存入集合:利用集合实现O(1)时间的存在性查询。
在这直接用hash_set = set(nums)
- 仅处理序列起点:对于每个元素 num,若 num-1 不存在于集合中,则说明 num 是某个连续序列的起点。从该起点开始,不断尝试扩展序列长度(判断 num+1, num+2, … 是否存在),直到碰到不在集合中的数,说明序列走到了尽头,则更新答案。
注意这里num要从hash_set中取,避免原数组太多重复的数。
不断尝试扩展序列长度的代码实现就是用cur_num记录当前的num,然后while cur_num +1 in hash_set,记录最长序列长度。
2.双指针
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路
经典题,维护slow和fast指针。slow指针指向非零元素的右边界(最后一个非零元素的下标再+1),fast指针向后遍历(代码实现为while fast < len(nums))
-
如果遇到了非零元素,那么赋值给slow区域,因为slow下标所在已经被赋值了,所以slow++,同时fast遍历到的数也已经赋值过去了,所以fast++
没有写成nums[slow] = nums[fast],是因为nums[slow]还需要被考察,这样写会丢失掉nums[slow]的
-
如果遇到了为零的元素,那么fast自己向后走就行了
盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
思路
相当于最大水量的长和宽都不固定,所以我们用双指针从两边收缩,这样保障长是从最长长度开始收缩的,那么一开始我们有一个初始的面积,就是双指针围出来的面积。然后我们需要去收缩双指针,试图找到更大的面积。
因为双指针是从两边收缩的,那么每次收缩是移动左指针还是右指针呢?
答案是,我们只需要移动高度较小的那边。因为高度较小的那边限制了面积,只有移动他才有可能出现面积更大的情况。
三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
思路
同样经典老朋友了,将数组排序后
- 最外层循环寻找的是第一个数(注意这里
i
只能取到len(nums)-2
) - left指针寻找第二个数(注意:如果对当前的
i
找到答案后,left需要+1继续寻找新的答案三元组,因为同一个i
可能有多个left和right组成答案三元组。并且right不要重新赋值为len(nums)-1,因为left右移了,结果只会增大,我们要找到新的答案三元组需要right左移。) - right指针寻找第三个数
接雨水
思路
典中典。不多说,这道题我不喜欢用双指针,喜欢用leftMax和rightMax。
leftMax和rightMax存储每个下标其左边/右边最高的高度(严格大于)。
而对于左边界和右边界,因为没有左边的柱子/右边的柱子,赋值为-1。
我们需要找到每个位置左右比它高的柱子中较矮的那个柱子min_h。只考虑这个柱子这一列,其储水空间就是min_h - height[i] 本身,然后答案就是每列的累加。
注意要判断这个min_h - height[i]是否大于0,大于0才累加到答案中。
3.滑动窗口
无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
思路
滑动窗口+哈希表。
i从左到右遍历字符串,以当前字符的下标为右边界,并不断试图往左扩宽左边界。
代码实现就是while s[j] not in hashtable and j >= 0
滑动窗口内维护的就是当前字符下标为右边界的无重复字符的最长子串的哈希映射。
找到字符串中所有字母异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
思路
统计目标字符串 p 的字符频率:使用哈希表(Python 中的 Counter)统计 p 中每个字符的出现次数。
初始化滑动窗口:统计s中前p个字符的频率,相当于把前p个字符放入滑动窗口中,代码实现为:Counter(s[:len_p])
。
在开始滑动前,比较一下现在初始滑动窗口是否就已经满足了和p字符频率相等的条件(因为都是Counter类,所以直接==
判断即可),是的话将下标0放入答案数组中。
从左向右滑动窗口(注意i
从1开始,不用滑动到s的末尾,因为窗口本身有长度,所以滑动到len_s - len_p + 1即可),移除窗口左侧的字符(频率减 1),并添加窗口右侧的新字符(频率加 1)。比较当前窗口的字符频率与 p 的字符频率是否完全一致
注意Counter类删除元素的代码实现为:del window_counter[s[i-1]]
4.子串
和为k的子数组(dd)
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
思路
因为是连续非空序列,所以想到前缀和。
其实前缀和也是O(n^2)的做法。注意这里前缀和要初始化成len(nums)+1的数组,然后presum[i]表示下标为0 ~ i-1
的数字的和。
伪代码如下:
presum = [0] * (len(nums)+1)
for i in range(1, len(nums)+1):
presum[i] = presum[i-1]+nums[i-1]
for i in range(len(nums)):
for j in range(i, len(nums)):
if presum[j+1] == presum[i]+k:
ans += 1
滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
提示:
- 1 <= nums.length <= 10^5
- -10^4 <= nums[i] <=
10^4
- 1 <= k <= nums.length
思路
经典问题,将滑动窗口中的元素放入单调队列,使得我们维护的是一个递减的队列。
- 如果新员工比老员工强(或者一样强),把老员工裁掉。(元素进入窗口,while)
- 如果老员工 35 岁了,也裁掉。(元素离开窗口,if)
这样下来,只会有越来越大(强)且符合下标(年龄)要求的元素不断进入窗口(队列),最后因为是递减的队列,所以窗口最大值就是队首(最左边),所以每次滑动时将答案放入答案队列里(注意滑动窗口未成形时不要放入)。
最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
- 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
- 如果 s 中存在这样的子串,我们保证它是唯一的答案。
进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
提示:
- m == s.length
- n == t.length
- 1 <= m, n <= 10^5
- s 和 t 由英文字母组成
思路
题目可以转化为,找到涵盖t的最小滑动窗口的长度。
因为想要高效判断涵盖,所以滑动窗口也用Counter
类,记作s_counter
,判断时代码实现为直接while s_counter >= t_counter
。
虽然我们遍历右端点,其实也就是滑动窗口的右边界,for right, char in enumerate(s)
,如果子串涵盖t,那么对于当前下标i
的右端点,我们不断收缩左端点直到不涵盖为止。
因为题目要求返回最短子串,其实就是最终滑动窗口内的字符,所以我们额外维护一个ans_left和ans_right,用来指向找到最短长度时滑动窗口左边界和右边界的下标。在移动过程中更新这个ans_left和ans_right。
5.普通数组
最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
提示:
- 1 <= nums.length <=
10^5
-10^4
<= nums[i] <=10^4
思路
dp[i]表示一直考虑到nums[i]结尾(一定包含nums[i])的连续子数组最大和。
所以转移方程为dp[i] = max(dp[i-1]+nums[i], nums[i])
。
合并区间
一道熟悉的笔试题
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
提示:
- 1 <= intervals.length <= 10^4
- intervals[i].length == 2
- 0 <= starti <= endi <= 10^4
思路
-
将所有区间按照左端点排序,维护一个已经合并过的列表merged,merged最后一个区间记为
merged_back
代码实现为
intervals.sort(key=lambda x:x[0])
-
如果当前区间的左端点<=
merged_back
的右端点,那么当前区间会和merged_back
重叠,我们需要用当前区间的右端点更新merged_back
的右端点。 -
否则的话,当前区间和
merged_back
不重叠,我们将当前区间记作新的merged_back
轮转数组
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
提示:
- 1 <= nums.length <= 10^5
-2^31
<= nums[i] <=2^31 - 1
- 0 <= k <= 10^5
进阶:
尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
思路
原地做法:
对于例子
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
注意对于k超出元素个数范围的情况,一开始要用k %= len(nums)
处理。
注意翻转整个数组的话不要用nums = nums[::-1]
,这个只会原列表保持不变,只是变量 nums 指向了新的翻转后的列表副本。而是要用nums[:] = nums[::-1]
,使用切片操作使得原变量原地修改。
除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。
这句话好绕hhh,一开始都没看出来。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
提示:
- 2 <= nums.length <= 10^5
- -30 <= nums[i] <= 30
- 输入 保证 数组 answer[i] 在 32 位 整数范围内
进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)
思路
没啥好说的,上灵茶山
缺失的第一个正数
记这道题一辈子凸(艹皿艹 )
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
提示:
- 1 <= nums.length <= 10^5
-2^31
<= nums[i] <=2^31 - 1
思路
我能说什么,智障题目,记你一辈子。沙雕python解包……记你一辈子……
对于每个数,认为其学号是nums[i],那么这个数应该坐在nums[i]-1
的位置上。
如果这个数虽然没有坐在nums[i]-1
的位置上,但该位置上已经坐了相同值的数,那么我们可以认为这个数是重复数的影分身,不用管这个数了。
并且因为我们要找的是最小正整数,所以一个数的学号如果是负数或0,也就是不在1-n的范围内,那么不是我们要考虑坐位置的学生,直接跳过即可。
最后从左到右便利,第一个没有坐在正确位置上的数nums[i]!=i+1
,返回i+1
。
否则返回n+1
注意不能是nums[i], nums[nums[i]-1] = nums[nums[i]-1], nums[i]
而应该是nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
6.矩阵
矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
提示:
- m == matrix.length
- n == matrix[0].length
- 1 <= m, n <= 200
- -
2^31
<= matrix[i][j] <=2^31
- 1
进阶:
一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?
思路
一开始想的是多起点bfs,但是这样的话空间复杂度最坏是O(m*n),而且bfs也会让同一行或者同一列被多次访问,性能不算很高。
所以想的是,用第一行来记录哪些列需要被置零,第一列来记录哪些行需要被置零,并且用两个变量来记录本来第一行是否就存在0,第一列是否就存在0,这样的话空间复杂度为O(1)
这个的话,主要是代码实现的细节需要注意一下,一不小心就容易弄混。
代码
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
firstRowHasZero = any(matrix[0][j] == 0 for j in range(n))
firstColHasZero = any(matrix[i][0] == 0 for i in range(m))
# 标记需要置零的行和列
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = 0
matrix[0][j] = 0
# 根据标记置零
for i in range(1, m):
for j in range(1, n):
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0
# 处理第一行和第一列
if firstRowHasZero:
for j in range(n):
matrix[0][j] = 0
if firstColHasZero:
for i in range(m):
matrix[i][0] = 0
螺旋矩阵
又是一道做过的笔试题
给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
提示:
- m == matrix.length
- n == matrix[i].length
- 1 <= m, n <= 10
- -100 <= matrix[i][j] <= 100
思路
没什么好说的,这个就是一个模拟。但是代码实现挺值得借鉴的。
代码
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
direction = ((0,1),(1,0),(0,-1),(-1,0))
# 按照右 下 左 上 的顺序
ans_list = []
m, n = len(matrix), len(matrix[0])
i = j = di = 0
for _ in range(m*n):
# 一共要走m*n步
ans_list.append(matrix[i][j])
matrix[i][j] = '#'
# 建议不要用None,用None的话,下面已访问的判断也不要用or not matrix[dx][dy],因为有可能
# matrix[dx][dy]本身是0,这样会误判已经访问过的,所以最好还是用特殊字符
dx, dy = i + direction[di][0], j + direction[di][1]
if dx<0 or dx>=m or dy<0 or dy>=n or matrix[dx][dy]=='#':
# 如果 (dx, dy) 出界或者已经访问过,右转90度进行尝试
di = (di+1)% 4
i += direction[di][0]
j += direction[di][1]
return ans_list
旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
提示:
- n = matrix.length = matrix[i].length
- 1 <= n <= 20
- -1000 <= matrix[i][j] <= 1000
思路
又是一道做过的笔试题
矩阵顺时针旋转 90º 后,可找到以下规律:
「第 i 行」元素旋转到「第 n−1−i 列」元素;
「第 j 列」元素旋转到「第 j 行」元素;
根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 matrix[i][j]→matrix[j][n−1−i] 后,原矩阵元素 matrix[j][n−1−i] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。
为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。
注意这里辅助矩阵需要用深拷贝,而不是浅拷贝
代码
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
# 深拷贝 matrix -> tmp
tmp = copy.deepcopy(matrix)
# 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素
for i in range(n):
for j in range(n):
matrix[j][n - 1 - i] = tmp[i][j]
搜索二维矩阵Ⅱ
又是一道做过的笔试题
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
提示:
- m == matrix.length
- n == matrix[i].length
- 1 <= n, m <= 300
-10^9
<= matrix[i][j] <=10^9
-10^9
<= target <=10^9
思路
因为m和n不大,才百级,所以要么暴力,要么遍历行,然后每行二分
代码
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m = len(matrix)
n = len(matrix[0])
# 遍历每行,对行使用二分
for i in range(m):
# 要确保该行的第一个数字小于目标值,最后一个数字大于目标值,则答案才可能在该行
# 所以如果matrix[i][0]>target或者matrix[i][n-1]<target,都直接跳过
if matrix[i][0] > target or matrix[i][n-1] < target:
continue
left, right = 0, n-1
while left <= right:
mid = (left+right)//2
if target < matrix[i][mid]:
right -= 1
elif target > matrix[i][mid]:
left += 1
else:
return True
return False
7.链表
相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
思路
说真的……感觉这道题不算
ez
啊,思路明明至少算mid
,虽然代码实现确实很简单就是了。
思路来自灵茶山
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路
没什么好说的,经典题了,注意这里不用虚拟头节点会更简单。但是有时候就是想不起来……感觉一遇到指针就好混乱┭┮﹏┭┮,化身纯背战士得了。
代码
prev = None
cur = head
while cur:
tmp = cur.next
cur.next = prev
prev = cur
cur = tmp
# 譬如对于1 → 2 → 3 → None,最后cur指向None,prev指向3
return prev
回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
思路
如果是双向链表的话,很简单,从头遍历一遍,从尾遍历一遍,然后比较是否相等。可惜这里不是,所以我们想的思路就是“寻找中间节点+反转一半+比较”
环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
思路
太爱这道题了,因为做了比较多遍,所以手到擒来好吧(╹ڡ╹ )
用速度差为1的快慢指针遍历,看是否相遇,相遇的话就是有环的,否则无环。
环形链表Ⅱ
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路
同爱这道,依旧是手拿把掐(╹ڡ╹ )
用速度差为1的快慢指针遍历,得到相遇点后再用速度为1的两个慢指针,一个从头遍历,一个从相遇点遍历,再次相遇时就是环入口。
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路
我真的爱这种只需要关注代码细节的题
这有啥好说的捏,直接两个指针遍历,比较后插入就行了。
两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
思路
没啥好说的,注意构造虚拟头节点+进位+最后删除前导0
删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
思路
没啥好说的,遍历一遍得到节点总数,然后构造虚拟头节点(因为头节点也可能被删)。
两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路
注意构造虚拟头节点,以及代码细节即可。
K个一组翻转链表!!!
记它一辈子呜呜呜呜呜呜呜。坏题!坏题!!!
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
思路
建议看不懂灵茶山的不要硬看。就单纯地写两个函数,一个纯单链表反转的函数,一个控制k个一组的函数即可。
代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode],cur: Optional[ListNode],k:int):
for _ in range(k):
nextNode=head.next
head.next=cur
cur=head
head=nextNode
return cur
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
cnt=0
Fast=head
while Fast and cnt<k:
cnt+=1
Fast=Fast.next
if cnt==k:
Fast=self.reverseKGroup(Fast,k)
head=self.reverseList(head,Fast,k)
return head
作者:gh0st
链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/solutions/3627953/di-gui-kge-yi-zu-fan-zhuan-lian-biao-by-k6748/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
思路
没什么好说的,模拟题。
排序链表
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
提示:
- 链表中节点的数目在范围 [0, 5 * 10^4] 内
-10^5
<= Node.val <=10^5
思路
既然是10的4次方的话,直接遍历一遍后sort,然后按照排序数组构造链表也不是不行捏~
合并 K 个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
提示:
- k == lists.length
- 0 <= k <= 10^4
- 0 <= lists[i].length <= 500
-10^4
<= lists[i][j] <=10^4
思路
这道题用最小堆是最好的,代码实现很简单。也可以用分治合并的思想。
LRU缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
思想
woc大名鼎鼎,但其实我没有做过这道题hhh,这道题就不带题解了,真的太出名了hhh
8.二叉树
呃呃呃终于到了,万众喜爱的二叉树环节。。。
二叉树的中序遍历
思路
有什么好说的,狠狠递归啊。
二叉树的最大深度
思路
BFS,每次加入队列时层数+1
翻转二叉树
思路
BFS,弹出节点时调换左右节点,并且压入队列时顺序为右左。
对称二叉树
思路
递归检查左右孩子是否相等
二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
提示:
- 树中节点数目在范围
[1, 10^4]
内 - -100 <= Node.val <= 100
思路
找到最左孩子节点(在这过程leftdis不断++),以及找到最右孩子节点(在这过程中rightdis不断++),答案就是leftdis+rightdis
找到最左/右孩子节点的过程就是递归的过程,往上返回dis不断++。
二叉树的层序遍历
思路
BFS
将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
提示:
- 1 <= nums.length <= 10^4
-10^4
<= nums[i] <=10^4
- nums 按 严格递增 顺序排列
思路
不断从中间取节点去构造二叉树,相当于归并的逆向过程,即二分。
验证二叉搜索树
思路
递归向下的过程带上全局最小值和全局最大值。
二叉搜索树中第 K 小的元素
思路
中序遍历二叉搜索树可以得到一个递增序列。因此,第 k 小的元素就是中序遍历序列中的第 k 个元素。
二叉树的右视图
这道题还蛮有意思的hhh
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
思路
层序遍历,因为每次放入的是一层的节点,那么该层最右边的节点就是可见的。
二叉树展开为链表
给你二叉树的根结点 root ,请你将它展开为一个单链表:
展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
展开后的单链表应该与二叉树 先序遍历 顺序相同。
思路
按照先序遍历进行递归,然后每次递归时插入右孩子节点
从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路
递归函数,参数是节点列表以及根节点,如果节点列表只有单个节点,那么直接返回,如果有多个节点,那么根据根节点,再次划分左孩子列表和右孩子列表,分别再次传入这个递归函数。
路径总和Ⅲ
也听说过路径总和这个系列的鼎鼎大名hhh
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
思路
请出灵茶山老祖……
作者:灵茶山艾府
链接:https://leetcode.cn/problems/path-sum-iii/solutions/2784856/zuo-fa-he-560-ti-shi-yi-yang-de-pythonja-fmzo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
二叉树的最近公共祖先
思路
记住这张图就好了。
要么,p和q其中有一方是对方的最近公共祖先。最简单的情况如图2。
要么,p和q的最近公共祖先是第三个节点。最简单的情况如图1。
所以递归函数的参数有三个,一个是当前遍历到的节点node,一个是p,一个是q。
递归函数的终止条件为:如果node为空或者node为p或者node为q,那么返回node。
这样保证了返回的子树中如果有p或者q,那么才会返回p或者q,否则返回空。
然后递归左节点和右节点。
如果左节点结果和右节点结果都不为空,说明左子树有p或q其中之一。右子树有p或q其中之一。那么此时就应该返回它俩的父节点。
如果左节点结果或者右节点结果有一方为空,那么返回不空的那一个。
二叉树中的最大路径和
思路
路径的结构:路径可以从任意节点出发,向上或向下延伸,但不能同时包含左右子树的分支(否则会形成环)。因此,对于每个节点,其最大路径和可能由以下三部分组成:
- 节点自身:单独作为一个路径。
- 节点 + 左子树路径:从节点出发,向左子树延伸。
- 节点 + 右子树路径:从节点出发,向右子树延伸。
- 节点 + 左子树路径 + 右子树路径:以该节点为顶点,同时包含左右子树的路径。
递归的贡献值:对于每个节点,递归计算其左右子树的单边最大贡献值(即从该节点出发,向下延伸的最大路径和)。该贡献值必须包含该节点本身,且只能选择左子树或右子树中的一条路径(或不选)。
全局最大值更新:在递归过程中,维护一个全局变量记录所有可能路径中的最大和。当计算某个节点的路径和时,若其路径和超过当前全局最大值,则更新该值。
9.图论
岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
思路
经典dfs题目。bfs也行。
腐烂的橘子
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
思路
一眼bfs。
课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
提示:
- 1 <= numCourses <= 2000
- 0 <= prerequisites.length <= 5000
- prerequisites[i].length == 2
- 0 <= ai, bi < numCourses
- prerequisites[i] 中的所有课程对 互不相同
思路
一看就知道要用拓扑排序做。
方法一:入度表(广度优先遍历)
算法流程:
统计课程安排图中每个节点的入度,生成 入度表 indegrees。
借助一个队列 queue,将所有入度为 0 的节点入队。
当 queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre:
并不是真正从邻接表中删除此节点 pre,而是将此节点对应所有邻接节点 cur 的入度 −1,即 indegrees[cur] -= 1。
当入度 −1后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点已经被 “删除”,此时将 cur 入队。
在每次 pre 出队时,执行 numCourses–;
若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
复杂度分析:
时间复杂度 O(N+M): 遍历一个图需要访问所有节点和所有临边,N 和 M 分别为节点数量和临边数量;
空间复杂度 O(N+M): 为建立邻接表所需额外空间,adjacency 长度为 N ,并存储 M 条临边的数据。
作者:Krahets
链接:https://leetcode.cn/problems/course-schedule/solutions/18806/course-schedule-tuo-bu-pai-xu-bfsdfsliang-chong-fa/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
实现Trie(前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
提示:
- 1 <= word.length, prefix.length <= 2000
- word 和 prefix 仅由小写英文字母组成
- insert、search 和 startsWith 调用次数 总计 不超过 3 * 10^4 次
思路
其实就是一棵 26 叉树,对于 26 叉树的每个节点,可以用哈希表,或者长为 26 的数组来存储子节点。
10.回溯
我太爱这种回溯了,多做几次就熟了的感觉,别管,已膨胀(
全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 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]]
思路
回溯终止条件是path长度==nums长度,因为是全排列,所以i遍历数组时每次都会从头遍历(因为全排列不在乎数字的先后顺序),所以需要用used数组记录这个数字是否已经在path中使用过。
子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路
回溯终止条件依旧是path长度==nums长度。但是子集要求是有顺序的,所以i遍历数组时不会每次都会从头遍历,相反的是每次回溯我们都需要用cur来记录i之前已经遍历到的位置,这次回溯再从这个cur(cur为i+1,保证了不会选到重复的数字)开始,所以也就不再需要used数组记录。
电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
思路
相当于子集那道题多了一个字母映射。
组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
思路
相当于给你一个整数数组,数组中的元素互不相同,返回该数组所有可能的子集,**要求子集的和为target。解集不可以包含重复的子集,但可以包含重复的数字。**你可以按 任意顺序 返回解集。
和子集那道题对比,子集那道题如下:
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
相当于子集那道题的基础上多了两个加粗的条件。为此,我们的回溯条件改为
子集的和为target就加入答案并返回,如果子集的和已经>target,就直接返回(说明要回溯)。
除此之外,虽然解集可以包含重复的数字,但依旧是不包含重复的子集的,所以我们依旧要维护cur。 通过 cur,每次递归只能从当前位置或之后的元素开始选择,避免生成重复的组合。例如,当你已经生成组合 [2,2,3] 后,不会再生成 [2,3,2] 或 [3,2,2]。
而回溯时,我们的cur传入 i 而非 i+1,保障了可以选取到重复的数字。
在这再次贴一下子集那道题的思路:
回溯终止条件依旧是path长度==nums长度。但是子集要求是有顺序的,所以i遍历数组时不会每次都会从头遍历,相反的是每次回溯我们都需要用cur来记录i之前已经遍历到的位置,这次回溯再从这个cur(cur为i+1,保证了不会选到重复的数字)开始,所以也就不再需要used数组记录。
括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
提示:
1 <= n <= 8
思路
这道题有点意思啊hhh
有效括号的条件:
- 平衡性:在生成过程中,左括号 ( 的数量必须始终大于等于右括号 ) 的数量。
- 完整性:最终生成的字符串中,左括号和右括号的数量均为 n。
回溯法的实现:
递归路径:在每一步递归中,可以选择添加左括号或右括号,但需要满足上述条件。
剪枝条件:
- 当左括号数量小于 n 时,可以添加左括号。
- 当右括号数量小于左括号数量时,可以添加右括号。
算法步骤
递归函数参数:当前字符串 path、左括号数量 left、右括号数量 right、目标对数 n、
结果列表 result。
终止条件:当 left = n 且 right = n 时,将当前字符串加入结果列表。
递归选择:
- 添加左括号:若 left < n,添加 ( 并递归 left + 1。
- 添加右括号:若 right < left,添加 ) 并递归 right + 1。
单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
思路
标准DFS回溯。找到word[0] 的单元格 (i, j) 作为起点。
从每个起点出发,递归检查四个方向:
终止条件:
- 若当前字符索引 k 等于单词长度,说明单词已完全匹配,返回 True。
- 若越界、字符不匹配或单元格已被访问,返回 False。
递归过程:
- 标记当前单元格为已访问(例如,将 board[i][j] 改为特殊字符 #)。
- 递归搜索四个方向,只要有一个方向返回 True,则整体返回 True。
- 回溯:恢复当前单元格为原始值,继续尝试其他方向。
分割回文串
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
提示:
1 <= s.length <= 16
s 仅由小写英文字母组成
思路
其实这道题一眼看过去,会想到动态规划来做的。毕竟dp擅长处理这种子串分割的问题。
用二维数组 dp[i][j] 表示子串 s[i:j+1] 是否为回文串。
状态转移方程:
dp[i][j] = (s[i] == s[j]) and (j - i <= 2 or dp[i+1][j-1])
即当前字符相等,且子串长度小于等于 2 或去掉首尾后仍为回文。
但是纯 DP 通常更适合计算 “方案数量”,而本题要求生成所有具体的分割方案,所以即使用了动态规划,我们还是需要用回溯来生成具体的分割方案的。
譬如分割等和子集那道题:给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
就是可以用纯dp来做。
如果这道题我们真的要用纯dp来做的话。思路如下:
状态转移:
对于每个位置 i,遍历所有可能的分割点 j(0 ≤ j < i),如果子串 s[j:i] 是回文,则将 dp[j] 中的每个方案末尾添加 s[j:i],并加入 dp[i]。
路径记录:
在 DP 数组中,每个 dp[i] 存储一个列表,列表中的每个元素是一个分割方案(即字符串列表)。
时间复杂度超高!O(n^2 * 2^n)
所以我们用回溯法直接来做。
回溯函数的参数有两个,一个是cur,我们用cur维护分割点,一个是答案集合path。
cur:表示当前递归层的起始分割点,即从哪个位置开始尝试分割新的回文子串。
i:表示当前递归层的结束分割点,用于生成子串 s[cur:i+1] 并检查其是否为回文。
如果生成的子串是回文的,那么我们加入答案集和path,并且继续尝试新的起始分割点(cur=i+1)。(注意这里没有显式地去判断尝试新的结束分割点,但是实际上通过外层循环,我们是会去判断的)。如果生成的子串不是回文的,我们尝试新的结束分割点(i=i+1)。
直到达到回溯边界条件:起始分割点达到字符串终点,说明整个字符串已被成功分割为回文子串,记录结果并终止递归。
在这一个注意点是,我们为什么不需要判断 s[0:cur] 是否回文?
这是因为每次递归调用时,cur 参数表示当前待处理的子串起始位置。在递归进入下一层前,我们已经确保了:
- 当前路径 path 中的所有子串都是回文(因为加入 path 前已检查)。
- path 中的子串恰好覆盖 s[0:cur](因为每次递归时 cur 递增,且子串无重叠)。
因此,s[0:cur] 的合法性已经由历史递归步骤保证,无需重复检查。
示例说明:
假设当前递归层 cur=3,路径 path = [“aa”, “b”],说明:
s[0:2] = “aa” 是回文(已在 cur=0 时检查)。
s[2:3] = “b” 是回文(已在 cur=2 时检查)。
因此,s[0:3] 已被合法分割为回文子串,无需再次验证。
也就是说,两层循环,外层循环i(结束分割点),内层通过递归循环cur(起始分割点)。
外层循环负责在当前起始位置 cur 下,尝试所有可能的子串(通过 i 扩展结束位置)。
递归调用负责在找到一个合法子串后,固定该子串,并从下一个位置 i+1 开始处理剩余字符串。
N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
思路
这个需要说明吗?需要题解吗?
黎吧皇,你很有名, 我就不给思路了。hhh
11.二分查找
搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
提示:
- 1 <= nums.length <=
10^4
-10^4
<= nums[i] <=10^4
- nums 为 无重复元素 的 升序 排列数组
-10^4
<= target <=10^4
思路
标准二分了只能说
搜索二维矩阵
给你一个满足下述两条属性的 m x n 整数矩阵:
每行中的整数从左到右按非严格递增顺序排列。
每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false 。
思路
转化为一维数组后直接二分
在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
思路
两次二分,分别找左边界和右边界,主要是代码的一些细节。
搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
思路
原数组升序排列,旋转后数组被分为两个有序部分,左半部分元素均大于右半部分。相当于在传统二分的基础上,要额外判断一下mid落在第一段还是第二段数组上。
寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
思路
- 旋转数组的特性:
- 原数组升序排列,旋转后数组被分为两个有序部分,左半部分元素均大于右半部分。
- 例如,[4,5,6,7,0,1,2] 中,左半部分 [4,5,6,7] 均大于右半部分 [0,1,2],且最小值位于右半部分的起始位置。
- 二分查找的调整:
通过比较中间元素 mid 与右边界 right 的值,可以判断最小值位于哪一半:
- 若 nums[mid] > nums[right],说明 mid 位于左半部分,最小值在 mid 的右侧,更新 left = mid + 1。
- 若 nums[mid] < nums[right],说明 mid 位于右半部分,最小值可能是 mid 或其左侧,更新 right = mid。
- 终止条件:
当 left 和 right 相遇时,即 left == right,此时的元素即为最小值。
寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
思路
只能说不愧为hard题
我们可以将这道题转化为,寻找两个正序数组中第k1小和第k2小的元素,其中k1为(m+n+1)//2,k2为(m+n+2)//2。如m+n为奇数,则k1==k2.
递归二分查找第 k 小元素:
- 基准条件:
- 若其中一个数组为空,直接返回另一个数组的第 k 小元素。
- 若 k=1,返回两个数组首元素的最小值。
- 递归步骤:
- 比较两个数组的前 k/2 个元素(或数组剩余长度的最小值)。
- 若 nums1[k/2-1] >= nums2[k/2-1],说明 nums2 的前 k/2 个元素不可能是第 k 小元素,排除这些元素,继续在 nums1 和 nums2[k/2:] 中查找第 k - k/2 小的元素。
- 反之,排除 nums1 的前 k/2 个元素。
因为每次递归将问题规模缩小约一半,所以时间复杂度为O(log (m+n))。
在这里细说为什么,若 nums1[k/2-1] >= nums2[k/2-1],说明 nums2 的前 k/2 个元素不可能是第 k 小元素。
假设 nums1 = [5,6,7],nums2 = [1,2,3],我们要找第 4 小的元素(k=4)。
计算 k/2 = 2,比较 nums1[1] = 6 和 nums2[1] = 2。
由于 6 ≥ 2,我们可以断言:nums2 的前 2 个元素(即 [1,2])不可能是第 4 小的元素。
为什么呢?因为合并后,nums2 的前 2 个元素 [1,2] 必然排在合并后数组的前 2 位(因为它们比 nums1 的前 2 个元素都小)。所以这两个会是第k//2小的元素,但绝对不会是第k小的元素。于是至此,我们排除了 nums2 的前 k/2 个元素,第k小的元素需要在nums1和nums2[k/2:]中继续查找。
12.栈
有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
思想
没什么好说的,纯裸栈,考数据结构的。
最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
MinStack() 初始化堆栈对象。
void push(int val) 将元素val推入堆栈。
void pop() 删除堆栈顶部的元素。
int top() 获取堆栈顶部的元素。
int getMin() 获取堆栈中的最小元素。
思想
因为是常数时间内检索到最小元素,所以不能遍历整个栈,所以应该在插入或者弹出时就能拿到最小值。为此,我们让这个栈同时保存的是每个数字 x 进栈的时候的值 与 插入该值后的栈内最小值。即每次新元素 x 入栈的时候保存一个元组:(当前值 x,栈内最小值)。
字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
思想
感觉普通遍历模拟也可以做。用栈的话,栈的元素可以是一个元组 (count, str),其中:
- count 是当前层级的重复次数(对应外层的 k),
- str 是该层级下已解码的字符串片段。
每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
思路
找左/右第一个大于本元素的元素在哪,是单调栈的经典用途。
柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路
在这里最大面积就是对于每个柱子,其找左/右第一个小于本元素的元素在哪,然后计算和这个元素的下标差就是矩形的宽,然后得到候选矩形面积,最后从候选矩形面积列表挑选最大的。
因为某大厂的算法没有撕出来,怒而整理该贴。部分题目有AC代码。
13.堆
数组中的第k个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
思路
感觉和堆也没多大关系,当然,可以用堆。但是我选择快排(
前k个高频元素
面试有考到这个场景题。
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
提示:
- 1 <= nums.length <= 10^5
- k 的取值范围是 [1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。
思路
使用 Counter 计算每个数字的频率,然后创建大小为k的小顶堆。如果堆未满,直接添加元素。如果当前元素的频率大于堆顶元素的频率,则弹出堆顶元素,并将当前元素加入堆中。最后返回堆中的元素,因为可能有相同频次的元素。
数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
例如 arr = [2,3,4] 的中位数是 3 。
例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
实现 MedianFinder 类:
MedianFinder() 初始化 MedianFinder 对象。
void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
提示:
-10^5
<= num <=10^5
- 在调用 findMedian 之前,数据结构中至少有一个元素
- 最多 5 * 10^4 次调用 addNum 和 findMedian
思路
为什么不能直接快排?因为快排不能保证插入有序。而我们维护小顶堆可以保证插入有序。
建立一个 小顶堆 A 和 大顶堆 B ,各保存列表的一半元素,且规定:
A 保存 较大 的一半,长度为M/2
( N 为偶数)或 (M+1)/2
( M 为奇数)。
B 保存 较小 的一半,长度为N/2
( N 为偶数)或 (N+1)/2
( N 为奇数)。
随后,中位数可仅根据 A,B 的堆顶元素计算得到
中位数为 A的堆顶元素(M≠N)或者 (A的堆顶元素+B的堆顶元素)/ 2(M==N)
时间复杂度 O(logN) :
- 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间。
- 添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。
空间复杂度 O(N) : 其中 N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。
14.贪心
买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
提示:
- 1 <= prices.length <= 10^5
- 0 <= prices[i] <= 10^4
思路
一看就知道用dp,dp[i][0]
代表第i
天持有股票的最大利润,dp[i][1]
代表第i
天不持有股票的最大利润,最后根据贪心思想可知,答案返回dp[length-1][1]
。
跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
提示:
- 1 <= nums.length <= 10^4
- 0 <= nums[i] <= 10^5
思路
从左到右遍历数组,用一个变量记录当前能到达的最远位置。在遍历过程中,每次都尝试更新这个最远位置。如果在遍历结束前,最远位置已经超过或等于数组的最后一个下标,那就说明可以到达最后一个下标。时间复杂度为O(N)
也可以用动态规划,但是很麻烦,因为状态转移很麻烦,而且时间复杂度为O(n)
- 核心思想:从数组的最后一个位置开始向前推导,判断每个位置是否能到达最后一个下标,利用前面位置的结果来计算当前位置的结果。
- 具体过程:
- 创建一个长度为数组 nums 长度的列表 dp
- 初始值都为 False ,dp[i] 表示从位置 i 出发能否到达最后一个下标。
- 将 dp 数组的最后一个元素设为 True ,因为最后一个位置本身就在最后一个下标处,肯定能到达。
- 从倒数第二个位置开始向前遍历数组,对于每个位置 i :
- 遍历从 1 到 nums[i] 的所有可能跳跃距离 j ,如果 i + j 不超过数组长度,并且 dp[i + j] 为 True ,说明从位置 i 跳跃 j 步后能到达最后一个下标,那么 dp[i] 为 True ,并跳出当前循环(因为只要有一种跳跃方式能到达就可以了)。
- 最后返回 dp[0] ,即从起始位置是否能到达最后一个下标。
跳跃游戏Ⅱ
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
思路
最开始肯定想到的是dp,但是这个时间复杂度为也为O(n^2)
dp[i] = 从索引i跳到最后一个位置的最小跳跃次数
倒数开始遍历dp,dp[length-1] = 0
对每个i
for j in range(i-nums[i], i):
dp[i] = dp[j]+1
返回dp[0]
所以我们用贪心 + 层次遍历,模拟跳跃过程,按跳跃次数分层,每次记录当前层能到达的最远范围。
- 核心思想:
- 维护当前层的跳跃范围:记录当前层(第 step 次跳跃)能到达的最远左边界 left 和右边界 right。
- 扩展下一层范围:遍历当前层 [left, right] 内的所有位置,计算下一层能到达的最远右边界 next_right。
- 更新跳跃次数:每扩展一层,跳跃次数 step 加 1,直到 right 到达或超过终点。
划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 “ababcc” 能够被分为 [“abab”, “cc”],但类似 [“aba”, “bcc”] 或 [“ab”, “ab”, “cc”] 的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”、“defegde”、“hijhklij” 。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 这样的划分是错误的,因为划分的片段数较少。
提示:
1 <= s.length <= 500
s 仅由小写英文字母组成
思路
记录每个字符最后出现的位置:首先遍历字符串,用哈希表记录每个字符最后一次出现的索引。
- 贪心扩展当前片段边界:从左到右遍历字符串,维护当前片段的起始位置 start 和结束位置 end。
- 对于每个字符 s[i],更新 end 为当前字符最后出现的位置与 end 的较大值。
- 当遍历到 i == end 时,说明当前片段可以结束(所有字符的最后出现位置都在 end 内),记录片段长度 end - start + 1,并将 start 移动到下一片段的起始位置 i + 1。
15.动态规划
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路
dp[i]的状态由dp[i-1]或者dp[i-2]转移而来,很简单的问题。
杨辉三角
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路
dp[i][j]的状态由dp[i-1][j-1]+dp[i-1][j]转移而来。
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路
可以用dp[i][0]表示偷了第i家得到的最高金额,dp[i][1]表示没有偷第i家得到的最高金额。也可以直接一维dp数组处理,对于第i间房屋,有两种选择:
- 偷第i间:则第i-1间不能偷,此时总金额为 前i-2间的最高金额 + 第i间的金额。
- 不偷第i间:则总金额为 前i-1间的最高金额。
状态转移方程为:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
隐式表示偷了与没偷第i家的状态。
完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
思路
dp[i]表示 和为i 的完全平方数的最少数量。
关键点:每个数 n 可以表示为 n = k + s,其中 s 是某个完全平方数(如 1, 4, 9, …),则问题转化为求 n - s 的最少完全平方数个数加 1。
零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
思路
完全背包问题。dp[i]表示价值为i的最小重量。在这重量替换为了面额。求填满价值为amount的背包,所需的最少物品是多少。
单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
思路
dp[j]表示从下标0,一直到下标j-1,即长度为j的s的子串是否可以由单词列表的单词拼接而成。排列数来做
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
思路
dp[i]
表示以nums[i]
结尾的满足要求的子序列的最长长度(所以一定包括nums[i]
)。
初始化dp都为1,因为自己可以作为一个序列。状态转移方程为:
for i in range(1, length):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
这里之所以是dp[i] = max(dp[i], dp[j] + 1)
而不是单纯的dp[i] = dp[j] + 1
,是因为这里同一个dp[i]
可能被不同位置的dp[j]
更新。
譬如对于测试用例[0,1,0,3,2,3]
i=3,nums[i]=3时,因为3大于前面所有的数,所以dp[i]会被下标0一直到下标2的dp[j]
更新。而下标0到下标2的dp[j]分别是1 2 1
所以如果是单纯的dp[i] = dp[j] + 1
,会导致明明dp[i]被下标为1的dp[j]更新为了3,但是最后又被下标为2的dp[j]更新为了更小的2.
我们应该维护最大的dp[j]更新的状态,所以用了dp[i] = max(dp[i], dp[j]+1)
。
乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
- 1 <= nums.length <=
2 * 10^4
- -10 <= nums[i] <= 10
- nums 的任何子数组的乘积都 保证 是一个 32-位 整数
思路
因为nums 的任何子数组的乘积都 保证 是一个 32-位 整数,所以也就是说每个状态都是一个32位的整数,所以我们知道可以用整型数组dp去存储每个状态。
这道题的难点就是存在负数,为此除了我们常规维护的代表最大乘积的dp数组外,我们还维护一个代表最小乘积的dp数组。
那么全局最大乘积为所有 max_dp[j] 中的最大值。
分割等和子集
哦才发现,这道题笔试中也遇到过。
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路
最好想的边界判断就是总和必须是偶数。
除此之外,为什么能想到用dp动态规划呢?是因为它具有典型的子集划分问题特征,且满足动态规划的适用条件:最优子结构和重叠子问题。
题目可以转化为:是否存在一个子集,其元素和恰好为 target。在这target为sum/2
这个问题又可以进一步转化为0-1 背包问题:
- 背包容量为 target,
- 物品重量为数组元素 nums[i],
- 目标:判断是否可以用若干物品填满背包(即重量总和恰好为 target)。
状态定义
设 dp[i]表示是否能组成和为i的子集,所以为布尔值。
在 0-1 背包问题中,必须逆序遍历容量(从 target 到 num),确保每个元素只被选取一次。若正序遍历,会导致同一元素被重复选取(类似完全背包),与题意不符。
一旦发现 dp[target] 为 True,可以立即返回结果
最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
提示:
- 0 <= s.length <=
3 * 10^4
- s[i] 为 ‘(’ 或 ‘)’
思路
这道题一眼看到估计会用栈。
利用栈保存括号的下标,而非括号本身。通过下标差值计算有效括号子串的长度。
初始时,栈中压入 -1 作为 “基准下标”,用于计算第一个有效括号的长度(例如 “()” 中,右括号与 -1 的差值为 2)。
遍历字符串:
- 遇到 ( 时,将当前下标压入栈。
- 遇到 ) 时,弹出栈顶元素。
- 若此时栈为空,说明当前 ) 无匹配的 (,将当前下标压入栈作为新的基准。
- 若栈不为空,当前 ) 与栈顶元素(最近的未匹配的 ( 的下标)的差值,即为以当前 ) 结尾的有效括号子串的长度。
- 更新最长长度:每次计算差值后,与当前最大值比较,取较大者。
用dp也可以,但是个人感觉比较麻烦。
16.多维动态规划
不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
提示:
- 1 <= m, n <= 100
- 题目数据保证答案小于等于 2 * 109
思路
为什么会考虑用纯dp做,而不是回溯或者dfs或者bfs。
因为如果用回溯/DFS/BFS来做的话,每次移动有 2 种选择(右 / 下),总路径数为组合数 C(m+n-2, m-1)。当m和n达到20左右时,就可能导致栈溢出或超时了。而这道题的m和n可以达到100的量级。
为什么是这个组合数呢?因为从 (0,0) 到 (m-1, n-1),机器人需要向右移动 (m-1) 次,向下移动 (n-1) 次,总移动次数为 (m-1)+(n-1) = m+n-2 次,其中向右移动m-1次,所以组合数为C(m+n-2, m-1)。
为此我们考虑dp,dp[i][j] 表示到达 (i,j) 的路径数。最后返回dp[-1][-1]即可。
最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路
也是dp[i][j]表示从左上角达到(i,j)的最小数字总和。
最长回文子串
给你一个字符串 s,找到 s 中最长的 回文 子串。
提示:
- 1 <= s.length <= 1000
- s 仅由数字和英文字母组成
思路
这道题可以用dp做,也可以用贪心做。
贪心的做法是,对于每个位置i,尝试寻找以i结尾的最长回文子串,若找到,则更新。若没找到,则i++。时间复杂度为O(n^2)
dp做就是,设 dp[i][j] 表示字符串 s 从索引 i 到 j(闭区间)的子串是否为回文子串。
状态值:dp[i][j] = True 表示子串 s[i…j] 是回文,False 则不是。
目标:找到所有 dp[i][j] = True 的子串中长度最长的,记录其起始位置和长度。
时间复杂度也是O(n^2)
最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
思路
dp[i][j]表示考虑到text1[i-1]和text2[j-1]结尾的最长公共子序列的长度
编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
思路
dp[i][j]就是以word1[i-1]为结尾的字符串转化到以word2[j-1]为结尾的字符串所用的最少操作数。
伪代码如下:
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
"""
dp[i-1][j]+1就是在word1结尾进行删除
dp[i][j-1]+1就是在word1结尾进行添加
dp[i-1][j-1]+1就是在word1结尾进行替换
"""
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])+1
17.技巧
只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
1 <= nums.length <= 3 * 10^4
-3 * 10^4 <= nums[i] <= 3 * 10^4
除了某个元素只出现一次以外,其余每个元素均出现两次。
思路
我们可以利用 异或运算(XOR) 的特性:
异或的性质:
a ^ a = 0(相同数字异或结果为 0)
a ^ 0 = a(任何数字与 0 异或仍是它本身)
异或满足交换律和结合律,即 a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b。
因此,对整个数组进行异或运算,最终结果就是只出现一次的数字。
代码
class Solution:
def singleNumber(self, nums: List[int]) -> int:
res = 0
for num in nums:
res ^= num
return res
多数元素
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
n == nums.length
1 <= n <= 5 * 10^4
-10^9
<= nums[i] <= 10^9
进阶:尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。
思路1:排序
假设整个数组元素个数为n,因为多数元素的个数一定大于n//2
。
所以排序后,下标为n//2
的元素一定是多数元素。
因为假设排序后的数组构成如下:
前x个比多数元素小的元素+k个多数元素+后y个比多数元素大的元素
其中x一定小于n//2,y一定小于n//2,不然就和多数元素的定义违背了
所以构成就是
x+k+y=n
其中x<n//2,y<n//2,k>n//2
画线段长度,找到中间的点,那么一定是在k那部分出现的。
代码复杂度为O(nlogn)
,因为python
底层的nums.sort()
时间复杂度是这个。
空间复杂度为O(1)(原地排序)
或O(n)(非原地排序)
。
代码1:排序
class Solution:
def majorityElement(self, nums: List[int]) -> int:
nums.sort()
return nums[len(nums)//2]
0ms,击败100.00%
思路2:候选人算法
维护一个候选人数字和候选人数字对应的选票,然后遍历数组。
如果遍历的当前数字和候选人数字不同的话,候选人数字对应的选票-1.
如果遍历的当前数字和候选人数字相同的话,候选人数字对应的选票+1.
如果选票为0,候选人数字被替代成当前数字。
这个算法正确是因为多数元素的选票最后一定>=0,所以最后候选人数字一定是多数数字。时间复杂度为O(n)
,空间复杂度为O(2)
代码2:候选人算法
class Solution:
def majorityElement(self, nums: List[int]) -> int:
candidate = nums[0]
vote = 1
for i in range(1, len(nums)):
if candidate != nums[i]:
vote -= 1
else:
vote += 1
if vote == 0:
candidate = nums[i]
vote = 1
return candidate
6ms,击败54.58%。
明明时间复杂度更低,但是实际运行时间更长hhh
颜色分类
思路:荷兰国旗解法/三指针法/三分类问题
荷兰国旗问题就是该题目的问题。
三指针法适用于该类的所有变种,就是需要划分为三个部分
<x和=x和>x的三个部分
的问题。
我们维护三个指针:
left
:0的右边界(指向最终数组最后一个0的下标+1)right
:2的左边界(指向最终数组第一个2的下标-1)cur
:当前遍历的数字。
初始时left为0,cur为0,right为len(nums)-1,然后随着cur的向右遍历,
left逐步向右扩大,right逐步向左扩大,直到我们cur超过right指针,表示所有的0和2都已经排序好,那么相应地,1也会排序好。
注意:当nums[right]==2时,我们不应该有cur+=1。
即right和cur位置进行交换,因为right位置的数字可能是0,1,2。
所以cur不能向右移动,因为需要二次检查。
而如果nums[cur]==0,那么就是left和cur位置进行交换。
又因为left永远指向第一个非0位置,并且left永远在cur的左边。
所以left位置都是排列好的数字,所以left位置只会是1.所以不需要二次检查。
可以通过[1,2,0]例子来查看。
代码
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
left, cur = 0, 0
right = len(nums)-1
while cur <= right:
if nums[cur] == 0 :
nums[left], nums[cur] = nums[cur], nums[left]
left += 1
cur += 1
elif nums[cur] == 2:
nums[right], nums[cur] = nums[cur], nums[right]
right -= 1
"""
注意这里不能有cur += 1。right和cur位置进行交换,因为right位置的数字可能是0,1,2
所以cur不能向右移动,因为需要二次检查。而如果nums[cur]==0,那么就是left和cur位置进行交换
又因为left永远指向第一个非0位置+left永远在cur的左边,所以left位置都是排列好的数字
所以left位置只会是1.所以不需要二次检查。可以通过[1,2,0]例子来查看。
"""
else:
cur += 1
return
0ms,击败100.00%
下一个排列
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100
思路
错误思路
最开始我想的思路是:
从左到右遍历数组,对每个数找到其右边第一个大于它的数。
- 找得到,冒泡到当前数的前面,并进行输出
- 找不到,继续向右遍历下一个数
如果直到遍历完所有数,都找不到,则直接输出升序排列的数组。
为此我们可以预处理整个数组,得到rightMax,如果rightMax都是-1,
则意味着直到遍历完所有的数,都会找不到右边更大的数,那么直接输出升序排列的数组。
但实际上这样的思路是错的,归根结底错在从左到右遍历这上面。
因为越左的数权重越大,越右的数权重越小,我们要找的下一个更大的字典序排列应该是尽可能修改越右的数的。
其次,错误的点在于,不应该找右边第一个大于它的数。而应该找右边第一个大于它且最接近它的数。
譬如对于例子1,3,2
,按照我的思路一开始修改的是
3,1,2
,但实际上对于这个例子的正确答案应该是2,1,3
。
正确思路
正确的思路应该是,我们将整个数组根据上升/下降趋势划分为不同的区间。
如果排除掉题目的特殊规定,即如果找不到下一个区间,那么将字典序最小的区间(完全上升)认为是其下一个区间,那么我们发现:
如果一个区间是上升的(从左往右看),那么它存在字典序更大的下一个区间。
如果一个区间是下降的(从左往右看),那么它不会存在字典序更大的下一个区间。
所以我们如果要找到题目的下一个区间,首先我们得找到从左往右看的最后一个上升区间,修改它,修改的权重小,如果是从左往右看的第一个上升区间的话,修改的权重大。
所以反过来,就是我们要找到从右往左看的第一个下降区间,我们要修改它。
那么具体修改这个区间的哪个数呢?同样的,根据我们应该修改权重最小的那个数,那么就是这个区间的右边界这个数(从左往右看)。
所以准确来说,我们要修改的是,从右往左看的第一个下降点。
具体应该怎么修改呢?刚刚也有提到,从左往右看的话,对于要修改的数,不应该找右边第一个大于它的数,而应该找右边第一个大于它且最接近它的数。
那么在这里,从右往左看的话,我们需要找到下降点右边第一个大于它且最接近它的数,又因为这个数肯定处在上升区间(从右往左看),所以从右往左遍历到的第一个大于下降点的数就是了。
我们交换这两个数,之后从左往右看,反转下降点右边的下降区间,将其都变为上升区间,从而保证了字典序最小。
看不懂的话看一下这一版deepseek的解释:
-
排列的“递增”和“递减”趋势:
- 如果我们从后向前看,排列的末尾部分通常是降序的(比如 […, 3, 2, 1]),这时候已经是这部分的最大排列,无法再增大。
- 如果末尾部分是升序的(比如 […, 1, 2, 3]),我们可以通过交换来生成更大的排列。
-
关键点:第一个下降的位置:
- 从后向前遍历,找到第一个满足 nums[i] < nums[i+1] 的位置 i。这意味着 nums[i] 是可以增大的,而 nums[i+1…n-1] 是降序的(即这部分已经最大,无法再增大)。
- 例如,[1, 3, 2] 中 i=0(nums[0]=1 < nums[1]=3),而 nums[1…2]=[3,2] 是降序的。
-
交换和反转:
为了生成最小的更大排列,我们需要:- 在 nums[i+1…n-1] 中找到最小的比 nums[i] 大的数 nums[j](因为这部分是降序的,所以从后向前找第一个比 nums[i] 大的数即可)。
- 交换 nums[i] 和 nums[j],这样 nums[i] 变大了,但 nums[i+1…n-1] 仍然是降序的。
- 反转 nums[i+1…n-1],使其变成升序(即最小的排列),从而保证新排列是严格的下一个排列。
寻找重复数
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
提示:
- 1 <= n <= 10^5
- nums.length == n + 1
- 1 <= nums[i] <= n
- nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
- 如何证明 nums 中至少存在一个重复的数字?
- 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?
思路
最关键点在于1 <= nums[i] <= n,所以将数组视为一个链表,其中 nums[i] 表示节点 i 指向的下一个节点是 nums[i]。由于存在重复数字,链表一定存在环,且环的入口就是重复的数字。
确定这个链表不会存在独立节点的关键点就是因为数字范围是 [1, n],而数组长度是 n + 1,因此可以将 nums[i] 看作指针。
那么就转换为了快慢指针问题。
- 第一阶段:检测环:
用快慢指针,慢指针每次走一步(slow = nums[slow]),快指针每次走两步(fast = nums[nums[fast]])。直到快慢指针相遇。 - 第二阶段:找到环的入口(重复数字):
将快指针重置到起点(0),然后快慢指针每次都走一步。
再次相遇的点就是环的入口(重复数字)。