力扣算法学习记录
`
文章目录
- 力扣算法学习记录
- 前言
- 一、排序问题
- 一、前缀和+哈希表
- 二、数位DP
- 1. 算法简述
- 2. 练习习题
- 2.1 [2376. 统计特殊整数](https://leetcode.cn/problems/count-special-integers/description/)
- 2.2 1012. 至少有 1 位重复的数字
- 2.3 [233. 数字 1 的个数](https://leetcode.cn/problems/number-of-digit-one/description/)
- 2.4 [面试题 17.06. 2出现的次数](https://leetcode.cn/problems/number-of-2s-in-range-lcci/description/)
- 2.5 [600. 不含连续1的非负整数](https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones/description/)
- 2.6 [902. 最大为 N 的数字组合](https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/description/)
- 2.7 [1397. 找到所有好字符串](https://leetcode.cn/problems/find-all-good-strings/description/)
- 三、模运算相关
- 三、树状数组与线段树
- 四、动态规划
- 五、拓扑排序
- 六、区间dp
- 七、杂项
前言
记录2023年在寻找工作过程中学习过的数据结构与算法。题目均以用到的核心算法思想划为单位划分。这都是来自于大佬灵茶山艾府的总结。我也记录了一些我自己的理解。
值得一提的是,所有的算法和数据结构都是为了解决问题而服务的,思考问的方式应该由简到繁。
一、排序问题
1.冒泡
2. 选择
3. 插入
4. 归并
4.1 算法思想
4.2 算法模板
5. 快速排序
5.1 算法思想
5.2 算法模板
6. 堆排序
6.1 算法思想
6.2 算法模板
一、前缀和+哈希表
1.算法简述
核心表达式:s[right] = Function(s[left]), 最经典的条件即 s[right] = s[left]
2.练习习题
2.1 面试题 17.05. 字母与数字
题目链接:求解最长的包含相同数量字母与数字的连续数组长度。
思路:
- 转换:数字 → -1,字母→1。
- 前缀和:根据前缀和S的定义,S[j] - S[i]可以表示区间(i, j](左开右闭)的元素和。
- 经过前两项处理,可以得当S[j] - S[i] = 0时,区间(i, j]包含的字母与数字数量相等。
- 加速查询:若遍历所有的(i,j)时间复杂度为O(n^2),依然有点高,因此借用哈希表来提高效率。S[j] - S[i] = 0 → S[j] = S[i]。
代码如下:
class Solution:
def findLongestSubarray(self, array: List[str]) -> List[str]:
n = len(array)
S = [0] * (n + 1) # 前缀和
helper = {0: 0} # 哈希表
l, r, ans = 0, 0, 0
for i in range(n):
if len(array[i]) >= 2 or ord('0') <= ord(array[i]) <= ord('9'): # 预处理
S[i + 1] = S[i] - 1
else:
S[i + 1] = S[i] + 1
if helper.get(S[i + 1], -1) != -1: # 不存在相同值
if (i + 1 - helper[S[i + 1]]) > ans:
ans = i + 1 - helper[S[i + 1]]
l, r = helper[S[i + 1]], i + 1 # 最长区间记录
else:
helper[S[i + 1]] = i + 1 # 只记录最左边的坐标以保证求解最长子区间
return array[l:r]
2.2 523. 连续的子数组和
题目链接
思路同上:首先分析题目,要求区间和能被k整除,即(s[j] - s[i]) % k = 0,根据取模的性质:
(
s
[
j
]
−
s
[
i
]
)
m
o
d
k
=
0
→
(
(
s
[
j
]
m
o
d
k
)
−
(
s
[
i
]
m
o
d
k
)
)
m
o
d
k
=
0
→
(
s
[
j
]
m
o
d
k
)
=
(
s
[
i
]
m
o
d
k
)
(s[j] - s[i])\ mod\ k = 0 \\ → ((s[j]\ mod\ k) - (s[i]\ mod\ k) ) \ mod\ k = 0 \\ → (s[j]\ mod\ k) = (s[i]\ mod\ k)
(s[j]−s[i]) mod k=0→((s[j] mod k)−(s[i] mod k)) mod k=0→(s[j] mod k)=(s[i] mod k)
其他同理:
代码如下:
class Solution:
def checkSubarraySum(self, nums: List[int], k: int) -> bool:
n = len(nums)
s = [0] * (n + 1)
helper = {0:0} # 注意初始化
for i in range(n):
s[i + 1] = s[i] + nums[i]
if helper.get(s[i + 1] % k, -1) != -1:
if i + 1 - helper[s[i + 1] % k] >= 2:
return True
else:
helper[s[i + 1] % k] = i + 1
return False
2.3 525. 连续数组
2.4 560. 和为 K 的子数组
2.5 974. 和可被 K 整除的子数组
2.6 1590. 使数组和能被 P 整除
2.7 2488. 统计中位数为 K 的子数组
二、数位DP
1. 算法简述
一种回溯的变体,主要针对的问题都是寻找区间[1, n]内满足某些条件的数字。
针对给的模板,通常改动的地方分为n个部分:
通用模板:
def dfs(i, mask, is_num, is_limit):
if i == len(s):
return int(is_num)
cnt = 0
if not is_num:
cnt = dfs(i + 1, mask, False, False) # 跳过高位
up = s[i] if is_limit else 9 # 确定上限
for i in range(1 - int(is_num), up + 1):
dfs(i + 1, mask, True, i == up and is_limit) # 这里高位已经填充了数字 所以后面就不能跳过了
return
2. 练习习题
2.1 2376. 统计特殊整数
2.2 1012. 至少有 1 位重复的数字
题目链接
绕个弯,就是求解(区间内数字的总数 - 每一位都不重复的数字)
2.3 233. 数字 1 的个数
2.4 面试题 17.06. 2出现的次数
2.5 600. 不含连续1的非负整数
经过这道题学习到了,应该学习的是其中蕴含的思想,强行套模板反而会限制自己。
2.6 902. 最大为 N 的数字组合
2.7 1397. 找到所有好字符串
数位DP 与 KMP 算法的结合
三、模运算相关
1.算法简述
2.练习习题
3.1 1015. 可被 K 整除的最小整数
三、树状数组与线段树
1.算法简述
核心:区间查询与单点修改,这里面涉及到的计算不仅仅局限于求和,还可以维护最大值什么的。
2.练习习题
2.1 1626. 无矛盾的最佳球队
区间最大值的维护,目前只想清楚了查询[0, R]的最大值。
四、动态规划
1. 算法简述
- 最简单的最直观的思考方式是 先递 再回退,可利用修饰符@cache去减少消耗时间,@cache的本质就是记录访问过的状态,不用做大量的重复工作
- 然后利用数组省略递的过程,化简为推
- 然后再用状态压缩对将dp数组进行压缩,从而降低空间占用
2. 练习习题
2.1 1143. 最长公共子序列
2.2 1092. 最短公共超序列
题目链接
新的思想,并不是传统的返回数字,而是需要返回构造的字符串,假如将构造的过程放在递归或者递推里面,同样会超时。因此将构造与递推的过程拆解开来,这样时间复杂度就能由O(nm(n + m)) 降为O(nm + (n + m))了
五、拓扑排序
1. 算法简述
2. 习题练习
2.1 207. 课程表
题目链接
这是最基本的拓扑排序的应用。
- 建立入度表
- 建立哈希表,记录节点的前置节点是哪些,便于入读表的更新
- 入度为0的节点入栈
- 不断出栈,更新入度表
- 遍历入度表,新增入度为0的节点入栈
- 若栈为空则结束遍历,若不为空,返回至步骤4
- 遍历入度表,若存在没遍历过的节点则无法修满所有课程。
六、区间dp
1.算法思想
输入i, j 确定递归的区间,然后遍历k进一步将问题拆解。
2.习题练习
2.1 1039. 多边形三角剖分的最低得分
2.2 375. 猜数字大小 II
- 运用区间dp的思想去思考这个问题
- 假设dfs(i, j)为给定区间[i, j]时,返回能够确保你获胜的最小现金数。
- 假设我们猜数字k,需要假设我们会付出最大的代价,即答案在代价更高的那个区间。则获胜的现金数为 k + max{dfs(i, k - 1), dfs(k + 1, j)}
- 为了使得获胜的现金数量最少,我们需要确保选择的k在当前阶段选择是代价最小的一个,因此,根据以下公式去选择k:argmin{ k + max{dfs(i, k - 1), dfs(k + 1, j)}},where k ∈ [1, n]
2.3 1000. 合并石头的最低成本
题目链接
另一种区间dp的形式,附加了额外辅助变量。又一次的敲响警钟。算法是为了解决问题而服务的。这次我虽然想到了区间dp,但是硬套模板还是让我想不明白怎么实现具体细节。
这次的问题难点在于不知道怎么处理数组中间融合后的情况,这也思考的话数组会变化,也不好用区间去处理。需要换个角度思考,从边界入手。
七、杂项
1.与字符串有关的例题
1.1 1638. 统计只差一个字符的子串数目
题目链接
优化后的代码思路:
1.枚举所有的 d = i - j
2.枚举所有的终点 i
3.维护k1, k0
1.2 795. 区间子数组个数
题目链接
自己想到的思路:利用单调栈,枚举nums[i]以为最大值的子区间数量
灵茶山艾府的思路:枚举以nums[i]为右端点的符合条件的子区间,并且维护两个关键坐标i1, i0.
1.3 2444. 统计定界子数组的数目
题目链接
同一种思想,具体的求解方式还是需要思考。对于较为复杂的情况,需要从简单的情况出发,进一步延申到复杂情况下的解。
维护三个坐标:
r
0
,
m
a
x
I
,
m
i
n
I
r0, maxI,minI
r0,maxI,minI
2.前缀和+二分法
2.1 2602. 使数组元素全部相等的最少操作次数
题目连接
第一眼就能看出来暴力解法(时间复杂度为O(n^2))会超时,但是不知道怎么区别出正负的情况。
解决思路:先对数组进行排序,然后就能用二分查找找出哪些num[i]比queries[i]大,哪些比queries[i]小。然后就很好处理了,利用前缀和进一步降低时间复杂度。