news 2026/10/9 3:25:05

C#与ASP.NET Core实现大文件分片上传、秒传与断点续传实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#与ASP.NET Core实现大文件分片上传、秒传与断点续传实战指南

大附件上传一直是网页端的老大难,做过几年后台和全栈之后,我对这块的感受特别深。很多项目里,测试环境文件几MB、几十MB都很正常,一上生产,用户传个几百MB的视频、图纸、压缩包,要么直接超时断连,要么服务器内存被撑爆,要么就是传了半天失败了一次性从头再来,体验简直灾难。这个问题的标准解法就是分段上传加秒传,配合断点续传,基本能覆盖绝大多数大文件场景。这篇文章我直接用C#加ASP.NET Core写后端,前端用纯JavaScript配合分片逻辑,把实现思路、代码细节和踩过的坑一次讲清楚。

先说适用人群:正在做文件上传功能、被大文件折磨过的后端或全栈开发者,或者需要给现有系统加一个稳定的上传模块的同学。我会把原理部分也讲透,连哈希计算、分片大小怎么定这类比较细节的问题都会覆盖到,照着抄基本就能跑起来。

1. 项目整体设计与思路拆解

1.1 为什么必须做分段上传,而不是一把梭

很多入门项目里上传文件就是一次HTTP请求把整个文件塞进去,这种方案在小文件上问题不大,但文件一旦上了几百MB甚至几个GB,就会暴露三个致命问题。

首先是服务端内存压力。你如果用ASP.NET Core默认的模型绑定方式接收IFormFile,框架会把整个请求体缓冲到内存或者临时文件里,内存占用直接跟文件大小挂钩。假设服务器只有4GB内存,同时有10个人各传一个500MB的文件,基本就把内存打满了。分段上传的核心思路就是把这个大文件拆成若干个小块,每个块单独作为一个请求上传,服务端每次只处理一小块数据,内存占用从“文件大小”变成“分片大小”,这是一个数量级的差别。

其次是网络中断的恢复成本。一个500MB的文件传了80%,突然断网或者用户刷新了页面,如果是一次性上传,全部白传,得从头再来。分段上传天然支持断点续传,已经传完的分片不需要重新传,只需要继续传剩余的分片就行。这个体验差异对用户来说是非常直接的感受。

第三是并发与进度的控制。分段上传可以控制同时有几个分片在传,相当于给上传这个动作加了一个“流量阀门”。你可以限制并发数为3或者5,避免客户端把带宽全部占满,其他请求全部卡死。同时,每个分片独立的进度也给前端展示精确的上传百分比提供了基础。

1.2 秒传到底在“秒”什么

秒传这个名字容易让人误解,它并不是把上传速度优化到一秒完成,而是如果你上传的文件在服务器上已经存在,就直接跳过上传过程,返回一个“上传成功”的结果。在用户看来,这个文件好像在一瞬间就传完了,所以叫秒传。

这件事的本质是内容去重。比如一个团队里很多人会上传同一个安装包、同一份设计稿或者同一个视频素材,服务端用文件的哈希值作为唯一标识,在新文件上传前先算好哈希,去服务端查一下有没有相同的文件。如果有,就不需要再传了,直接把这个文件关联到当前用户的账号下,秒传成功。

所以秒传涉及两个核心工作:一个是在客户端计算出可靠的、能代表整个文件内容的哈希值;另一个是服务端要能根据哈希值快速检索是否已有相同文件,并且要把新产生的文件记录正确地映射到当前用户或当前业务模块下。这两块后面我都会展开讲。

1.3 整体架构与自定义流程设计

这个项目我设计的完整流程是这样的:

用户在前端选择一个文件之后,前端先计算这个文件的哈希值,然后带上文件名、文件大小、哈希值去请求服务端的接口。服务端查一下这个哈希值是否已经存在,如果存在,直接返回“秒传成功”,整个过程结束。如果不存在,服务端创建一个上传任务,返回一个任务ID,并同步告知前端建议的分片大小。前端拿到任务ID后,把文件按照分片大小切成若干块,每个分片携带任务ID和分片序号,逐个或者并发上传。服务端接收并保存每一个分片,等所有分片都到齐了,就把它们合并成一个完整的文件,再校验一下合并后文件的哈希值与之前客户端上报的是否一致,一致则整个上传流程闭环。

