1. 为什么选择LeetCode刷题
作为一名从2015年开始接触LeetCode的老用户,我见证了国内程序员群体对算法练习态度的转变。最初只有少数准备海外求职的程序员会刷题,如今已成为国内一线大厂面试的标配环节。选择LeetCode作为日常练习平台,主要基于以下几个考量:
首先是题库质量。LeetCode目前拥有2000+道算法题目,覆盖数据结构、算法、数据库、Shell等各个方向。每道题目都经过专业设计,有明确的输入输出规范,且提供测试用例验证功能。相比其他平台的题目,LeetCode的题目描述更加严谨,边界条件考虑更全面。
其次是社区生态。每道题目下都有讨论区,可以看到全球程序员的不同解法。特别是一些高票答案,往往能提供最优解或独特的解题思路。这种集体智慧的共享,对提升编程思维非常有帮助。
最后是面试关联度。根据我的统计,国内一线互联网公司技术面试中,超过70%的算法题都直接来自LeetCode题库或改编题。坚持每日刷题,相当于在为未来的技术面试做最直接的准备。
2. C#刷题环境配置
2.1 开发工具选择
对于C#开发者,首推Visual Studio Community版作为开发环境。它不仅免费,还提供了强大的代码补全、调试和单元测试功能。安装时建议勾选".NET桌面开发"工作负载,这会包含必要的.NET SDK和运行时。
如果追求轻量级,也可以使用VS Code配合C#扩展。需要先安装.NET 6.0 SDK,然后在VS Code中安装官方的C#扩展。这种组合启动速度快,适合快速刷题场景。
提示:无论选择哪种工具,都建议安装LeetCode插件。VS的LeetCode插件可以直接在IDE中浏览题目、提交代码,大大提升刷题效率。
2.2 基础项目结构
建议为LeetCode练习创建专门的解决方案,按题目分类建立项目。我的项目结构通常如下:
code复制LeetCodePractice
├── ArrayProblems
├── StringProblems
├── TreeProblems
├── DynamicProgramming
└── SystemDesign
每个项目都设置为控制台应用程序,使用.NET 6.0。在Program.cs中直接编写解题代码,通过Console.WriteLine输出结果。对于需要多次测试的题目,可以建立专门的测试方法。
2.3 常用工具方法
刷题过程中会频繁使用一些辅助方法,建议提前准备好:
csharp复制// 快速生成链表
public static ListNode BuildLinkedList(int[] values)
{
if(values == null || values.Length == 0) return null;
var dummy = new ListNode(0);
var current = dummy;
foreach(var val in values)
{
current.next = new ListNode(val);
current = current.next;
}
return dummy.next;
}
// 打印数组
public static void PrintArray<T>(T[] array)
{
Console.WriteLine("[" + string.Join(",", array) + "]");
}
3. C#刷题技巧与最佳实践
3.1 语言特性利用
C#相比其他语言有一些独特优势,合理利用可以简化代码:
- LINQ:对于集合操作题目,LINQ可以极大简化代码。例如两数之和问题:
csharp复制public int[] TwoSum(int[] nums, int target)
{
return nums.Select((x,i) => new {x,i})
.FirstOrDefault(x => nums.Contains(target - x.x) &&
Array.IndexOf(nums, target - x.x) != x.i)?
.Let(x => new[]{x.i, Array.IndexOf(nums, target - x.x)})
?? new int[0];
}
- ValueTuple:需要返回多个值时,比out参数更优雅:
csharp复制public (int max, int min) FindMaxMin(int[] nums)
{
return (nums.Max(), nums.Min());
}
- Span
:处理大型数组时性能更好:
csharp复制public void ReverseString(char[] s)
{
var span = s.AsSpan();
for(int i=0; i<span.Length/2; i++)
{
(span[i], span[span.Length-1-i]) = (span[span.Length-1-i], span[i]);
}
}
3.2 常见算法实现模式
经过大量练习,我总结出几种高频算法模式的C#实现模板:
滑动窗口模板:
csharp复制public int SlidingWindowTemplate(string s)
{
var dict = new Dictionary<char, int>();
int left = 0, right = 0;
int maxLen = 0;
while(right < s.Length)
{
// 更新右指针
if(!dict.ContainsKey(s[right]))
dict[s[right]] = 0;
dict[s[right]]++;
right++;
// 收缩左指针的条件
while(/* 窗口不满足条件 */)
{
dict[s[left]]--;
if(dict[s[left]] == 0)
dict.Remove(s[left]);
left++;
}
// 更新结果
maxLen = Math.Max(maxLen, right - left);
}
return maxLen;
}
回溯法模板:
csharp复制public IList<IList<int>> BacktrackingTemplate(int[] nums)
{
var result = new List<IList<int>>();
Backtrack(nums, new List<int>(), result);
return result;
}
private void Backtrack(int[] nums, List<int> path, List<IList<int>> result)
{
if(/* 终止条件 */)
{
result.Add(new List<int>(path));
return;
}
for(int i=0; i<nums.Length; i++)
{
// 剪枝
if(/* 跳过条件 */) continue;
path.Add(nums[i]); // 做选择
Backtrack(nums, path, result);
path.RemoveAt(path.Count - 1); // 撤销选择
}
}
3.3 调试与性能优化
调试技巧:
- 使用条件断点:对于大型输入,可以设置断点条件,如
i == 500,直接跳到问题位置 - 即时窗口:在VS中可以使用即时窗口修改变量值,快速验证不同输入
- 可视化工具:对于树、链表等结构,使用VS的可视化工具查看内存状态
性能优化:
- 避免频繁的LINQ查询:虽然代码简洁,但在大数据量时性能较差
- 使用ArrayPool减少GC压力:对于临时大数组,可以从ArrayPool租用
- 结构体替代类:对于小型数据结构,使用struct可以减少堆分配
4. 题目分类与解题策略
4.1 数组与字符串
这类题目通常考察基础算法能力和边界条件处理。常见技巧包括:
- 双指针技巧(快慢指针、左右指针)
- 滑动窗口(解决子串/子数组问题)
- 前缀和(快速计算区间和)
示例题目(#238 除自身以外数组的乘积):
csharp复制public int[] ProductExceptSelf(int[] nums)
{
int n = nums.Length;
var result = new int[n];
// 左积
result[0] = 1;
for(int i=1; i<n; i++)
{
result[i] = result[i-1] * nums[i-1];
}
// 右积
int right = 1;
for(int i=n-1; i>=0; i--)
{
result[i] *= right;
right *= nums[i];
}
return result;
}
4.2 树与图
树相关问题通常需要掌握:
- 各种遍历方式(前序、中序、后序、层序)
- 递归与迭代实现
- 特殊树结构(BST、AVL、红黑树)
示例题目(#105 从前序与中序遍历序列构造二叉树):
csharp复制public TreeNode BuildTree(int[] preorder, int[] inorder)
{
return Build(preorder, inorder, 0, preorder.Length-1, 0, inorder.Length-1);
}
private TreeNode Build(int[] pre, int[] ino, int preStart, int preEnd, int inStart, int inEnd)
{
if(preStart > preEnd) return null;
var rootVal = pre[preStart];
var root = new TreeNode(rootVal);
int inRoot = Array.IndexOf(ino, rootVal);
int leftSize = inRoot - inStart;
root.left = Build(pre, ino, preStart+1, preStart+leftSize, inStart, inRoot-1);
root.right = Build(pre, ino, preStart+leftSize+1, preEnd, inRoot+1, inEnd);
return root;
}
4.3 动态规划
DP问题通常有固定解决模式:
- 定义dp数组含义
- 找出状态转移方程
- 确定初始条件
- 考虑优化空间
示例题目(#322 零钱兑换):
csharp复制public int CoinChange(int[] coins, int amount)
{
var dp = new int[amount + 1];
Array.Fill(dp, amount + 1);
dp[0] = 0;
for(int i=1; i<=amount; i++)
{
foreach(var coin in coins)
{
if(coin <= i)
{
dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
5. 刷题计划与进度管理
5.1 制定合理计划
根据我的经验,有效的刷题计划应该:
- 按难度梯度进行:先Easy熟悉语法和基础算法,再Medium掌握核心算法,最后挑战Hard
- 按专题突破:集中一段时间专攻某一类问题(如动态规划),形成肌肉记忆
- 定期复习:建立错题本,每周回顾之前做错的题目
建议每日刷题量:
- 新手:1-2题(保证理解透彻)
- 进阶:3-5题(多种解法)
- 冲刺:5-10题(限时训练)
5.2 进度跟踪工具
我使用Notion建立刷题数据库,记录以下信息:
- 题目编号、名称、难度
- 解题日期、所用时间
- 解题思路(文字描述)
- 代码链接(Gist或本地文件)
- 标签分类(数组、DP、树等)
- 掌握程度(1-5星)
每月统计各类型的解题数量和平均用时,找出薄弱环节针对性加强。
5.3 模拟面试训练
当刷题量达到200+时,建议开始模拟面试:
- 使用LeetCode的模拟面试功能
- 设置45分钟计时(15分钟读题+30分钟编码)
- 完成后检查:
- 代码是否通过所有测试用例
- 时间复杂度分析是否准确
- 是否有优化空间
我通常会录制模拟面试过程,回放观察自己的解题思路和编码习惯,找出需要改进的地方。