news 2026/10/22 5:42:22

C#刷LeetCode的高效闭环:每日复习、语法优化与调试模板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#刷LeetCode的高效闭环:每日复习、语法优化与调试模板

连续坚持了几个月每日打卡,某天回头复盘时突然意识到一个问题:LeetCode每日刷题这件事我一天没断,可真要我临时做一道上周做过的同类题,竟然只能想起一个模糊的框架,细节全丢。如果用的是C#,这种感觉会更明显——讨论区里C++、Java的题解铺天盖地,C#的版本得翻好几页才找到一个能用的,拿过来跑通了,却说不清楚它到底优在哪、坑在哪。后来我停掉了“今天又刷了一题”这种自我安慰,把每日刷题改造成了一套完整的训练流程:每天只花40分钟左右,重点是“新题+复习+总结”三段闭环,坚持一个多月后,旧题的复现率明显上来了。这篇文章就把我完整的玩法、C#刷题时的语法取舍、以及我用着很顺的调试模板都摊开聊一遍。

1. 每天打卡式刷题,一百天后脑子还是空的:先想清楚“每日刷题”到底在刷什么

1.1 把“刷了多少题”当KPI,是最容易掉进去的陷阱

如果你也习惯在日历App里把每天的任务画成绿格子,那下面这个场景你大概率不陌生:状态好的时候点开一道Medium,四十分钟做完,状态差的时候看两分钟没有思路,直接打开题解抄一遍,然后告诉自己“今天也刷过了”。每周日看一眼统计面板,绿色连续天数拉满,内心毫无波澜,甚至有点空虚。

这种模式的核心问题在于,我们把“每日刷题”这个动作本身当成了KPI,而完全没有关注这个动作带来的能力增量。做算法题其实跟练乐器很像:你每天按一小时琴键,如果只是把谱子从头到尾弹一遍,不针对难点单独拆解,练一年也很难有明显的进步。刷题也是一样,真正的成长发生在那段“思考为什么”的时间里,而不是发生在“通过了所有测试用例”的瞬间。

我在意识到这一点之后,给自己定了一条硬规矩:每天的新题时间只占整个流程的不到一半,剩下一半以上都用来做复习和结构化的总结。哪怕这一天状态差到完全写不出题,我也会把前一天做过的题重新默写一遍关键代码,而不是打开评论区复制一段交差。这样做的直接收益是,第二天再遇到类似题型时,我不是“好像见过”的模糊感,而是能清晰地写出第一步、第二步该做什么。

1.2 题解看得多、复习得少,等于反复回到起点

C#选手刷题还有一个额外的劣势:高质量的中文题解绝大多数是C++或Java写的。所以很多C#玩家养成了一个习惯——看到题解就存,GitHub仓库里收集了上百个链接,收藏夹吃灰。然后某天偶尔翻出来,发现里面的代码自己根本看不懂,或者看懂了也忘了当时为什么收藏它。

我自己也经历过这个阶段,而且踩过一个很具体的坑:遇到一道字符串处理题,看了三篇题解,每篇都用不同的解法,我兴奋地都存了下来,觉得自己“学到了三种解法”。结果一周后遇到同类型的变体题,我一种都写不出来。原因很简单:看题解时我是在“被动接收信息”,而没有任何主动回忆的动作。接收信息的时候人会产生一种错觉,觉得“我会了”,但大脑里根本没有形成可调用的路径。

后来我改了策略:看题解只允许自己看两种。一种是明显更优的复杂度解,比如从O(n^2)优化到O(n);另一种是能处理我没想到的边界情况的写法。看完之后必须做两件事:第一,把题解用自己的C#代码重写一遍并跑通;第二,在笔记里写一句话,说明这个解法的核心思路和适用边界。如果做不到这两步,这个题解对我而言等于不存在。这话听起来很绝对,但对治“收藏即学会”的心理非常有效。