这个流程里,秒传、分段、合并和校验,四个环节是环环相扣的,缺一不可。前端负责切片和哈希计算,后端负责任务管理、分片存取和文件合并,整体是一个“客户端计算、服务端校验”的协作模式。

2. 核心细节解析与实操要点

2.1 文件唯一标识:哈希算法的选择与计算

前端计算文件哈希,我建议用MD5,虽然它在安全性上早就不被推荐用于密码学场景,但在文件去重这个场景里,MD5的计算速度和碰撞风险是够用的。如果项目有比较高的安全要求,也可以换SHA-256,但计算耗时明显增加,大文件尤其明显。

需要特别注意的是,对几百MB的文件做一次全量哈希计算,整个过程可能需要几百毫秒到几秒不等,这取决于用户机器的性能和文件大小。如果直接在浏览器主线程里算,用户界面会有明显的卡顿感。我的做法是用Web Worker在后台线程里计算哈希,主线程照常渲染界面,计算完成后通过回调通知主线程。这样既不阻塞UI,也能及时拿到哈希值。

另外一个实用技巧是抽样哈希。严格来说,对大文件可以只取文件开头若干个字节、中间若干个字节和结尾若干个字节,拼成一个小样本片段后计算哈希。这样计算速度极快,去重效果在绝大多数业务场景下也够用。不过要注意,抽样哈希存在一定的碰撞概率,所以秒传成功后,务必要在服务端做一次全量比对,也就是把已存在文件的哈希和当前文件的哈希都算一遍进行确认,避免用户传了一个内容不同的文件却被误判为相同文件。如果对数据完整性有硬性要求,建议还是全量哈希,用时间换准确性。

2.2 分片大小与并发控制怎么定

分片大小的选择没有绝对标准,但有几个可参考的依据。分片太小,比如几百KB,HTTP请求的数量会急剧增加,每一个请求都有握手、传输、应答的开销,整体效率反而下降,还容易触发服务器的连接数限制。分片太大,比如50MB,就又回到了接近一次性上传的老问题,服务端单次内存占用高,失败重传的粒度太粗。

我常用的范围是2MB到10MB,默认选5MB。以500MB的文件为例,切成100片,每片5MB,这个体量在请求数和单片内存占用之间是比较平衡的。如果你所在的项目网络环境很好,内网传输,可以适当上调到10MB;如果是弱网环境,比如移动网络,可以降到2MB,失败后重传的成本更低。

并发数的控制也很有讲究。前端一次性把所有分片全部并发上传,看起来速度最快,但其实会造成两个问题:一是客户端带宽被全部占用,页面加载其他资源会变得很慢;二是服务端的并发压力会瞬间飙高,如果其他接口也在同一个服务上,会导致整个应用响应变慢。我的习惯是把并发数控制在3到5之间,既能充分利用带宽,又不至于把服务打崩。当然这个数值要根据服务器的带宽和处理能力来动态调整,如果服务器配置很好,适当调高到8到10也可以。

2.3 前后端接口协议设计

接口协议是整个功能的核心骨架,设计清晰了,前后端开发可以并行推进。我通常会定义这几个接口:

第一个是文件预检接口,前端把文件哈希、文件大小、文件扩展名发给服务端,服务端返回一个结果,可能包含“文件已存在,秒传成功”的标记,也可能包含“可以上传,任务ID为xxx,建议分片大小为xxx”。这个接口同时承担了秒传查询和上传任务创建两个职责,减少了请求次数。

第二个是分片上传接口,前端把任务ID、分片序号、分片数据一起POST到服务端。服务端校验任务ID是否有效,然后把分片数据保存到临时目录下,命名规则一般就是“任务ID_分片序号.tmp”。这个接口必须支持幂等,也就是说同一个分片如果重复发送,服务端不能报错,直接返回成功就行。这个细节很重要,前端在请求超时后会自动重试,如果服务端不处理幂等,就会导致分片数据错乱。

第三个是合并接口,前端在所有分片上传完成后,调用这个接口通知服务端合并。服务端按分片序号把所有临时分片文件拼接成一个完整文件,再对这个完整文件计算哈希,与前端上报的哈希做比对。

