普通视图
都2025年了,还有面试问A*寻路的???
Node.js v25.0.0 发布——性能、Web 标准与安全性全面升级 🚀🚀🚀
3个技巧让你彻底搞懂JavaScript异步编程
想偷卷?但微信不支持md文档?这个软件助你!
📝 Markdown 查看器 - 现代化的文档预览工具
一个基于 React 19 + TypeScript 构建的现代化 Markdown 文档查看器,支持实时预览、语法高亮、数学公式渲染等功能。
在微信或浏览器上打开此编辑器,上传你的md文档可以上课偷偷看自己写的博客哈哈,这个是我解决微信这个没有md预览的痛点,自己用ai搞了一个小工具出来,效果还不错,还有图片可以借助图床工具:图床 - 简单、快速、免费的图床把自己图片上传到这里,就不会导致路径问题了。
项目概述
项目背景
在日常开发和写作中,我们经常需要预览 Markdown 文档的渲染效果。虽然市面上有很多 Markdown 编辑器,但大多数要么功能过于复杂,要么界面不够现代化。因此,我开发了这个轻量级、功能完整的 Markdown 查看器。
核心特性
- 现代化 UI 设计 - 基于 Tailwind CSS 的响应式设计
- 多主题支持 - 亮色/暗色/护眼模式,适应不同使用场景
- 完美适配移动端 - 响应式布局,手机平板都能完美使用
- 强大的 Markdown 渲染 - 支持 GFM、数学公式、代码高亮
- 智能本地存储 - 自动保存文档,刷新不丢失
- 极速加载体验 - 优化的构建配置,秒开应用
技术架构
前端技术栈
React 19.1.1 # 最新的 React 版本,性能更优
TypeScript 5.9.3 # 类型安全,开发体验更好
Vite (Rolldown) # 下一代构建工具,构建速度极快
Tailwind CSS 3.4.10 # 原子化 CSS 框架
核心依赖
react-markdown 9.0.1 # Markdown 渲染引擎
highlight.js 11.9.0 # 代码语法高亮
katex 0.16.9 # 数学公式渲染
lucide-react 0.400.0 # 现代化图标库
项目结构
src/
├── components/ # 组件目录
│ ├── Header.tsx # 顶部导航栏
│ ├── Sidebar.tsx # 侧边栏文件管理
│ └── MainContent.tsx # 主内容区域
├── stores/ # 状态管理
│ └── useAppStore.ts # Zustand 全局状态
├── utils/ # 工具函数
│ ├── api.ts # API 接口
│ ├── cache.ts # 缓存管理
│ └── markdown.ts # Markdown 处理
└── types/ # TypeScript 类型定义
└── index.ts
项目地址: github.com/ac666666666…
在线预览: http://129.204.12.129:9080/
pc端:
移动端:
如果这个项目对你有帮助,欢迎 Star ⭐ 支持!
错误处理:异常好于状态码
错误处理有不同的方式。
JavaScript 和 Python 是抛出异常, Rust 语言是变相抛出异常。
C 语言和 Go 语言则是返回一个错误值,你必须判断该值是否为 -1 或空值。
我一直想知道,哪一种方式更好?
前不久,我读到一篇多年前的文章,明确提出抛出异常好于返回状态码。他的理由很有说服力,文章好像还没有中译,我就翻译出来了。
异常与返回状态码
作者:内德·巴切尔德(Ned Batchelder)
原文网址:nedbatchelder.com
在软件中,错误处理有两种方式:抛出异常(throwing exceptions)和返回状态码(returning status codes)。
几乎所有人都认为异常是更好的处理方式,但有些人仍然更喜欢返回状态码。本文解释为什么异常是更好的选择。
一、代码干净
异常可以让你在大部分代码中省去错误处理步骤。它会自动通过不捕捉异常的层,向上传递。你因此可以编写完全没有错误处理逻辑的代码,这有助于保持代码的简洁易读。
让我们比较一下,编写同一个简单函数的两种方法。
先是返回状态码。
STATUS DoSomething(int a, int b) { STATUS st; st = DoThing1(a); if (st != SGOOD) return st; st = DoThing2(b); if (st != SGOOD) return st; return SGOOD; }
上面示例中,必须判断DoThing1(a)
和DoThing2(b)
的返回值是否正常,才能进行下一步。
如果是抛出异常,就不需要中间的错误判断了。
void DoSomething(int a, int b) { DoThing1(a); DoThing2(b); }
这只是最简单的情况,如果遇到复杂的场景,状态码带来的噪音会更严重,异常则可以保持代码的整洁。
二、有意义的返回值
状态码会占用宝贵的返回值,你不得不增加代码,判断返回值是否正确。
有些函数本来只需要返回一个正常值,现在不得不增加返回错误的情况。随着时间的推移,代码量不断增长,函数变得越来越大,返回值也越来越复杂。
比如,很多函数的返回值是有重载的:"如果失败,则返回 NULL",或者失败返回 -1。结果就是每次调用这个方法,都需要检查返回值是否是 NULL 或 -1。如果函数后来增加新的错误返回值,则必须更新所有调用点。
如果是抛出异常,那么函数就总是成功的情况下才返回,所有的错误处理也可以简化放在一个地方。
三、更丰富的错误信息
状态码通常是一个整数,能够传递的信息相当有限。假设错误是找不到文件,那么是哪一个文件呢?状态码无法传递那么多信息。
返回状态码的时候,最好记录一条错误消息,放在专门的错误日志里面,调用者可以从中获取详细信息。
异常完全不同,它是类的实例,因此可以携带大量信息。由于异常可以被子类化,不同的异常可以携带不同的数据,从而形成非常丰富的错误消息体系。
四、可以处理隐式代码
某些函数无法返回状态码。例如,构造函数就没有显式的返回值,因此无法返回状态码。还有一些函数(比如析构函数)甚至无法直接调用,更不用说返回值了。
这些没有返回值的函数,如果不使用异常处理,你不得不想出其他方法来给出错误信息,或者假装这些函数不会失败。简单的函数或许可以做到无故障,但代码量会不断增长,失败的可能性也随之增加。如果没有办法表达失败,系统只会变得更加容易出错,也更加难以捉摸。
五、错误的可见性
考虑一下,如果程序员疏忽了,没有写错误处理代码,会发生什么情况?
如果返回的状态码没有经过检查,错误就不会被发现,代码将继续执行,就像操作成功一样。代码稍后可能会失败,但这可能是许多步操作之后的事情,你如何将问题追溯到最初错误发生的地方?
相反的,如果异常未被立刻捕获,它就会在调用栈中向上传递,要么到达更高的 catch 块,要么到达顶层,交给操作系统处理,操作系统通常会把错误呈现给用户。这对程序是不好的,但错误至少是可见的。你会看到异常,能够判断出它抛出的位置,以及它应该被捕获的位置,从而修复代码。
这里不讨论错误未能报出的情况,这种情况无论是返回状态码还是抛出异常,都没用。
所以,对于报出的错误没有被处理,可以归结为两种情况:一种是返回的状态码会隐藏问题,另一种是抛出异常会导致错误可见。你会选择哪一种?
六、反驳
著名程序员 Joel Spolsky 认为,返回状态码更好,因为他认为异常是一种糟糕得多的 goto。
"异常在源代码中是不可见的。阅读代码块时,无法知道哪些异常可能被抛出,以及从哪里抛出。这意味着即使仔细检查代码,也无法发现潜在的错误。"
"异常为一个函数创建了太多可能的出口。要编写正确的代码,你必须考虑每一条可能的代码路径。每次调用一个可能抛出异常的函数,但没有立即捕获异常时,函数可能突然终止,或者出现其他你没有想到的代码路径。"
这些话听起来似乎很有道理,但如果改为返回状态码,你就必须显式地检查函数每一个可能的返回点。所以,你是用显式的复杂性换取了隐式的复杂性。这也有缺点,显式的复杂性会让你只见树木不见森林,代码会因此变得杂乱无章。
当面临这种显式复杂性时,程序员会写得不胜其烦,最后要么用自定义的方法隐藏错误处理,要么索性省略错误处理。
前者隐藏错误处理,只会将显式处理重新变为隐式处理,并且不如原始的 Try 方法方便和功能齐全。后者省略错误处理更糟糕,程序员假设某种错误不会发生,从而埋下风险。
七、总结
返回状态码很难用,有些地方根本无法使用。它会劫持返回值。程序员很容易不去写错误处理代码,从而在系统中造成无声的故障。
异常优于状态码。只要你的编程语言提供了异常处理工具,请使用它们。
(完)
文档信息
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
- 发表日期: 2025年10月22日
每日一题-执行操作后元素的最高频率 II🔴
给你一个整数数组 nums
和两个整数 k
和 numOperations
。
你必须对 nums
执行 操作 numOperations
次。每次操作中,你可以:
- 选择一个下标
i
,它在之前的操作中 没有 被选择过。 - 将
nums[i]
增加范围[-k, k]
中的一个整数。
在执行完所有操作以后,请你返回 nums
中出现 频率最高 元素的出现次数。
一个元素 x
的 频率 指的是它在数组中出现的次数。
示例 1:
输入:nums = [1,4,5], k = 1, numOperations = 2
输出:2
解释:
通过以下操作得到最高频率 2 :
- 将
nums[1]
增加 0 ,nums
变为[1, 4, 5]
。 - 将
nums[2]
增加 -1 ,nums
变为[1, 4, 4]
。
示例 2:
输入:nums = [5,11,20,20], k = 5, numOperations = 1
输出:2
解释:
通过以下操作得到最高频率 2 :
- 将
nums[1]
增加 0 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
0 <= k <= 109
0 <= numOperations <= nums.length
两种方法:差分/三指针+双指针(Python/Java/C++/Go)
方法一:差分
前置知识:差分原理讲解。
设 $x = \textit{nums}[i]$。根据题意,$x$ 可以变成 $[x-k,x+k]$ 中的整数。
题目让我们计算操作后,最多有多少个数相同。
例如 $\textit{nums}=[2,4]$,$k=1$,$\textit{numOperations}=2$。$2$ 可以变成 $[1,3]$ 中的整数,$4$ 可以变成 $[3,5]$ 中的整数。$2$ 和 $4$ 都可以变成 $3$,所以答案是 $2$。
一般地,$x$ 可以变成 $[x-k,x+k]$ 中的整数,我们可以把 $[x-k,x+k]$ 中的每个整数的出现次数都加一,然后统计出现次数的最大值。这可以用差分实现。
计算差分的前缀和。设有 $\textit{sumD}$ 个数可以变成 $y$。
如果 $y$ 不在 $\textit{nums}$ 中,那么 $y$ 的最大出现次数不能超过 $\textit{numOperations}$,与 $\textit{sumD}$ 取最小值,得 $\min(\textit{sumD}, \textit{numOperations})$。
如果 $y$ 在 $\textit{nums}$ 中,且出现了 $\textit{cnt}$ 次,那么有 $\textit{sumD}-\textit{cnt}$ 个其他元素(不等于 $y$ 的数)可以变成 $y$,但这不能超过 $\textit{numOperations}$,所以有
$$
\min(\textit{sumD}-\textit{cnt}, \textit{numOperations})
$$
个其他元素可以变成 $y$,再加上 $y$ 自身出现的次数 $\textit{cnt}$,得到 $y$ 的最大频率为
$$
\textit{cnt} + \min(\textit{sumD}-\textit{cnt}, \textit{numOperations}) = \min(\textit{sumD}, \textit{cnt}+\textit{numOperations})
$$
注意上式兼容 $y$ 不在 $\textit{nums}$ 中的情况,此时 $\textit{cnt}=0$。
本题视频讲解,欢迎点赞关注~
答疑
问:为什么代码只考虑在 $\textit{diff}$ 和 $\textit{nums}$ 中的数字?
答:比如 $x$ 在 $\textit{diff}$ 中,$x+1,x+2,\ldots$ 不在 $\textit{diff}$ 中,那么 $x+1,x+2,\ldots$ 的 $\textit{sumD}$ 和 $\textit{x}$ 的是一样的,无需重复计算。此外,要想算出比 $\min(\textit{sumD}, \textit{cnt}+\textit{numOperations})$ 更大的数,要么 $\textit{sumD}$ 变大,要么 $\textit{cnt}$ 变大。「变大」时的 $x$ 必然在 $\textit{diff}$ 或 $\textit{nums}$ 中。
###py
class Solution:
def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
cnt = defaultdict(int)
diff = defaultdict(int)
for x in nums:
cnt[x] += 1
diff[x] # 把 x 插入 diff,以保证下面能遍历到 x
diff[x - k] += 1 # 把 [x-k,x+k] 中的每个整数的出现次数都加一
diff[x + k + 1] -= 1
ans = sum_d = 0
for x, d in sorted(diff.items()):
sum_d += d
ans = max(ans, min(sum_d, cnt[x] + numOperations))
return ans
###java
class Solution {
int maxFrequency(int[] nums, int k, int numOperations) {
Map<Integer, Integer> cnt = new HashMap<>();
Map<Integer, Integer> diff = new TreeMap<>();
for (int x : nums) {
cnt.merge(x, 1, Integer::sum); // cnt[x]++
diff.putIfAbsent(x, 0); // 把 x 插入 diff,以保证下面能遍历到 x
// 把 [x-k, x+k] 中的每个整数的出现次数都加一
diff.merge(x - k, 1, Integer::sum); // diff[x-k]++
diff.merge(x + k + 1, -1, Integer::sum); // diff[x+k+1]--
}
int ans = 0;
int sumD = 0;
for (Map.Entry<Integer, Integer> e : diff.entrySet()) {
sumD += e.getValue();
ans = Math.max(ans, Math.min(sumD, cnt.getOrDefault(e.getKey(), 0) + numOperations));
}
return ans;
}
}
###cpp
class Solution {
public:
int maxFrequency(vector<int>& nums, int k, int numOperations) {
unordered_map<int, int> cnt;
map<int, int> diff;
for (int x : nums) {
cnt[x]++;
diff[x]; // 把 x 插入 diff,以保证下面能遍历到 x
diff[x - k]++; // 把 [x-k, x+k] 中的每个整数的出现次数都加一
diff[x + k + 1]--;
}
int ans = 0, sum_d = 0;
for (auto& [x, d] : diff) {
sum_d += d;
ans = max(ans, min(sum_d, cnt[x] + numOperations));
}
return ans;
}
};
###go
func maxFrequency(nums []int, k, numOperations int) (ans int) {
cnt := map[int]int{}
diff := map[int]int{}
for _, x := range nums {
cnt[x]++
diff[x] += 0 // 把 x 插入 diff,以保证下面能遍历到 x
diff[x-k]++ // 把 [x-k, x+k] 中的每个整数的出现次数都加一
diff[x+k+1]--
}
sumD := 0
for _, x := range slices.Sorted(maps.Keys(diff)) {
sumD += diff[x]
ans = max(ans, min(sumD, cnt[x]+numOperations))
}
return
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
- 空间复杂度:$\mathcal{O}(n)$。
方法二:同向三指针 + 同向双指针
核心思路
- 计算有多少个数能变成 $x$,其中 $x = \textit{nums}[i]$。用同向三指针实现。
- 计算有多少个数能变成 $x$,其中 $x$ 不一定在 $\textit{nums}$ 中。用同向双指针实现。
同向三指针
把 $\textit{nums}$ 从小到大排序。
遍历 $\textit{nums}$。设 $x=\textit{nums}[i]$,计算元素值在 $[x-k,x+k]$ 中的元素个数,这些元素都可以变成 $x$。
遍历的同时,维护左指针 $\textit{left}$,它是最小的满足
$$
\textit{nums}[\textit{left}] \ge x - k
$$
的下标。
遍历的同时,维护右指针 $\textit{right}$,它是最小的满足
$$
\textit{nums}[\textit{right}] > x + k
$$
的下标。如果不存在,则 $\textit{right}=n$。
下标在左闭右开区间 $[\textit{left},\textit{right})$ 中的元素,都可以变成 $x$。这有
$$
\textit{sumD} = \textit{right} - \textit{left}
$$
个。
遍历的同时,求出 $x$ 有 $\textit{cnt}$ 个。然后用方法一的公式,更新答案的最大值。
同向双指针
同向三指针没有考虑「变成不在 $\textit{nums}$ 中的数」这种情况。
然而,不在 $\textit{nums}$ 中的数太多了!怎么减少计算量?
假设都变成 $y$,那么只有 $[y-k,y+k]$ 中的数能变成 $y$。
把 $\textit{nums}[i]$ 视作一维坐标轴上的点,想象一个窗口 $[y-k,y+k]$ 在不断地向右滑动,什么时候窗口内的元素个数才会变大?
- 如果我们从 $[y-k-1,y+k-1]$ 移动到 $[y-k,y+k]$,且 $y+k$ 不在 $\textit{nums}$ 中,此时窗口内的元素个数不会变大,甚至因为左端点收缩了,元素个数可能会变小。
- 所以,只有当 $y+k$ 恰好在 $\textit{nums}$ 中时,窗口内的元素个数才可能会变大。
- 结论:我们只需考虑 $y+k$ 在 $\textit{nums}$ 中时的 $y$!
于是,枚举 $x=\textit{nums}[\textit{right}]$,计算元素值在 $[x-2k,x]$ 中的元素个数,这些元素都可以变成同一个数 $y=x-k$。
左指针 $\textit{left}$ 是最小的满足
$$
\textit{nums}[\textit{left}] \ge x-2k
$$
的下标。
计算好 $\textit{left}$ 后,下标在 $[\textit{left}, \textit{right}]$ 中的数可以变成一样的,这有
$$
\textit{right} - \textit{left} + 1
$$
个。注意上式不能超过 $\textit{numOperations}$。
小优化
由于同向双指针算出的结果不超过 $\textit{numOperations}$,所以当同向三指针计算完毕后,如果发现答案已经 $\ge \textit{numOperations}$,那么无需计算同向双指针。
###py
class Solution:
def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
nums.sort()
n = len(nums)
ans = cnt = left = right = 0
for i, x in enumerate(nums):
cnt += 1
# 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n - 1 and x == nums[i + 1]:
continue
while nums[left] < x - k:
left += 1
while right < n and nums[right] <= x + k:
right += 1
ans = max(ans, min(right - left, cnt + numOperations))
cnt = 0
if ans >= numOperations:
return ans
left = 0
for right, x in enumerate(nums):
while nums[left] < x - k * 2:
left += 1
ans = max(ans, right - left + 1)
return min(ans, numOperations) # 最后和 numOperations 取最小值
###java
class Solution {
public int maxFrequency(int[] nums, int k, int numOperations) {
Arrays.sort(nums);
int n = nums.length;
int ans = 0;
int cnt = 0;
int left = 0;
int right = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
cnt++;
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if (i < n - 1 && x == nums[i + 1]) {
continue;
}
while (nums[left] < x - k) {
left++;
}
while (right < n && nums[right] <= x + k) {
right++;
}
ans = Math.max(ans, Math.min(right - left, cnt + numOperations));
cnt = 0;
}
if (ans >= numOperations) {
return ans;
}
left = 0;
for (right = 0; right < n; right++) {
int x = nums[right];
while (nums[left] < x - k * 2) {
left++;
}
ans = Math.max(ans, right - left + 1);
}
return Math.min(ans, numOperations); // 最后和 numOperations 取最小值
}
}
###cpp
class Solution {
public:
int maxFrequency(vector<int>& nums, int k, int numOperations) {
ranges::sort(nums);
int n = nums.size();
int ans = 0, cnt = 0, left = 0, right = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
cnt++;
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if (i < n - 1 && x == nums[i + 1]) {
continue;
}
while (nums[left] < x - k) {
left++;
}
while (right < n && nums[right] <= x + k) {
right++;
}
ans = max(ans, min(right - left, cnt + numOperations));
cnt = 0;
}
if (ans >= numOperations) {
return ans;
}
left = 0;
for (int right = 0; right < n; right++) {
int x = nums[right];
while (nums[left] < x - k * 2) {
left++;
}
ans = max(ans, right - left + 1);
}
return min(ans, numOperations); // 最后和 numOperations 取最小值
}
};
###go
func maxFrequency(nums []int, k, numOperations int) (ans int) {
slices.Sort(nums)
n := len(nums)
var cnt, left, right int
for i, x := range nums {
cnt++
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n-1 && x == nums[i+1] {
continue
}
for nums[left] < x-k {
left++
}
for right < n && nums[right] <= x+k {
right++
}
ans = max(ans, min(right-left, cnt+numOperations))
cnt = 0
}
if ans >= numOperations {
return ans
}
left = 0
for right, x := range nums {
for nums[left] < x-k*2 {
left++
}
ans = max(ans, right-left+1)
}
return min(ans, numOperations) // 最后和 numOperations 取最小值
}
也可以把两个 for 循环合起来。
###py
class Solution:
def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:
nums.sort()
n = len(nums)
ans = cnt = left = right = left2 = 0
for i, x in enumerate(nums):
while nums[left2] < x - k * 2:
left2 += 1
ans = max(ans, min(i - left2 + 1, numOperations))
cnt += 1
# 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n - 1 and x == nums[i + 1]:
continue
while nums[left] < x - k:
left += 1
while right < n and nums[right] <= x + k:
right += 1
ans = max(ans, min(right - left, cnt + numOperations))
cnt = 0
return ans
###java
class Solution {
public int maxFrequency(int[] nums, int k, int numOperations) {
Arrays.sort(nums);
int n = nums.length;
int ans = 0;
int cnt = 0;
int left = 0;
int right = 0;
int left2 = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
while (nums[left2] < x - k * 2) {
left2++;
}
ans = Math.max(ans, Math.min(i - left2 + 1, numOperations));
cnt++;
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if (i < n - 1 && x == nums[i + 1]) {
continue;
}
while (nums[left] < x - k) {
left++;
}
while (right < n && nums[right] <= x + k) {
right++;
}
ans = Math.max(ans, Math.min(right - left, cnt + numOperations));
cnt = 0;
}
return ans;
}
}
###cpp
class Solution {
public:
int maxFrequency(vector<int>& nums, int k, int numOperations) {
ranges::sort(nums);
int n = nums.size();
int ans = 0, cnt = 0, left = 0, right = 0, left2 = 0;
for (int i = 0; i < n; i++) {
int x = nums[i];
while (nums[left2] < x - k * 2) {
left2++;
}
ans = max(ans, min(i - left2 + 1, numOperations));
cnt++;
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if (i < n - 1 && x == nums[i + 1]) {
continue;
}
while (nums[left] < x - k) {
left++;
}
while (right < n && nums[right] <= x + k) {
right++;
}
ans = max(ans, min(right - left, cnt + numOperations));
cnt = 0;
}
return ans;
}
};
###go
func maxFrequency(nums []int, k, numOperations int) (ans int) {
slices.Sort(nums)
n := len(nums)
var cnt, left, right, left2 int
for i, x := range nums {
for nums[left2] < x-k*2 {
left2++
}
ans = max(ans, min(i-left2+1, numOperations))
cnt++
// 循环直到连续相同段的末尾,这样可以统计出 x 的出现次数
if i < n-1 && x == nums[i+1] {
continue
}
for nums[left] < x-k {
left++
}
for right < n && nums[right] <= x+k {
right++
}
ans = max(ans, min(right-left, cnt+numOperations))
cnt = 0
}
return
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。瓶颈在排序上。
- 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。
专题训练
- 下面数据结构题单的「§2.1 一维差分」。
- 下面双指针题单的「§3.2 同向双指针」和「五、三指针」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
- 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
离散化+差分 最简洁C++代码
枚举
解法:枚举
如果最后的众数是 $x$,那么答案就是 $c_x + \min(m, \sum\limits_{x - k \le i \le x + k, i \ne x} c_i)$,其中 $c_x$ 是 nums
里 $x$ 的数量,$m$ 是操作次数。
这个式子的值会随着 $x$ 的变化发生怎样的改变?$c_x$ 肯定在 $x$ 取到 nums
里有的数时才尽可能大,而 $\sum\limits_{x - k \le i \le x + k, i \ne x} c_i$ 的值和 $x$ 被几个数的“范围”覆盖到有关。一个数 $x$ 能覆盖的范围是 $[x - k, x + k]$,当 $x$ 增加到某个区间的左端点时,被覆盖到的数量会增加 $1$,之后在区间中间不发生变化。
所以我们只要考虑 $x$ 取 nums
中出现的数,以及它们 $-k$,共 $2n$ 种数。$\sum$ 的值可以用二分的方式快速计算。复杂度 $\mathcal{O}(n\log n)$。
参考代码(c++)
###cpp
class Solution {
public:
int maxFrequency(vector<int>& nums, int k, int numOperations) {
// 把所有要考虑的值放进 set 里
unordered_set<int> st;
// 统计 nums 里每种数出现了几次
unordered_map<int, int> mp;
for (int x : nums) {
st.insert(x); st.insert(x - k);
mp[x]++;
}
// 给 nums 排序,方便接下来二分计数。
sort(nums.begin(), nums.end());
int ans = 0;
for (int x : st) {
int tmp = 0;
// 二分计算 (x, x + k] 里有几个数
tmp += upper_bound(nums.begin(), nums.end(), x + k) - upper_bound(nums.begin(), nums.end(), x);
// 二分计算 [x - k, x) 里有几个数
tmp += lower_bound(nums.begin(), nums.end(), x) - lower_bound(nums.begin(), nums.end(), x - k);
tmp = min(tmp, numOperations);
ans = max(ans, mp[x] + tmp);
}
return ans;
}
};
在富文本编辑器中封装实用的 AI 写作助手功能
下载按钮点击一次却下载两个文件问题
Webapck系列-初识Webpack
当前端项目从编写几个简单的HTML
、CSS
、JS
文件,过渡到开发需要成千上百个模块、依赖各种第三方库、甚至需要处理less
、TypeScript
、图片
等资源的项目时,随之而来的问题和麻烦接踵而至:如何组织文件
、浏览器不识别的语法如何处理
以及如何高效加载文件
等等。
Webpack正是在这样的背景下诞生的。它不仅前端开发的基础工具,更是打通开发便捷性
与生产性能
的关键桥梁。无论你是前端的小白,还是想夯实工程化基础的进阶者,理解Webpack
的工作逻辑和使用方法,都让你在项目少走弯路,更好的应对其复杂场景。
接下来,让我们从Webpack是什么
开始,一步一步揭开其神秘面纱,从原理到基础实战,彻底搞懂这个前端工程化工具。
Webpack是什么
Webpack是一个静态模块打包工具。它视所有文件为模块
(JS、sass/less、图片、字体、TS),通过分析它们之间的依赖关系,最终打包成浏览器能直接识别的静态资源(JS或CSS等文件)。
可以将Webpack
理解成为工厂
:原料是项目中各种文件(js、css、less、ts等),按照依赖关系和特定规则处理它们,最后生成浏览器可以识别的产品
(bundle文件)。
Webpack底层原理
Webpack
的核心工作简单地说:从入口文件,递归解析所有依赖,处理后打包成输出文件。一步一步了解它的构建过程。
1. 确定规则
Webpack运行构建时,会默认读取项目根目录下webpack.config.js
文件或读取命令行--config
后的参数的配置文件。有如下核心配置:
- 入口(entry):定义从哪个文件开始解析依赖
- 出口(output):确定打包后文件名称和放入位置
-
解释器(loader):由于Webpack只能识别JS和JSON文件,webpack通过配置
loader
,将非JS文件转化为JS模块 - 插件(plugin):在Webpack的构建的生命周期中额外处理。如自动生成HTML,压缩代码等等。
2, 编译阶段
这是Webpack最为核心的阶段。分为构建依赖图
和模块转换
-
构建依赖图
Webpack会从
webpack.config.js
配置文件中的entry
定义的入口文件地址开始,一层层地解析文件。- 先读取入口文件的内容,分析出依赖了哪些文件。
- 递归解析被依赖的文件,直到所有关联的文件都被找到
- 最后形成一张Graph,记录了所有文件的路径、内容以及它们之间的依赖关系
举个例子:如果 index.js
依赖 a.js
,a.js
依赖 b.css
和 c.png
,依赖图就会清晰记录这层关系。
// 构建类似如下伪代码
{
"./src/index.js": () => {
// 加载a.js
},
"./src/a.js": () => {
// 加载b.css和c.png
},
"./src/b.css": () => { // 读取文件},
"./src/c.png": () => { // 读取图片}
}
- 模块转换
Wepack本身只能识别JS和JSON文件,如遇到其他文件就需要Loader
进行翻译
。
-
loader
的作用是转换
:将非JS文件转换为JS模块 - 处理顺序是
从右到左
或从下往上
{
test: /.css$/,
// 从右到左 依次执行
use: ['style-loader', 'css-loader'],
// use: [
// 'style-loader',
// 'css-loader'
//]
},
-
插件介入
在编译的各个环节,插件都可以介入进行处理。
-
插件是 “扩展器”:可以做 loader 做不到的事情,比如自动生成 HTML 文件(
HtmlWebpackPlugin
)、压缩代码(TerserPlugin
)、清除旧文件(CleanWebpackPlugin
)等。 -
插件基于 “事件钩子” 工作:Webpack 在打包过程中会触发很多事件(比如
beforeCompile
、afterCompile
),插件可以监听这些事件,在特定时机执行逻辑。
Webpack基础使用
在使用Webpack之前,确保项目安装Webpack
npm init -y // 初始化项目
npm i webpack webpack-cli -D // 开发依赖下安装webapck
指定入口
入口用于告诉Webpack
从哪个文件开始解析依赖。
module.exports = {
entry: "./src/index.js" // 单入口文件
/* 适合多页面应用
entry: {
index: "./src/index.js",
print: "./src/print.js"
}
*/
}
指定出口
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
// 多入口时: filename: '[name].bundle.js'
path: path.resolve(__dirname, "dist"),
clean: true, // 清理dist目录
},
}
加载资源
Webpack处理非JS文件需要在module.rules
配置,通过test
匹配文件类型,用use
指定对应的loader
1. 处理CSS文件
需要借助css-loader
转换CSS文件变为JS
模块;style-loader
将CSS插入到页面的style
标签
- 安装loader
npm i css-loader style-loader -D # 开发依赖
- 配置
module.exports = {
module: {
rules: [
{
test: /\.css$/,
// loader 执行顺序:从右到左(先 css-loader 再 style-loader)
use:["style-loader", 'css-loader']
}
]
}
}
如果使用sass
还需要sass-loader
和sass
npm i sass sass-loader -D
{
test: "/\.scss$/",
use: ["style-loader", "css-loader", "scss-loader"]
}
如果使用less
还需要less-loader
和less
npm i less less-loader -D
{
test: "/\.less$/",
use: ["style-loader", "css-loader", "less-loader"]
}
2. 处理图片/字体
Webpack5
内置通过Rule.type
处理字体和图片,无需额外配置loader。Rule.type
的值如下:
-
asset/resource
将资源作为单独文件输出,并返回URL -
asset/inline
将文件作为Data URL内联到bundle中 -
asset/source
将文件作为原始源码导入 -
asset
根据配置决定是inline
还是resource
module.exports = {
module: {
rules: [
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8kb
}
}
]
}
}
开发模式
开发时需要方便调试和自动刷新,以便提高开发效率。
1. 基础开发配置
module.exports = {
mode: 'development', // 定义为开发模式
devtool: 'inline-source-map' // 生成source-map 方便调试
}
2. 开发服务器
webpack-dev-server
提供热更新(修改代码后自动刷新页面),无需手动重新打包:
npm i webpack-dev-server -D
配置
module.exports = {
devServer: {
static: './dist', // 服务器根目录
port: 3000, // 服务器端口号
open: true, // 启动服务后自动打开浏览器
hot: true // 开启热模块替换 修改代码后自动刷新页面
}
}
在package.json
的scripts
字段中配置运行指令
{
"scripts": {
"dev": "webpack serve" // 启动开发服务器
}
}
🎯 本文小结
💡 核心要点回顾
Webpack 的本质:静态模块打包工具,将各种资源转化为浏览器可识别的静态文件
四大核心概念:
- 入口(Entry) :构建起点
- 出口(Output) :打包结果位置
- 加载器(Loader) :非JS文件转换器
- 插件(Plugin) :构建过程扩展器
构建两大阶段:
- 依赖图构建:从入口开始,递归分析模块依赖关系
- 模块转换:通过Loader将各类资源转为JS模块
开发效率提升:
-
webpack-dev-server
提供热更新 -
mode: 'development'
优化开发体验