1.3 刷题计划里必须出现的“复习缺口”模型

聊完前面的误区,接下来给一个我一直在用的规划模型,叫“复习缺口”。一句话解释:把已经掌握的题型当成存量资产,每天的新题是增量;如果你只盯增量,你的真实水平永远由最近三五天的存量决定,想有积累几乎不可能。

以每天40分钟为例,我的时间分配长这样:

  • 前10分钟:不查资料,默写前一天题目的核心思路,最好连代码骨架也要写出来
  • 中间15分钟:做今日新题,只看题目,不先看题解
  • 后15分钟:新题对照题解,并且完成一句话总结;如果新题没做出来,这15分钟就用来复盘卡点在哪
  • 每周额外有一天不刷新题,专门重新做本周错题或“需要复习”标记的题

这个模型的核心不是“每日刷题”,而是“每日复习”。因为遗忘曲线在前24小时下降最快,第二天如果不碰一下昨天的内容,到了第三天基本就清零了。反过来,一旦你在第二天主动回忆了一次,三天后、七天后还能轻松调出来的概率会大幅上升。我坚持了一个月左右就发现,那些被标记为“复习”的旧题,越来越不需要花时间,这其实就是长期记忆形成的信号。

2. 用C#写算法题,字典、排序、字符串这三块要先理顺

2.1 字典三板斧:TryGetValue、GetValueOrDefault、GetValueRefOrAddDefault

很多C#新手刷题时会写出这种代码:

if (dict.ContainsKey(key)) { dict[key]++; } else { dict[key] = 1; }

这个写法的问题在于,ContainsKey和索引器赋值各做了一次哈希查找,相当于同一把钥匙开了两次锁。在数据量到十万级的时候性能差异不明显,但LeetCode的测试用例普遍会把它拉到百万级,这时候同一套逻辑,用TryGetValue就要明显更稳。

更推荐的标准写法是这样:

var dict = new Dictionary<string, int>(); foreach (string item in items) { if (dict.TryGetValue(item, out int count)) { dict[item] = count + 1; } else { dict[item] = 1; } }

如果刷题环境的.NET版本到了.NET 6以上,还可以用更狠的写法,直接拿引用改值:

ref int value = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out bool exists); value++;

这种写法只做一次查找就能完成“不存在则初始化、存在则自增”,在读多写多的场景下收益最明显。不过我必须提醒一句:这是刷题向的优化写法,它依赖内部实现细节,普通业务代码里不要这样用,可读性太差。你只需要知道有这个东西,万一遇到卡时间的题目,它能帮你提一点速。

2.2 排序不是无脑Array.Sort,稳定性的坑在这里

C#里最常见的排序API就是Array.Sort和List.Sort,但这两个方法默认都是不稳定的。什么概念呢?如果两个元素在排序规则下“相等”,稳定排序会保持它们原来的先后顺序,不稳定排序不保证这一点。

举个算法题里会踩坑的例子:有一个数组,存的是(val, index)结构,你要按val升序排,同时希望index小的排在前面。很多人会直接把数组丢给Array.Sort,配一个只比较val的比较器,然后发现结果里相同val的元素顺序变了。有些题目不要求稳定性,怎么排都行;但有些依赖原顺序的题,这就直接WA。

我现在的习惯是:只要排序结果还需要依赖原始顺序,就不赌Array.Sort的运气,直接转成LINQ的OrderBy。OrderBy是稳定排序,语义上也更明确:

var sorted = pairs .OrderBy(p => p.Value) .ThenBy(p => p.Index) .ToList();

虽然OrderBy带来的额外开销比Array.Sort大一点,但在刷题场景下,一次O(n log n)的排序开销完全可以接受。为了性能去用一个隐含不确定性语义的API,然后把错误定位的锅甩给自己,完全不划算。

2.3 字符串拼接别用+=,StringBuilder也不是万能