另外,我还会加一个查询分片状态接口。这个接口的用途是断点续传:当用户因为网络波动中断上传后,重新打开页面选择同一个文件,前端可以带着任务ID去服务端查询哪些分片已经上传成功,然后只上传缺失的分片。这个接口虽然简单,但能显著提升弱网环境下的体验。我见过不少项目跳过这个接口,导致用户中断后只能全部重传,很可惜。

3. 实操过程与核心环节实现

3.1 前端切片与上传逻辑

前端实现切片的核心API是File.slice(),这个API在主流浏览器里早就成为标准了,兼容性很好。逻辑不复杂,先根据分片大小算出总片数,然后循环截取每个分片,逐个上传。

先看计算文件哈希的部分,我用的是SparkMD5这个库,它专门为浏览器环境做了优化,能增量计算哈希,配合Web Worker使用效果最好。核心逻辑是在Web Worker里读取文件的分片,逐个喂给SparkMD5,最终得到全文件的MD5值。

下面是Web Worker内部的代码示例:

// hash-worker.js importScripts('spark-md5.min.js'); self.onmessage = function(e) { const file = e.data.file; const chunkSize = e.data.chunkSize; let currentChunk = 0; const chunks = Math.ceil(file.size / chunkSize); const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onload = function(event) { spark.append(event.target.result); currentChunk++; self.postMessage({ progress: currentChunk / chunks }); if (currentChunk < chunks) { loadNext(); } else { self.postMessage({ hash: spark.end() }); } }; function loadNext() { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); fileReader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); };

主线程这边的逻辑是:

