普通视图

发现新文章,点击刷新页面。
今天 — 2025年10月22日技术

想偷卷?但微信不支持md文档?这个软件助你!

作者 前端AC
2025年10月22日 00:20

📝 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 ⭐ 支持!

错误处理:异常好于状态码

作者 阮一峰
2025年10月22日 00:28

错误处理有不同的方式。

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🔴

2025年10月22日 00:00

给你一个整数数组 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)

作者 endlesscheng
2024年11月10日 09:14

方法一:差分

前置知识差分原理讲解

设 $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)$。

方法二:同向三指针 + 同向双指针

核心思路

  1. 计算有多少个数能变成 $x$,其中 $x = \textit{nums}[i]$。用同向三指针实现。
  2. 计算有多少个数能变成 $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)$。忽略排序的栈开销。

专题训练

  1. 下面数据结构题单的「§2.1 一维差分」。
  2. 下面双指针题单的「§3.2 同向双指针」和「五、三指针」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

枚举

作者 tsreaper
2024年11月10日 00:09

解法:枚举

如果最后的众数是 $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;
    }
};
昨天 — 2025年10月21日技术

Webapck系列-初识Webpack

作者 云枫晖
2025年10月21日 18:46

当前端项目从编写几个简单的HTMLCSSJS文件,过渡到开发需要成千上百个模块、依赖各种第三方库、甚至需要处理lessTypeScript图片等资源的项目时,随之而来的问题和麻烦接踵而至:如何组织文件浏览器不识别的语法如何处理以及如何高效加载文件等等。

Webpack正是在这样的背景下诞生的。它不仅前端开发的基础工具,更是打通开发便捷性生产性能的关键桥梁。无论你是前端的小白,还是想夯实工程化基础的进阶者,理解Webpack的工作逻辑和使用方法,都让你在项目少走弯路,更好的应对其复杂场景。

接下来,让我们从Webpack是什么开始,一步一步揭开其神秘面纱,从原理到基础实战,彻底搞懂这个前端工程化工具。

Webpack是什么

Webpack是一个静态模块打包工具。它视所有文件为模块(JS、sass/less、图片、字体、TS),通过分析它们之间的依赖关系,最终打包成浏览器能直接识别的静态资源(JS或CSS等文件)。

a620f1c107ee4e5b869aca1bcc49ac86~tplv-k3u1fbpfcp-zoom-in-crop-mark_3024_0_0_0.png

可以将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.jsa.js 依赖 b.css 和 c.png,依赖图就会清晰记录这层关系。

image.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 在打包过程中会触发很多事件(比如 beforeCompileafterCompile),插件可以监听这些事件,在特定时机执行逻辑。

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标签

  1. 安装loader
npm i css-loader style-loader -D # 开发依赖
  1. 配置
module.exports = {
  module: {
   rules: [
     {
       test: /\.css$/,
       // loader 执行顺序:从右到左(先 css-loader 再 style-loader)
       use:["style-loader", 'css-loader']
     }
   ]
  }
}

如果使用sass还需要sass-loadersass

npm i sass sass-loader -D
{
  test: "/\.scss$/",
  use: ["style-loader", "css-loader", "scss-loader"]
}

如果使用less还需要less-loaderless

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.jsonscripts字段中配置运行指令

{
  "scripts": {
    "dev": "webpack serve" // 启动开发服务器
  }
}

🎯 本文小结

💡 核心要点回顾

Webpack 的本质:静态模块打包工具,将各种资源转化为浏览器可识别的静态文件

四大核心概念

  • 入口(Entry) :构建起点
  • 出口(Output) :打包结果位置
  • 加载器(Loader) :非JS文件转换器
  • 插件(Plugin) :构建过程扩展器

构建两大阶段

  1. 依赖图构建:从入口开始,递归分析模块依赖关系
  2. 模块转换:通过Loader将各类资源转为JS模块

开发效率提升

  • webpack-dev-server提供热更新
  • mode: 'development'优化开发体验

基于 SpreadJS 的百万级数据在线数据透视表解决方案:技术解析与实践

2025年10月21日 17:30
基于 SpreadJS 的百万级数据在线数据透视表解决方案:技术解析与实践 在企业数字化进程中,百万级数据的多维分析已成为业务决策的核心需求,而数据透视表作为高效的数据分析工具,其在线化应用常受限于性
❌
❌