算法题里的字符串处理场景特别多,而C#的string是不可变类型,每次+=操作都会生成一个新的字符串,如果在一个循环里反复用+=拼接,会产生大量中间对象,直接拖慢执行速度,严重时就是TLE。

基础的替代方案是StringBuilder:

var sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.Append(...); }

但我想多提一层:有些字符串题目的本质其实是对字符数组的原地操作,用StringBuilder反而绕了。比如字符串反转、去掉指定字符、逐字符处理再比较这类题,直接声明char[]数组,操作完再new string(arr),往往比StringBuilder更利落,也更接近算法题想要的“操作数据”思维,而不是“操作字符串API”的思维。

char[] arr = s.ToCharArray(); // 交换、赋值等操作 string result = new string(arr);

一句话总结:拼接量小用+=无所谓,拼接量大用StringBuilder,涉及频繁修改单个字符时先考虑char[]。

3. 三类高频题型的C#成套写法:哈希计数、前缀和、双指针

3.1 哈希计数:从频次统计到K个高频词

哈希计数是出现频率最高的套路,一类典型的题是:给一个字符串,统计每个字符出现次数,返回出现次数排名前K的字符。C#写起来很顺手:

public static IList<char> TopKFrequentChars(string s, int k) { var dict = new Dictionary<char, int>(); foreach (char c in s) { if (dict.TryGetValue(c, out int cnt)) { dict[c] = cnt + 1; } else { dict[c] = 1; } } return dict .OrderByDescending(p => p.Value) .Take(k) .Select(p => p.Key) .ToList(); }

这段代码的时间复杂度是O(n + m log m),n是字符串长度,m是不同的字符数量。如果题目把数据量加大到极端,可以再用桶排序优化成O(n),把频率相同的字符分到同一个桶里,再用一个数组从频率高的桶往下扫。这个优化思路不用急着背下来,先掌握基础的OrderBy版本,遇到TLE再上桶优化,否则容易一次性吃太多概念。

这种“遍历计数 -> 排序取前K”的套路能跑通一大半与频次相关的题,包括数组里出现次数超过一半的元素、找出字符串中第一个不重复字符等,核心都是先建字典,再基于字典做二次处理。

3.2 前缀和:把“区间和查询”的时间复杂度降下来

前缀和解决的核心问题是:频繁计算子数组的和,如果每次都重新累加,O(n^2)逃不掉;用前缀和数组,一次计算就能把区间和查询降成O(1)的减法操作。

有一类典型题是:统计数组里有多少个子数组的和等于k。朴素的双重循环会超时,标准解法就是“前缀和+哈希表”组合:

public static int SubarraySum(int[] nums, int k) { var prefix = new Dictionary<int, int> { [0] = 1 }; int sum = 0; int ans = 0; foreach (int x in nums) { sum += x; if (prefix.TryGetValue(sum - k, out int cnt)) { ans += cnt; } prefix[sum] = prefix.GetValueOrDefault(sum, 0) + 1; } return ans; }

为什么字典初始化时要写[0] = 1?因为前缀和的起点是0:当从头到某个位置的完整和正好等于k时,需要从“空前缀”这一项里找到匹配。这个细节如果不加,所有答案会少1,属于这道题的高频踩坑点。

这个套路的核心思想是:我们不在遍历时重复计算区间,而是把“之前出现过的前缀和”存进字典,用目标值减去当前前缀和,看能否在历史里找到对应的补数。理解了这个逻辑,你会发现它能从连续子数组求和一路扩展到二维矩阵里的矩阵和问题,本质上是同一套抽屉原理。

3.3 双指针:排序之后的两数之和,边界如何处理

双指针最经典的场景是从数组中找两个数,使它们的和等于目标值。无序数组先排序,然后用一左一右两个指针往中间逼:

