使用方法 ToDictionary() 来优化查询时间复杂度:O(N*M) -> O(1*M)【C# 基础】

〇、前言

对于一个对象 List,通过调用 .Where() 来筛选数据,当数据量比较大时,就会大大拖慢程序运行效率,从秒级直接到分钟级。

即 O(N*M) 中,N 的基数非常大时,单次查询就比较慢,循环 M 次的累加时间将逐渐扩大。

那么有没有更好的方法来做筛选,本文将进行简单尝试,主要涉及三种方法:ToDictionary()、ToLookup()、Span<T>,供参考。

一、前因后果

1.1 为什么通过调用 .Where() 来筛选数据会比较慢?

通过调用 .Where() 来筛选数据,每次查询都会进行一次线性扫描(时间复杂度为 O(N))。

例如以下代码示例。

复制代码
// 两个对象,通过 params2 进行关联,将 params1 赋值给 params3
// 这是一个对象数量有限的列表
var detail_objs = new List<ObjectModel_1>();  // 属性:params2,params3
string sql_getdata = "select params1,params2 from tableName where flag>0";
// 这是一个上百万的对象列表
var objectlist = dapperFactory.Query<ObjectModel>(sql_getdata).ToList();
ObjectModel obj = null;
foreach (var item in detail_objs)
{
    obj = objectlist.Where(a => a.params2 == item.params2).FirstOrDefault();
    if (obj != null)
    {
        item.params3 = obj.params1;
    }
}

如果 detail_objs 有 M 条数据,总的时间复杂度就会飙升到 O(N * M),这会导致极其严重的性能问题。

1.2 一个简单的解决方案:使用 ToDictionary 或 ToLookup 建立索引

解决方案的核心思路是,用空间换时间:将列表转换为哈希字典结构,把查找的时间复杂度降为 O(1)

先准备简单的测试数据。

复制代码
using System;
using System.Collections.Generic;
using System.Linq;

public class Employee
{
    public string Name { get; set; }
    public string Department { get; set; }
}

var employees = new List<Employee>
{
    new Employee { Name = "张三", Department = "研发部" },
    new Employee { Name = "李四", Department = "销售部" },
    new Employee { Name = "王五", Department = "研发部" },
    new Employee { Name = "赵六", Department = "销售部" },
    new Employee { Name = "钱七", Department = "研发部" },
};

1.2.1 关于 ToDictionary() 的简单示例

复制代码
// 三个不同的【实现方案】
// 【一】【仅适用于 key 不重复的场景】否则抛异常
Dictionary<string, Employee> nameDict = employees
    .ToDictionary(e => e.Name);  // 假设 Name 唯一
Console.WriteLine(nameDict["张三"].Department);  // 输出:研发部

// 【二】先 GroupBy,再 ToDictionary 【实现一对多索引】
Dictionary<string, List<Employee>> deptDict = employees
    .GroupBy(e => e.Department)
    .ToDictionary(
        g => g.Key,              // key = 部门名称
        g => g.ToList()          // value = 该部门的员工列表
    );

// 【三】手动遍历构建(性能更好,只遍历一次)
var deptDict = new Dictionary<string, List<Employee>>();
foreach (var emp in employees)
{
    if (!deptDict.TryGetValue(emp.Department, out var list))
    {
        list = new List<Employee>();
        deptDict[emp.Department] = list;
    }
    list.Add(emp);
}

// 【使用】
// 按 key 查找 ------ 【返回该部门的所有员工】
var devTeam = deptDict["研发部"];
foreach (var emp in devTeam)
{
    Console.WriteLine(emp.Name);  // 输出:张三、王五、钱七
}

// 【直接查找不存在的 key】 ------ 会抛 KeyNotFoundException!
// var hrTeam = deptDict["人事部"];  // 抛异常

// 【正确做法】用 TryGetValue
if (deptDict.TryGetValue("人事部", out var hrTeam))
{
    foreach (var emp in hrTeam)
        Console.WriteLine(emp.Name);
}
else
{
    Console.WriteLine("人事部没有员工");  // 输出:人事部没有员工
}

// 【指定 Value Selector(只索引某个属性)】
// 等价于 ToLookup 的 element selector
Dictionary<string, List<string>> nameByDept = employees
    .GroupBy(e => e.Department)
    .ToDictionary(
        g => g.Key,
        g => g.Select(e => e.Name).ToList()  // 只取 Name
    );
foreach (var name in nameByDept["研发部"])
{
    Console.WriteLine(name);  // 输出:张三、王五、钱七
}

