资讯详情 C# 通用算法与数据结构库实战:从泛型设计到百种算法落地
📅 2026/10/10 13:07:36
简介这是一份以C#实现100余种算法和数据结构的源码仓库打包特别适合.NET开发者、算法爱好者及需要稳定参考实现的程序员能够解决从零实现代价高、缺少可运行C#示例的问题。资源以Advanced.Algorithms类库为主体覆盖排序、图算法、AVL树、红黑树、B树、区间树、R树、斐波那契堆、Treap等经典与进阶结构也涉及堆、二叉树、四叉树、配对堆、伸展树等主题面向算法学习、源码阅读和二次修改场景。压缩包共558个文件以258个C#源文件为核心辅以HTML文档、csproj/sln工程配置、JSON与config设置、bat/ps1构建脚本等整体仅3.01MB便于快速下载、还原依赖并本地编译查看。目前已有206人浏览学习目录围绕算法分类组织代码结构清晰。读者可直接查看BTree.cs、RedBlackTree.cs、AvlTree.cs等关键实现并结合项目脚本自行编译运行作为算法对比、面试复习或功能模块移植的实用参考。1. 用 C# 把 100 多种算法和数据机构做成一个通用库这事到底靠不靠谱先给个结论用 C# 实现一套覆盖 100 多种算法和数据结构的通用库不是噱头而是完全能落地的事。很多团队一提到算法库默认就是 C 或 Python 的天下。C 追求极致性能Python 胜在快速验证。但如果你是做上位机、桌面工具、工业软件或者公司内部的数据处理管道把算法层用 C# 写死反而是更省心的选择内存管理交给 GC跨平台用 .NET 运行时兜底再用泛型和接口把整个库的组织方式固定下来后续加算法就像填格子而不是拆地基。我见过不少开发者一上来就翻开源项目找现成轮子结果要么依赖太重要么算法版本太老要么跟业务数据结构耦合太深改一个排序要动三个类。自己动手做一个通用算法与数据结构库价值不在「能不能写出快排」而在「怎么设计一组接口让 100 多种算法能共享同一套遍历、比较、交换、度量逻辑」。这套设计一旦立住面试刷题、日常开发、内部工具链都会受益。本文就沿着「通用库怎么立项 → 接口怎么抽象 → 具体算法怎么落地 → 数据结构怎么设计 → 测试和踩坑 → 进阶玩法」这条线把这套东西讲透。适合谁看想系统整理算法知识的新手被项目逼着要自己造轮子的上位机或后端工程师以及厌倦了每次都重新写队列、树、图的那类熟手。这套方案不需要你有 C 底子不需要引入第三方库一个 .NET 控制台项目就能从头跑起来。2. 先搞定通用抽象泛型、委托与迭代器是这套库的三根柱子2.1 为什么 IComparable 和泛型约束能省掉一半重复代码写通用算法库第一道坎就是「算法不关心元素是什么类型」。排序算法只关心元素之间怎么比较大小搜索算法只关心怎么判断目标相等图算法只关心边上的权值怎么读取。C# 里的IComparableT和泛型约束恰好能把「元素类型」和「元素比较方式」解耦。我一般会这样设计接口的基座所有算法方法都定义在静态类里方法签名统一为T泛型并对T加上where T : IComparableT约束。这样int、double、string天然满足约束自定义类只需要实现一个比较接口就能直接丢进算法库。不用为每种类型写一份重载这是第一层去重。namespace AdvancedAlgorithms.Core { // 所有排序、搜索算法都基于这个约束 public static class Sort { public static void QuickSortT(T[] array) where T : IComparableT { QuickSort(array, 0, array.Length - 1); } private static void QuickSortT(T[] array, int low, int high) where T : IComparableT { if (low high) return; int pivot Partition(array, low, high); QuickSort(array, low, pivot - 1); QuickSort(array, pivot 1, high); } private static int PartitionT(T[] array, int low, int high) where T : IComparableT { T pivotValue array[high]; int i low - 1; for (int j low; j high; j) { if (array[j].CompareTo(pivotValue) 0) { i; Swap(array, i, j); } } Swap(array, i 1, high); return i 1; } private static void SwapT(T[] array, int i, int j) { (array[i], array[j]) (array[j], array[i]); } } }逻辑上这是最经典的 Lomuto 分区快排选了最后一个元素做基准。泛型签名保证CompareTo是唯一比较通道所以任何实现了IComparableT的类型都能直接调用。参数上需要注意这个版本使用递归数组极长时建议换成显式栈的迭代实现否则调用深度可能撑爆线程栈。对于通用库来说我给调用方留出两个入口数组全量排序和子区间排序子区间版本没在上面的代码里贴但在实际库中我会保留low、high参数的重载方便在归并、分治场景内复用。where T : IComparableT还有一层隐藏的好处编译期就把「不可比较类型」挡在门外而不是等到运行时抛异常。这符合通用库的第一原则——能在编译期解决的错误不要拖到运行期。2.2 用 IComparer 注入自定义排序规则绕开默认比较的坑IComparableT解决的是「类型自身怎么比」但实际项目里经常出现「同一类型有多种排序方式」。员工类默认按工号排序但某次需求要按入职时间倒序某次要按薪资升序。这时候如果只依赖IComparableT算法库就僵死了。解法是给每个算法留一个IComparerT参数重载默认等于ComparerT.Default。public static class QuickSortExtensions { public static void QuickSortT(this T[] array, IComparerT? comparer null) { comparer ?? ComparerT.Default; QuickSortInternal(array, 0, array.Length - 1, comparer); } private static void QuickSortInternalT(T[] array, int low, int high, IComparerT comparer) { if (low high) return; int pivot Partition(array, low, high, comparer); QuickSortInternal(array, low, pivot - 1, comparer); QuickSortInternal(array, pivot 1, high, comparer); } private static int PartitionT(T[] array, int low, int high, IComparerT comparer) { T pivot array[high]; int i low - 1; for (int j low; j high; j) { if (comparer.Compare(array[j], pivot) 0) { i; (array[i], array[j]) (array[j], array[i]); } } (array[i 1], array[high]) (array[high], array[i 1]); return i 1; } }comparer ?? ComparerT.Default这行是精髓它让调用方不传参数时走系统默认比较器传了 Lambda 或自定义比较器就走定制逻辑。在 .NET 5 以上版本里ComparerT.Create能把比较 lambda 直接转成比较器所以实际调用时可以写成arr.QuickSort((x, y) y.CompareTo(x))一行实现倒序排序。这套扩展方法写法让排序算法用起来像 LINQ 一样自然。2.3 迭代器让二叉树的三种遍历只需写一套 foreach 逻辑数据结构库最难抽象的不是存储而是遍历。树的先序、中序、后序、层序图的深度优先、广度优先传统的做法是每一种遍历写一个方法方法内部维护栈或队列然后把访问逻辑通过回调函数暴露出去。回调的方式能用但调用方写起来很别扭——回调里不能方便地break不能自然地嵌套。用 C# 的yield return把遍历逻辑写成迭代器这是通用库设计里性价比最高的决定。以二叉树中序遍历为例public class BinaryTreeNodeT { public T Value { get; set; } public BinaryTreeNodeT? Left { get; set; } public BinaryTreeNodeT? Right { get; set; } public BinaryTreeNode(T value) { Value value; } } public static class BinaryTreeTraversal { // 中序遍历左子树 - 根 - 右子树 public static IEnumerableT InOrderT(BinaryTreeNodeT? root) { if (root null) yield break; var stack new StackBinaryTreeNodeT(); var current root; while (current ! null || stack.Count 0) { while (current ! null) { stack.Push(current); current current.Left; } current stack.Pop(); yield return current.Value; current current.Right; } } }说下这里为什么用显式栈而不是递归递归版本在树深度很大时会触发 StackOverflow而 C# 的迭代器方法本质上是状态机显式栈版本配合yield return后遍历过程是流式的——调用方可以只取前三个元素然后放弃迭代库不会白算整棵树。调用方这样用var root new BinaryTreeNodeint(10) { Left new BinaryTreeNodeint(5), Right new BinaryTreeNodeint(15) }; foreach (var val in BinaryTreeTraversal.InOrder(root)) { Console.WriteLine(val); // 输出 5, 10, 15 }这就是迭代器模式在算法库里的真正价值遍历逻辑与消费逻辑完全解耦而且天然支持 LINQ 的Take、Where等操作组合。3. 百种算法分层落地排序、搜索、图论、动态规划的组织方式3.1 排序算法家族从 O(n²) 到 O(n log n) 再到非比较排序怎么组织命名空间100 多种算法不可能平铺在一个类里否则类文件会变成几千行的怪兽。我习惯按算法族划分命名空间Sorting、Searching、Graph、String、Numeric、DynamicProgramming、DataStructures。每个命名空间内一个类管一族算法比如Sorting.BubbleSort、Sorting.MergeSort、Sorting.HeapSort、Sorting.CountingSort。这样查找便捷IDE 的智能提示也友好。排序这块我会覆盖三档第一档是教学意义的冒泡、选择、插入第二档是工程意义的快排、归并、堆排序第三档是非比较排序——计数排序、桶排序、基数排序。非比较排序在通用库里有个前提只适用于整数或可哈希到整数的类型所以接口需要单独设计不能用IComparableT硬套。给个计数排序的典型实现public static class CountingSorter { // 仅适用于非负整数数组值域范围已知的场景 public static void CountingSortInPlace(int[] array, int maxValue) { int[] counts new int[maxValue 1]; for (int i 0; i array.Length; i) { if (array[i] 0) throw new ArgumentException(CountingSort 不适用于负数); counts[array[i]]; } int index 0; for (int value 0; value maxValue; value) { while (counts[value] 0) { array[index] value; counts[value]--; } } } }这个算法的时间复杂度是 O(n k)其中 k 是值域跨度。它的适用场景非常窄只适合元素值域小且范围已知的情况比如年龄分布统计、成绩等级统计。如果数组里有负数要么先整体加偏移量要么直接换基数排序。参数maxValue是最容易踩坑的入口——传小了会数组越界传大了内存浪费调用方需要自己保证这个值跟数据匹配。3.2 搜索算法全覆盖线性、二分、插值、指数以及它们的适用边界搜索算法的家族和排序一样要分层次。线性搜索最简单但很多人忽略了它还有个优化分支——带哨兵的线性搜索可以减少每次循环里的边界比较。二分搜索是工程主力但边界条件永远是重灾区。我把二分搜索的代码写成返回「第一个不小于目标值的位置」这样一套逻辑同时覆盖查找、插入点定位、区间查询三个需求。public static class BinarySearchHelper { // 返回第一个 target 的元素索引若都小于 target返回 array.Length public static int LowerBoundT(T[] array, T target) where T : IComparableT { int low 0, high array.Length; while (low high) { int mid low (high - low) / 2; if (array[mid].CompareTo(target) 0) low mid 1; else high mid; } return low; } }注意两点high初始值取array.Length而非array.Length - 1这样天然支持「插入位置在末尾」的情况mid low (high - low) / 2是为了防止low high溢出这在数组特别大时不是玄学而是实打实的 bug。这个LowerBound配合Array.Copy就能在一个有序数组里做区间查询、密度统计等操作。指数搜索和插值搜索在通用库里作为进阶选项保留前者适合无限长或长度未知的有序序列后者适合数据分布均匀的场景但不均匀时会退化成线性复杂度所以我不会把它们设为首选。3.3 图算法模块邻接表、优先级队列和访问标记三位一体图算法是这类通用库的重头戏。Dijkstra、Bellman-Ford、Floyd-Warshall、Kruskal、Prim、拓扑排序、连通分量……十几个算法如果每个都从邻接矩阵读数据代码会非常冗余。正确做法是先定义统一的图存储结构——邻接表然后让所有图算法都基于这个结构工作。public class Graph { private readonly List(int To, int Weight)[] _adjacency; public int VertexCount { get; } public Graph(int vertexCount) { VertexCount vertexCount; _adjacency new List(int To, int Weight)[vertexCount]; for (int i 0; i vertexCount; i) _adjacency[i] new List(int To, int Weight)(); } public void AddDirectedEdge(int from, int to, int weight) { _adjacency[from].Add((to, weight)); } public IReadOnlyList(int To, int Weight) GetNeighbors(int vertex) _adjacency[vertex]; }这里用了 .NET 的元组语法(int To, int Weight)比自定义边类省事很多。在此基础上实现 Dijkstra 时优先级队列直接用PriorityQueue(int Node, int Dist), int这是 .NET 6 引入的内置泛型优先级队列之前需要自己手写堆。public static class ShortestPath { // Dijkstra 最短路径dist 数组输出每个点到起点的最短距离 public static int[] Dijkstra(Graph graph, int source) { int n graph.VertexCount; int[] dist new int[n]; Array.Fill(dist, int.MaxValue); dist[source] 0; var pq new PriorityQueue(int Node, int Dist), int(); pq.Enqueue((source, 0), 0); while (pq.Count 0) { var (node, d) pq.Dequeue(); if (d dist[node]) continue; // 跳过过期的松弛记录 foreach (var (to, weight) in graph.GetNeighbors(node)) { int newDist d weight; if (newDist dist[to]) { dist[to] newDist; pq.Enqueue((to, newDist), newDist); } } } return dist; } }这段代码里最关键的是if (d dist[node]) continue它处理了同一个节点可能多次入队的问题。Dijkstra 的教科书版本会给每个节点记录 true/false 的 visited 标记但工程版本用这条延迟删除逻辑更简洁——不需要额外数组不会漏掉更短路径。PriorityQueue的优先级参数其实就是距离本身所以入队时传newDist即可。3.4 字符串与动态规划从 KMP 到 LCS这类算法该怎么组织成通用类字符串算法和动态规划在通用库里往往以静态方法的形式各自独立。KMP 的next数组、Boyer-Moore 的坏字符表这些东西很难抽象成统一接口所以标准做法是每个算法一个公开静态方法参数只暴露「主串和模式串」两个入口预处理细节全部封装在私有方法里。public static class StringSearch { // 返回模式串在主串中第一次出现的位置不存在则返回 -1 public static int KmpIndexOf(string text, string pattern) { if (string.IsNullOrEmpty(pattern)) return 0; int[] next BuildNext(pattern); int i 0, j 0; while (i text.Length) { if (text[i] pattern[j]) { i; j; if (j pattern.Length) return i - j; } else if (j 0) { j next[j - 1]; } else { i; } } return -1; } private static int[] BuildNext(string pattern) { int[] next new int[pattern.Length]; int j 0; for (int i 1; i pattern.Length; i) { while (j 0 pattern[i] ! pattern[j]) j next[j - 1]; if (pattern[i] pattern[j]) { j; next[i] j; } } return next; } }KMP 的next数组构建是公认的反直觉环节。这个版本里next[i]表示长度为i 1的前缀子串中最长的相等前后缀长度。当失配时跳转到next[j - 1]而不是next[j]很多人的 bug 就出在这——把next[j]当成跳转目标会导致死循环或者跳过匹配位。我的排错经验在BuildNext里打印每一步的i、j值和next数组变化一眼就能看出哪里跳错了。动态规划部分更是顺着问题域拆类LcsSolver、KnapsackSolver、EditDistanceSolver。每个类内部维护自己的 DP 表格不共享状态。为什么不能像排序那样统一成一个DynamicProgramming类因为 DP 问题的状态定义、转移方程、边界初始化差异太大强行统一只会加重理解成本。4. 数据结构库的设计链表、栈、队列、堆与红黑树怎么做到零基础可用4.1 统一接口 ICollection 为什么栈和队列也要实现 Remove数据结构库比算法库更讲究接口设计。C# 自带的StackT和QueueT很实用但通用算法库里如果需要给「可回退的搜索结果」做一个容器栈和队列就要被统一抽象。我一般定义一个ICollectionT风格的接口包含Add、Remove、Peek、Count、IsEmpty五个成员。栈的Remove是 Pop队列的Remove是 Dequeue这样上层算法只需要面向接口编程不用关心底层是先进先出还是后进先出。这个设计看起来违反常识因为队列和栈行为完全不同。但它的价值在于当你写一个适配器或者装饰器时比如「带最大容量限制的栈」「带并发锁的队列」你只需要让装饰器实现同一个接口然后把内部操作转发给具体的集合类即可。通用库的抽象程度越高上层算法越自由。public interface ISimpleCollectionT { void Add(T item); T Remove(); T Peek(); int Count { get; } bool IsEmpty { get; } } public class SimpleStackT : ISimpleCollectionT { private readonly StackT _stack new(); public void Add(T item) _stack.Push(item); public T Remove() _stack.Pop(); public T Peek() _stack.Peek(); public int Count _stack.Count; public bool IsEmpty _stack.Count 0; } public class SimpleQueueT : ISimpleCollectionT { private readonly QueueT _queue new(); public void Add(T item) _queue.Enqueue(item); public T Remove() _queue.Dequeue(); public T Peek() _queue.Peek(); public int Count _queue.Count; public bool IsEmpty _queue.Count 0; }4.2 二叉堆优先队列内部的优先级反转与下沉/上浮实现二叉堆是堆排序和 Dijkstra 算法的底层依赖。.NET 的PriorityQueue默认是最小堆但如果你要自己实现一版需要把「上浮」和「下沉」两个操作写稳。public class BinaryHeapT where T : IComparableT { private ListT _items new(); private readonly bool _isMinHeap; public BinaryHeap(bool isMinHeap true) { _isMinHeap isMinHeap; } public int Count _items.Count; public void Insert(T item) { _items.Add(item); SiftUp(_items.Count - 1); } public T ExtractTop() { if (_items.Count 0) throw new InvalidOperationException(堆为空); T top _items[0]; int lastIndex _items.Count - 1; _items[0] _items[lastIndex]; _items.RemoveAt(lastIndex); if (_items.Count 0) SiftDown(0); return top; } private void SiftUp(int index) { while (index 0) { int parent (index - 1) / 2; if (Compare(_items[index], _items[parent]) 0) { (_items[index], _items[parent]) (_items[parent], _items[index]); index parent; } else { return; } } } private void SiftDown(int index) { while (true) { int left index * 2 1; int right left 1; if (left _items.Count) return; int best left; if (right _items.Count Compare(_items[right], _items[left]) 0) best right; if (Compare(_items[best], _items[index]) 0) { (_items[index], _items[best]) (_items[best], _items[index]); index best; } else { return; } } } private int Compare(T a, T b) { return _isMinHeap ? a.CompareTo(b) : b.CompareTo(a); } }_isMinHeap参数实现大小堆切换通过取反比较结果来达成。这里有个性能点SiftDown里每次比较都要访问_items多次如果你要在算法库里追求极致性能可以把数组引用缓存到局部变量里但对于通用库来说可读性优先。这个类最常被误解的地方是ExtractTop返回堆顶但不保证剩余元素有序。堆只保证堆顶最大或最小不保证全局有序。如果你需要「反复取最大/最小」的场景堆比排序快很多但如果你需要「输出全部有序元素」则应该用真正的排序算法不要拿堆顶一个个弹。4.3 红黑树和 AVL 树的取舍什么时候别自己造自平衡树自平衡二叉搜索树是通用库里最重的数据结构新手最纠结的问题是「红黑树代码这么长到底要不要自己写」。我的态度很明确如果只是业务开发直接用SortedSetT或SortedDictionaryTKey, TValue自己写自平衡树的唯一理由是学习原理或者面试前补课又或者你需要一个带「范围查询」能力且能统计子树大小的定制版树结构。如果真的决定写AVL 树比红黑树更友好。AVL 的平衡条件是「每个节点的左右子树高度差不超过 1」只需要在插入和删除后沿路径做 LL、RR、LR、RL 四种旋转。它的代码量大约是红黑树的一半高度更严格查询性能稳定。缺点是插入删除时旋转更频繁但通用库的查询场景远多于修改场景这个取舍是划算的。4.4 哈希表和字典设计哈希函数三个容易翻车的细节哈希表这块绝大多数情况下直接用DictionaryTKey, TValue即可。但通用库如果要做「可哈希的算法键」比如图算法的路径缓存、动态规划的记忆化搜索就需要理解哈希的底层行为。三个细节需要格外留意第一自建类型的GetHashCode必须和Equals保持同步。两个对象Equals为 true 时哈希码必须相同反过来不要求。违反这条规则你会得到幽灵键——字典里存得进去取不出来。第二不要用可变字段参与GetHashCode计算。一旦键对象被放进字典后字段发生了修改哈希码就变了字典底层数组索引会完全错乱。第三哈希桶数量是动态扩容的扩容阈值和负载因子会影响性能但通用库不需要给调用方暴露出这些参数。如果你要记录某个算法运行时的键数量分布可以用Dictionary.Keys.Count做监控但不建议手动干预扩容策略。5. 编译、测试与避坑从内存分配到泛型判等这种算法库最该注意的七件事5.1 泛型比较三件套EqualityComparer、Comparer 和 IComparable 的优先级泛型库出错频率最高的地方就是比较操作。写T类型的方法时千万别直接写if (a b)因为在没有约束时会被解析为object.ReferenceEquals而你会得到值相等但引用不同的两个元素无法匹配的结果。正确做法是用EqualityComparerT.Default.Equals(a, b)或者给泛型加where T : IEquatableT约束。public static int IndexOfT(T[] array, T target) where T : IEquatableT { for (int i 0; i array.Length; i) { if (EqualityComparerT.Default.Equals(array[i], target)) return i; } return -1; }这里用IEquatableT做约束是为了避免引入装箱。EqualityComparerT.Default内部会检查T是否实现IEquatableT实现了就调用强类型接口没有则回退到object.Equals。这就是为什么这个工具方法在泛型库中是必需品而不是可选项。5.2 数组交换和切片ref struct 与 Span 的性能坑通用算法库里大量使用数组交换、切片、拼接。Array.Copy和ArraySegmentT是常规武器但 .NET 5 以上的版本里SpanT更值得关注。它能直接操作一段连续内存不产生新的数组对象在字符串处理和数值计算算法里特别合适。public static void ReverseT(SpanT span) { int left 0, right span.Length - 1; while (left right) { (span[left], span[right]) (span[right], span[left]); left; right--; } }接受SpanT作为参数调用方可以直接传array.AsSpan(2, 5)来反转子区间不需要像Array.Reverse(array, 2, 5)那样额外分配一个临时数组。这里有个知识点SpanT是 ref struct不能放进类字段所以如果你的算法类需要保存一段切片供后续使用那就得换MemoryT或者干脆保留原始数组和索引对。通用库的设计上我会优先让方法接受ReadOnlySpanT来读取数据只在需要修改时接受SpanT。5.3 大 O 理论和实际性能测试的脱节为什么冒泡排序在小数据量时并不慢排序算法选型时光看时间复杂度会踩坑。冒泡排序 O(n²) 在 n100 时只需要几百次比较耗时连一毫秒都不到而快速排序虽然 O(n log n)但常数因子更大。通用库在提供算法选择时不应该替调用方做决定而应该把适用规模写在注释里。我在每个排序类上都加了Best/Average/Worst复杂度和「建议数据规模」的 XML 文档注释这样用 IDE 的鼠标悬浮就能看到选型建议。5.4 递归改迭代的两条路径显式栈和尾递归的 C# 限制很多树形和图算法天然用递归表达但 C# 对递归的容忍度是有限的。深度一万层的递归在 .NET 上会直接 StackOverflow进程崩溃连捕获都来不及。改造手段有两种一是把递归改成显式栈的迭代形式前面二叉树的遍历已经示范过二是把递归改成尾递归形式但 C# 编译器不保证尾递归优化所以这条路在 .NET 世界里基本是死路。我一般建议凡是遍历深度可能超过 1000 的算法一律在编码阶段就写成显式栈版本不要指望「也许用户的数据不会那么深」。5.5 种子随机数测试里的随机性既可以是帮手也可以是凶手算法库的单元测试如果依赖随机数最常见的翻车就是「跑一次红一次绿再跑一次反着来」。解决办法是让所有生成随机数据的测试工具方法接受int seed参数。这样出 bug 时只要把当初的种子记下来就能复现同样的输入序列。public static int[] GenerateRandomIntArray(int length, int min, int max, int seed) { var rng new Random(seed); int[] array new int[length]; for (int i 0; i length; i) array[i] rng.Next(min, max 1); return array; }5.6 Nullable 引用类型开启后能抓住一半空引用问题从 .NET 6 开始默认项目模板启用了 Nullable 上下文。在这个上下文里BinaryTreeNodeT?和BinaryTreeNodeT是不同含义的类型前者明确可空后者编译器会警告你「可能为空」。通用库的对外 API 一定要把可空性标清楚这不仅是为了消灭 NullReferenceException更是在给调用方传递设计意图——这个参数允许为空吗这个方法一定返回非空吗这样调用方在编译期就能发现一半的错误。5.7 性能测试基准BenchmarkDotNet 怎么用才不被结果误导最后谈验证。字典、树、堆这类结构跑得快不快不能靠Stopwatch掐表因为 JIT 预热、GC 干扰、第一次调用开销都会污染数据。标准做法是引入 BenchmarkDotNet在 Release 配置、不挂调试器的环境下为每个算法准备小数据和大数据两组用例每组跑几十遍取中位数。但也要提醒BenchmarkDotNet 的汇报数字对通用库来说只是参考不同机器、不同数据分布下结论会翻转所以别把这套基准结果写成 README 里的金科玉律。6. 用泛型委托打通算法策略排序、遍历、状态转移的一键替换技巧通用算法库走到后期追求的不再是「新算法」而是「算法可插拔」。C# 的泛型委托天然适合做策略注入调用方不修改算法代码只传入一个委托就能把比较逻辑、访问顺序、DP 转移规则整个替换掉。以「对同一批数据跑不同排序策略」为例可以定义一个SortStrategyT委托让排序器接受委托并执行。public delegate void SortStrategyT(T[] array) where T : IComparableT; public static class SortStrategyRunner { public static void RunT(T[] data, SortStrategyT strategy) { var copy (T[])data.Clone(); strategy(copy); Console.WriteLine($排序策略执行完毕数组长度{copy.Length}); } } // 调用侧示例 SortStrategyint strategy arr QuickSortExtensions.QuickSort(arr); SortStrategyRunner.Run(new int[] { 5, 2, 8, 1 }, strategy);这里有一个实用技巧是「策略链」对数组先执行一个预处理排序再执行一个后续转换通过组合多个委托实现管道式处理。比如先按字符串长度排序再按字典序排序。这比在排序算法内部做多重AndThen判断更符合单一职责原则。泛型委托的另一个有效应用是图算法的启发式搜索。A* 搜索的启发函数是一个FuncT, double参数调用方传入不同的启发式同一套 A* 骨架既能跑曼哈顿距离也能跑欧氏距离这对路径规划的复用价值很大。这个进阶起点很小但一旦掌握「算法骨架与策略参数分离」整个库的可扩展性会上一个台阶。最后还有一个习惯问题想提醒写完一个算法类不要急着继续下一个先写一个最小可跑的控制台调用打印关键输出确认调用方式顺手、参数命名没歧义再做所谓「优雅优化」。算法库最大的敌人不是性能而是调用方读不懂你的方法签名。以前我用回调函数暴露遍历结果调用方写起来像在拼积木翻车几次后全部改成yield return的迭代器风格再后来连我自己写业务代码都优先选这种「触发式调用」的 API。工具合理用能省力制作用力过猛就成了摆设希望这些经验能帮你在自己的项目里少走几步弯路。本文还有配套的精品资源点击获取