public static int[] TwoSumSorted(int[] nums, int target) { Array.Sort(nums); int left = 0; int right = nums.Length - 1; while (left < right) { int sum = nums[left] + nums[right]; if (sum == target) { return new[] { nums[left], nums[right] }; } if (sum < target) { left++; } else { right--; } } return Array.Empty<int>(); }

为什么要用双指针而不是穷举?因为数组排序之后,指针移动是有方向的:sum小于target时,只有把左指针右移让和变大;sum大于target时,只有把右指针左移让和变小。每一轮至少排除掉一个元素,所以最多走n步,比两两组合的O(n^2)减少一个量级。

这个写法的边界需要特别小心:while条件必须是left < right,而不是left <= right,否则会重复取到同一个元素;返回空数组时,调用方要能区分“无解”和“有解但值是0”的语义。我见过很多复盘报告,WA原因最后都落在这种一两行的边界条件上,所以宁可多写几行防御性判断,也不要硬省。

4. 我的每日刷题时间切片,以及C#刷题翻车记录

4.1 40分钟怎么切:10分钟复习、15分钟新题、15分钟对照

很多人听到“每日刷题”直觉反应是至少得一小时起步,但我实测下来,状态正常的40分钟完全够用。关键是这40分钟的结构必须固定,不能想到哪做到哪。

我按下面的表格分配时间:

时间段事项目的
0-10分钟遮住答案,重写前一天题目的核心解法用主动回忆对抗遗忘曲线
10-25分钟做今日新题,努力独立写出可运行解法训练从题目描述到代码的推导路径
25-35分钟对照题解,只研究更优解和边界处理避免收藏大量无用的解法
35-40分钟在新题旁写一句“核心套路”总结把解题思路抽象成可复用的模式

这里有个细节值得专门拿出来说:复习要放在新题前面。很多人习惯先做新题,等到快没时间了就“明天再复习旧题”,而这个“明天”永远不会真正到来,因为每天都有新的新题。把复习强制安排在最开始,相当于给旧知识一个不可跳过的高优先级分片,这是整个计划里最核心的制度设计。

每天多出来这10分钟复习,会让你的新题时间减少,短期内看起来刷题速度变慢了,但真实效果是旧题复现率大幅上升。我用这个方法试了三个星期,原来标记为“会但忘”的一批题目,大部分都能在一分钟之内回忆起思路。这种积累上的质变,是单纯刷新题给不了的。

4.2 值类型与可空值类型:int?在记忆化搜索里的隐性开销

刷题时经常用到记忆化搜索:开一个缓存数组,先判断某个状态算没算过,算过就直接返回。很多C#选手会直接用可空类型:

int?[,] memo = new int?[n, m];

写起来很直观,没有算过就是null,算过就存值。但在大量递归场景里,int?的每一次读取和赋值都附带额外的判空逻辑和装箱拆箱开销,状态一多,速度就下去了。更稳的做法是用普通int数组加一个“哨兵值”标记未计算:

int[,] memo = new int[n, m]; const int UNCOMPUTED = int.MinValue;

初始化时把所有格子填成哨兵值,访问时先判断memo[i, j] != UNCOMPUTED。只要题目里的合法结果不可能等于那个哨兵值,这种方式就既快又省心。

这个优化看起来只有毫秒级的差别,但在刷题平台上,算法正确但常数太大也是会TLE的,尤其是递归深度大、状态数量多的题目。我自己的习惯是:能不用可空类型就不用,给哨兵值取一个远离合法取值范围的数字,然后在注释里写明为什么用这个值。

4.3 LINQ顺手,但惰性求值会让你TLE

C#刷题最大的“爽点”之一就是LINQ:Where一下,Select一下,Count一下,代码简洁得赏心悦目。但LINQ的查询是惰性求值的,也就是说Where返回的不是结果集,而是一个“等真正枚举它的时候才执行的查询描述”。

一个非常经典的翻车写法:

var evens = nums.Where(x => x % 2 == 0); int count = evens.Count(); int total = evens.Sum();