1.2.2 关于 ToLookup() 的简单示例

复制代码
// 【按 Department 建立索引】
ILookup<string, Employee> deptLookup = employees.ToLookup(e => e.Department);

// 【使用索引查询】
// 按 key 查找 ------ 返回该部门的所有员工
var devTeam = deptLookup["研发部"];
foreach (var emp in devTeam)
{
    Console.WriteLine(emp.Name);  // 输出:张三、王五、钱七
}

// 【查找不存在的 key ------ 返回空集合】
// (不会抛异常!这是比 GroupBy + ToDictionary 好的地方)
var hrTeam = deptLookup["人事部"];
Console.WriteLine(hrTeam.Count());  // 输出:0

// 【指定 Element Selector(只索引某个属性)】
// 第二个参数指定 value 取哪个字段
ILookup<string, string> nameByDept = employees.ToLookup(
    e => e.Department,   // key selector
    e => e.Name          // element selector
);
foreach (var name in nameByDept["研发部"])
{
    Console.WriteLine(name);  // 输出:张三、王五、钱七
}

// 【自定义比较器(忽略大小写等)】
ILookup<string, Employee> lookup = employees.ToLookup(
    e => e.Department,
    StringComparer.OrdinalIgnoreCase  // 忽略大小写
);

1.3 数据库层面的优化建议

除了代码层面的算法优化,处理上百万条数据时,数据库查询本身也可能成为瓶颈。

以下是可以采取的两个优化点:

1)只查询需要的字段

只查询业务逻辑中用到的字段,其中 Model 也按照这个来设计,这避免了加载不必要的列,减少了内存分配和网络传输开销。

2)在数据库端直接 JOIN

如果目标数据列表可以存入临时表或表值参数,最彻底的做法是直接在 SQL 语句中完成关联(JOIN),避免将超百万条数据全部拉到 C# 内存中处理。

1.4 内存与 GC 压力优化(Span<T>)

由于数据量较大时,ToList() 会在堆上分配大量内存。如果这段代码处于高频调用的业务路径中,可以尝试以下两种方案。

1)如果 employees 对象列表,在应用生命周期内不发生变化,可以考虑将其缓存起来(如:使用 MemoryCache 或静态变量),避免每次都去查库和分配内存。

2)如果在极高频且对 GC 极其敏感的场景下,可以考虑使用 Span<T> 配合原生 for 循环来替代 LINQ,以实现零内存分配的遍历,但对于您当前的场景,使用 ToDictionary 已经能解决 99% 的性能问题。

**下边是一个基于 Dapper 的简单示例。**Dapper 是微 ORM,它通过反射或表达式树直接将 DataReader 的行映射到目标类型。Dapper 完美支持 struct 和 record struct。

复制代码
// 1. 定义 readonly record struct (C# 10+)
// 位置参数会自动生成构造函数和属性,Dapper 可以直接通过构造函数映射
public readonly record struct DataStruct(string dataid, string databm, string guigexh);

// 2. 使用 Dapper 直接查询为 struct 数组
using var conn = new SqlConnection("数据库连接字符串");
string sql = "SELECT shujuid, shujubm FROM tableName";

// 直接查询为 struct
// 注意:建议调用 .ToArray(),因为 Span<T> 需要数组(连续内存),而 Query 返回的是 IEnumerable/List
DataStruct[] dataArray = conn.Query<DataStruct>(sql).ToArray();

// 3. 配合 Span 进行零 GC 遍历/构建字典
Span<DataStruct> span = dataArray.AsSpan();
var dict = new Dictionary<string, DataStruct>(span.Length);
for (int i = 0; i < span.Length; i++)
{
    ref readonly var item = ref span[i];
    dict[item.dataid] = item; // 值类型拷贝,无堆分配
}
相关推荐
IT_陈寒2 小时前
我又被JavaScript的隐式类型转换坑了
前端·人工智能·后端
用户8356290780512 小时前
Python Word 转 PDF 和 PDF 转 Word 指南
后端·python
用户8356290780512 小时前
如何使用 Python 加密和保护 Word 文档
后端·python
他们叫我秃子3 小时前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go
SomeB1oody3 小时前
【RustyML入门】2.6. 线性判别分析
开发语言·后端·机器学习·rust·教程
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
Yao8063 小时前
SpringAI + Ollama + DeepSeek + RAG 搭建 AI 航线规划系统
后端
大勇前进3 小时前
SQL Server 分页查询多种写法对比:哪种性能最高?
后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十六):附件管理、增加根据文件后缀生成 SVG 文件图标的接口
前端·后端·go