async function handleFile(file) { const fileSize = file.size; const fileName = file.name; const worker = new Worker('hash-worker.js'); const hash = await new Promise((resolve, reject) => { worker.onmessage = function(e) { if (e.data.hash) resolve(e.data.hash); }; worker.onerror = reject; worker.postMessage({ file: file, chunkSize: 2 * 1024 * 1024 }); }); // 预检:查秒传状态或创建上传任务 const precheckResp = await fetch('/api/upload/precheck', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName, fileSize, hash }) }); const precheck = await precheckResp.json(); if (precheck.uploaded) { alert('秒传成功'); return; } const taskId = precheck.taskId; const chunkSize = precheck.chunkSize; const totalChunks = Math.ceil(fileSize / chunkSize); // 并发上传分片 const concurrency = 3; let currentChunk = 0; async function uploadNext() { if (currentChunk >= totalChunks) return; const index = currentChunk++; const start = index * chunkSize; const end = Math.min(start + chunkSize, fileSize); const blob = file.slice(start, end); const formData = new FormData(); formData.append('taskId', taskId); formData.append('index', index); formData.append('chunk', blob); await fetch('/api/upload/chunk', { method: 'POST', body: formData }); await uploadNext(); } const workers = Array.from({ length: concurrency }, () => uploadNext()); await Promise.all(workers); const mergeResp = await fetch('/api/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId }) }); const result = await mergeResp.json(); console.log(result); }

这段代码里的uploadNext是一个递归函数,每个并发任务处理完当前分片后自动取下一个,直到所有分片传完。注意我这里用的是FormData来包装分片数据,这是最常规也最稳妥的方式,服务端直接用IFormFile就能接收。

3.2 后端C#接口实现:创建任务、接收分片、合并文件

后端我用ASP.NET Core 6及以上版本,用Controller的方式实现,代码清晰,也方便大家对号入座。

先定义一个上传任务的模型:

public class UploadTask { public string TaskId { get; set; } public string FileName { get; set; } public long FileSize { get; set; } public string Hash { get; set; } public int ChunkSize { get; set; } public int TotalChunks { get; set; } public HashSet<int> UploadedChunks { get; set; } = new HashSet<int>(); public DateTime CreateTime { get; set; } }

在实际项目中,这个任务对象应该存到Redis或者数据库里,我这个示例先用内存字典存储,方便演示。需要注意一点,临时任务要被清理,否则内存会一直涨,我在后文会谈到。

然后是预检接口的实现:

[HttpPost("api/upload/precheck")] public async Task<IActionResult> Precheck([FromBody] PrecheckRequest request) { // 先查文件表,看有没有相同哈希的文件 var existingFile = await _fileRepository.FindByHashAsync(request.Hash); if (existingFile != null && existingFile.Size == request.FileSize) { return Ok(new { uploaded = true, message = "秒传成功" }); } var task = new UploadTask { TaskId = Guid.NewGuid().ToString("N"), FileName = request.FileName, FileSize = request.FileSize, Hash = request.Hash, ChunkSize = 5 * 1024 * 1024, TotalChunks = (int)Math.Ceiling((double)request.FileSize / (5 * 1024 * 1024)), CreateTime = DateTime.Now }; _uploads.TryAdd(task.TaskId, task); return Ok(new { uploaded = false, taskId = task.TaskId, chunkSize = task.ChunkSize, totalChunks = task.TotalChunks }); }

秒传成功的关键在这一步,服务端必须确认“哈希相同且文件大小相同”,只靠哈希或者只靠大小都是不严谨的,两个条件同时满足才能判定为同一文件。

分片上传接口是这个项目的重头戏:

[HttpPost("api/upload/chunk")] public async Task<IActionResult> UploadChunk([FromForm] ChunkUploadRequest request) { if (!_uploads.TryGetValue(request.TaskId, out var task)) { return NotFound("任务不存在"); } var chunkFile = request.Chunk; if (chunkFile == null || chunkFile.Length == 0) { return BadRequest("分片内容为空"); } string tempDir = Path.Combine(_env.WebRootPath, "upload_temp", request.TaskId); Directory.CreateDirectory(tempDir); string chunkPath = Path.Combine(tempDir, $"{request.Index}.tmp"); // 如果分片已经存在,说明是重复上传,直接返回成功(幂等) if (System.IO.File.Exists(chunkPath)) { return Ok(new { received = true }); } using (var stream = new FileStream(chunkPath, FileMode.Create)) { await chunkFile.CopyToAsync(stream); } task.UploadedChunks.Add(request.Index); return Ok(new { received = true }); }

这里有个细节值得强调:判断分片是否存在的逻辑必须放在接收分片之前。如果先接收再判断,重复上传时就会把已经存在的分片文件覆盖掉,等于前面的工作白做了。幂等处理是断点续传的基础,不做好后面全是坑。

合并接口负责把分片拼成完整文件:

[HttpPost("api/upload/merge")] public async Task<IActionResult> MergeChunks([FromBody] MergeRequest request) { if (!_uploads.TryGetValue(request.TaskId, out var task)) { return NotFound("任务不存在"); } if (task.UploadedChunks.Count != task.TotalChunks) { return BadRequest($"分片未齐全,已上传 {task.UploadedChunks.Count} / {task.TotalChunks}"); } string tempDir = Path.Combine(_env.WebRootPath, "upload_temp", request.TaskId); string finalPath = Path.Combine(_env.WebRootPath, "uploads", task.FileName); using (var finalStream = new FileStream(finalPath, FileMode.Create)) { for (int i = 0; i < task.TotalChunks; i++) { string chunkPath = Path.Combine(tempDir, $"{i}.tmp"); using (var chunkStream = new FileStream(chunkPath, FileMode.Open)) { await chunkStream.CopyToAsync(finalStream); } } } // 校验合并后文件的哈希 string actualHash = await CalculateFileHashAsync(finalPath); if (actualHash != task.Hash) { System.IO.File.Delete(finalPath); return BadRequest("文件校验失败,请重新上传"); } // 清理临时分片目录 Directory.Delete(tempDir, true); _uploads.TryRemove(request.TaskId, out _); return Ok(new { success = true, filePath = finalPath }); }

合并时会遇到分片不齐的情况,我的处理是直接返回错误,由前端提示用户继续补齐。有些方案会做成自动等待分片传完,但那样接口的复杂度会明显增加,也容易遇到前端长时间未上传的僵尸任务。所以我选择了简单直接的方式,归并控制权交给调用方。

哈希计算的工具方法:

private async Task<string> CalculateFileHashAsync(string filePath) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { using (var md5 = System.Security.Cryptography.MD5.Create()) { byte[] hashBytes = await md5.ComputeHashAsync(stream); return Convert.ToHexString(hashBytes); } } }

3.3 秒传的核心逻辑实现

秒传在代码层面的实现其实非常轻量,核心就藏在上面的预检接口里。当前端把文件哈希和大小发给服务端的那一刻,服务端的查询结果决定了整个上传任务的走向。

如果服务端文件表里有哈希相同且大小相同的记录,就说明内容相同的文件已经存在,服务端只需要把这个文件的引用关系记录到当前用户的文件列表里,不需要真的存储一份新文件。这样做的好处不只是快,还能大量节省存储空间。在网盘类系统里,同一个热门文件被十万人上传,物理存储只有一份,这就是秒传背后的真正价值。

文件等级我建议这样设计:

public class FileRecord { public int Id { get; set; } public string FileName { get; set; } public long Size { get; set; } public string Hash { get; set; } public string StoragePath { get; set; } public string OwnerUserId { get; set; } }

这里的OwnerUserId代表文件属于哪个用户,而Hash和StoragePath代表物理存储位置。秒传时创建FileRecord,StoragePath引用已存在文件的物理路径,不做复制,这就是“秒传”能在毫秒级完成的根本原因。

3.4 完整流程跑通演示

假设前端选择了一个500MB的文件,命名为demo_video.mp4,前端用Web Worker在后台算出了MD5值是7f7a...c9b0。前端先请求预检接口,带上文件名、大小和哈希。服务端查询文件表没有这个哈希的记录,于是创建一个上传任务,返回taskId是3f2a...,建议分片大小为5MB。前端算出一共100片,用3个并发任务开始上传。每传完一片,服务端的task.UploadedChunks集合就会加上对应的序号。等100片全部传完,前端调用合并接口,服务端把100个临时文件按顺序拼接成一个本地文件,再计算这个文件的MD5,与前端上报的7f7a...c9b0比对,一致则返回成功。整个流程从上传到合并完成,用户侧看到的是一个几百MB的文件在几十秒内传完,体验比一次性上传稳定得多。

如果这时候另一个用户也上传同一个demo_video.mp4,前端计算出同样的哈希,预检接口在文件表里命中了记录,直接返回uploaded: true,用户看到的结果就是文件立刻上传完成,这就是秒传。

4. 常见问题与排查技巧实录

4.1 分片乱序与丢片的处理

分片并发上传时,各个分片到达服务端的顺序是不确定的,这是正常的,因为文件本身并不会被前端读取的时候破坏,各分片之间是相互独立的,合并时靠序号排列就行。真正需要注意的是丢片。

丢片的最常见原因是网络超时导致请求失败。前端在fetch超时后如果直接抛出错误,整个上传流程就会终止,用户看到的是“上传失败”。我的处理方式是为每个分片的上传请求加一个重试机制,比如最多重试三次,每次间隔2秒。如果重试三次仍然失败,则标记这个分片为失败状态并且中止任务,提示用户检查网络后重新上传。这种情况下,已经传完的分片不会浪费,重新上传时通过查询分片状态接口只补充缺失的部分即可。

另外,服务端的IIS或Kestrel默认都有请求大小限制,ASP.NET Core的默认请求体大小限制是30MB,如果你的分片设置得比较大,比如超过30MB,服务端会直接拒绝请求,返回413状态码。这个问题踩的人特别多。解决办法是在Program.cs里配置:

builder.Services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10MB });

同时也可以在IIS或Nginx层面调整客户端请求体大小限制。总之分片大小设定后,一定要排查一遍链路中所有可能限制请求体大小的位置,否则会莫名其妙地收到413错误。

4.2 大内存与大并发下的表现

这个项目里最容易出的性能问题有两个:一是内存字典_uploads无限增长,二是合并文件时一次性把整个文件读进内存。

针对第一个问题,我在预检接口创建任务时会设置一个过期时间,所有任务在创建后超过24小时会自动清理。实现方式可以是用一个后台定时任务扫描_uploads,也可以用Redis的过期key机制,生产环境建议用后者,避免状态全部存在单机内存里,应用重启后任务全部丢失。

针对第二个问题,合并时的正确做法是用Stream.CopyTo流式写入,千万不能用File.ReadAllBytes把分片文件全部读进内存再写出去。500MB的文件,ReadAllBytes会瞬间占掉500MB内存,并发合并几个文件,服务器就直接告警了。我在上面的代码示例中用的就是流式写入,每次读取分片文件并向目标流写入,内存占用就只有当前分片的大小。

4.3 秒传误判与文件损坏问题

秒传误判是我见过最严重的数据问题。前端如果用了抽样哈希,而服务端只根据抽样哈希判断文件相同,就有一定概率把内容不同的文件当成相同的文件,用户上传的文件内容会直接错乱。所以我在预检接口里不仅要求哈希相同,还要求文件大小相同,并且在合并完成后做一次全量哈希校验。这道校验是数据完整性的最后一道防线,绝对省不得。

还有一个隐藏问题是前端计算哈希用到的文件读取方式。如果用户在上传过程中修改了文件,比如视频还在编辑,前端在算哈希时读取的是旧内容,但分片上传时文件已经被修改,上传的分片内容和哈希对不上,合并后的校验就会失败。解决方法是前端在读取文件、计算哈希、上传分片期间,使用同一个已选中的File对象引用,并且提示用户上传过程中不要修改文件。如果一定要支持上传过程中编辑,只好把文件内容先复制一份到内存或IndexedDB里,但那样内存占用又会变大,索引DB的写入速度也未必理想,所以一般的实现都要求用户别改文件。

4.4 排查清单速查表

我整理一份速查表,碰到问题可以直接对着查。

现象可能原因解决办法
上传请求返回413服务端请求体大小限制过小调整FormOptions.MultipartBodyLengthLimit,检查IIS/Nginx限制
分片上传超时分片大小过大或网络差调低分片大小,增加重试机制,调低并发数
合并后文件打不开分片丢失导致文件数据缺漏查询分片状态,补齐缺失分片后重新合并
秒传后文件内容不对抽样哈希碰撞或只按哈希判断必须同时校验大小,合并后做全量哈希校验
服务器内存飙升合并时ReadAllBytes或任务字典未清理改用流式合并,任务加过期策略
秒传接口很慢文件表没索引,哈希查询全表扫描给Hash字段加唯一索引
重传任务无法续传服务端任务状态没有持久化生产环境用Redis存任务状态,重启不丢

我在实际跑这个方案的过程中,最明显的一个感触就是分段上传的代码并不复杂,难的是把边界情况全部照顾到。幂等重试、哈希校验、分片状态查询、临时任务过期清理,这些细节看着不起眼,但任何一个漏掉,线上跑一段时间就会出问题,而且问题往往很难排查。做这类功能一定要把“数据不出错”放在“功能页面好看”前面,宁可上传慢一点、逻辑复杂一点,也不要在文件完整性上妥协。把这些基础打牢之后,后续还能在进度条、拖拽上传、目录上传、文件预览这些方向上继续扩展,大文件上传这关过去了,后面的路会顺很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 3:25:00

企业AI Agent隐私保护实战:从威胁模型到RAG全链路防护

我先把企业AI Agent在真实落地过程中的隐私问题摊开来看。很多团队上Agent项目&#xff0c;第一反应是冲模型能力、冲RAG效果&#xff0c;结果安全评估一到&#xff0c;发现数据流经的每个环节都有泄露点。这不是危言耸听&#xff0c;企业场景和个人玩ChatGPT完全是两码事——个…

作者头像 李华
网站建设 2026/10/9 3:24:24

题解:洛谷 AT_abc441_b [ABC441B] Two Languages

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/9 3:22:41

前缀和与数组预处理:两道经典题吃透中心下标和除自身乘积

刷算法题的时候&#xff0c;很多人一看“前缀和”三个字就有点怵&#xff0c;觉得这是不是又是什么高深技巧。其实它内核就一句话&#xff1a;把数组从头到尾的累计信息先算好存起来&#xff0c;之后任何一个位置的查询都变成O(1)的查表。今天要拆的这两道题&#xff0c;算是把…

作者头像 李华
网站建设 2026/10/9 3:22:31

Windows局域网屏幕多播实战:从GDI抓屏到IGMPv2组播部署

简介&#xff1a;本资源是一个基于C#开发的局域网屏幕多播与广播工具源码包&#xff0c;面向网络编程初学者、Windows桌面应用开发者及远程协作类软件学习者&#xff0c;解决局域网内高效同步共享屏幕内容的技术实现问题&#xff0c;适用于远程教学、团队演示、内部培训等场景。…

作者头像 李华