这段代码看起来只做了一次筛选,实际上Count遍历一次,Sum又把整个查询重新执行了一遍,等于把Where的谓词跑了两次。在数据量小的时候无所谓,到百万级随机数据时就是实打实的耗时翻倍,运气不好直接超时。

我的经验是:只要一个LINQ查询的结果会被使用两次以上,就立即用ToList或ToArray物化:

var evens = nums.Where(x => x % 2 == 0).ToArray();

物化之后后续的Count和Sum都是对已有数组的操作,不再重复执行原始筛选逻辑。刷题时这个习惯非常重要,因为题目的数据规模是设计好的,测试用例不会因为你用了“看起来也没错”的写法就降低要求。空间换时间,在算法题里是永远成立的。

5. 一套轻量级调试框架,让每日刷题从“赌运气”变“可复现”

5.1 用固定seed的随机数据造样例,比手编数据快得多

每日刷题时最尴尬的一个瞬间,是本地跑通了,提交却WA,然后你不知道去哪儿造一个能复现问题的数据。我之前手动编测试用例,编到一半往往发现自己编的都是对称的“好看数据”,根本触及不到边界。

后来我固定了一套随机测试套路:用Random类的固定种子生成数据,这样每次跑出来的随机序列完全一致,一旦发现错误用例,下次启动还能原样复现。代码大概长这样:

static int[] GenArray(int n, int seed) { var rand = new Random(seed); var arr = new int[n]; for (int i = 0; i < n; i++) { arr[i] = rand.Next(-100, 101); } return arr; }

为什么固定种子这么重要?随机数生成器在给定相同种子的情况下会生成完全相同的数据序列,这意味着你可以放心地用随机数据做大规模验证,而不用担心“这次跑出错,下台机器跑不出来”。对每日刷题来说,你只需要记一个参数——数据规模和seed值,就能随时重建出当时导致WA的测试环境。

我用这个方法抓到过好几个奇怪边界:比如数组全是0、全是负数、递增序列、递减序列等大概率手写用例根本想不起来的形态。每次跑出错误,先把这个用例降到最小规模,然后再一步步排查,定位效率比肉眼扫代码高得多。

5.2 空数组、单元素、全重复:每组题都要过的边界清单

刷题经验积累到一定程度之后,我总结出了一份固定的边界检查清单。每次写完一道题,不急着直接贴到LeetCode上,先在本地用下面几类数据各跑一遍,很多WA能提前拦截住。

测试类型检查重点
空数组代码会不会崩,返回值是否合理
单元素数组循环是否执行,索引是否越界
全部相同元素排序稳定性、去重逻辑、计数逻辑
严格递增/递减指针移动方向、边界条件
包含负数哨兵值的选取是否冲突
极端最大值加法是否溢出,int是否够用

这份清单不是每道题都要全跑一遍,它的作用是防止自己陷入“只挑看得顺眼的用例验证”的舒适区。很多时候题目的正确性就差在那一个扭着来的边界数据上,而固定清单能让你用最小成本覆盖掉最常见的雷区。我现在每次刷完题都会在本地脚本里把这些用例跑一遍,默认全部通过才提交,提交通过的稳定性比以前高了很多。

5.3 把刷题样例固化成一个本地测试脚本

把边界清单和随机测试结合起来,最终形成下面这样一个固定的测试骨架,几乎可以套用到大多数独立函数型题目上:

using System.Diagnostics; class Program { static void Main() { Run(new[] { 1, 1, 2, 3 }, 3, 2, "基础示例"); Run(Array.Empty<int>(), 0, 0, "空数组"); Run(new[] { 5 }, 5, 1, "单元素"); Run(new[] { -1, -2, -3 }, -6, 1, "全负数"); var rand = new Random(2024); Console.WriteLine("开始随机测试..."); Run(GenArray(2000, 2024), 10, -1, "量大随机"); } static void Run(int[] nums, int k, int expected, string name) { int actual = SubarraySum(nums, k); bool ok = actual == expected; Console.WriteLine($"{name}: {(ok ? "通过" : "失败")},实际 {actual}"); } static int[] GenArray(int n, int seed) { var rand = new Random(seed); var arr = new int[n]; for (int i = 0; i < n; i++) arr[i] = rand.Next(-10, 11); return arr; } static int SubarraySum(int[] nums, int k) { // 这里粘贴当天的解题函数 return 0; } }

这个脚本的思路是:把当天题目的函数复制进SubarraySum,然后按自己总结的清单补充几组用例,最后跑一次查看输出。它不需要任何第三方框架,一个控制台项目就能跑。真正有价值的是白纸黑字的“expected”字段,它逼着你在一开始就想清楚正确答案是什么,而不是等着看程序输出再判断“好像差不多”。这套框架我用下来最大的体会是,每天刷题后的收尾工作变得极其具象,不再是一句模糊的“好像做对了”,而是实打实的“这些用例全部通过”。

如果你也被每日刷题搞得很累又看不到进步,不妨先停掉盲目刷新题的冲动,花一周时间把我这套复习、固定套路、本地调试三板斧完整跑一遍。等你再点开一周前做过的那道题,直接在键盘上把解法敲出来的时候,那种成就感比日历上再多一排绿格子要踏实得多。

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

OpenClaw工具安全控制:沙盒隔离与权限边界设计实践

1. 项目概述&#xff1a;OpenClaw 工具策略与安全控制1.1 核心需求解析先直接说结论&#xff1a;OpenClaw 这套体系里&#xff0c;工具策略和安全控制是比“模型怎么调用工具”更值得关注的问题。因为模型本身是一个概率系统&#xff0c;它可能因为提示词的微小变化、上下文长度…

作者头像 李华
网站建设 2026/10/22 5:40:55

Nginx从入门到排障:反向代理、负载均衡与静态缓存实战

简介&#xff1a;面向Linux运维与Nginx学习者&#xff0c;这份资料包系统梳理了从基础指令到集群架构的完整知识体系&#xff0c;适合初学者进阶以及运维工程师随用随查。内容按基础篇、进阶篇、架构篇、模块篇划分&#xff0c;每个阶段侧重点不同&#xff0c;涵盖nginx指令配置…

作者头像 李华
网站建设 2026/10/22 5:40:07

小程序电影院购票系统:架构、锁座与订单状态机实现

简介&#xff1a;这套基于微信小程序与Spring Boot技术栈的电影院购票管理系统源码包&#xff0c;面向计算机类毕业生以及正在学习Java全栈开发的读者。系统采用前后端分离设计&#xff0c;管理后台基于vue与elementui&#xff0c;用户端为微信小程序&#xff0c;后端整合sprin…

作者头像 李华
网站建设 2026/10/22 5:40:06

YOLOv5云服务器GPU部署避坑指南:从环境搭建到稳定上线

“模型在本地能跑&#xff0c;换到云服务器就各种崩”&#xff0c;这大概是做目标检测部署时最常听见的一句话。我前段时间刚好把一套YOLOv5模型完整迁移到某云平台的GPU实例上&#xff0c;踩遍了环境编译、权重传输、显存溢出这些坑。这篇内容把整个部署过程拆开讲清楚&#x…

作者头像 李华
网站建设 2026/10/22 5:38:03

梯度下降实战解析:学习率、批量与优化器调参避坑指南

梯度下降这四个字&#xff0c;在机器学习领域几乎天天见。刷课程、读博客&#xff0c;公式都能背下来&#xff0c;但真正动手调参时&#xff0c;loss 曲线不降反升、反复震荡、卡在平台期&#xff0c;这些情况碰到过一遍才知道理论离实践有多远。最近我把那门经典的机器学习公开…

作者头像 李华