平淡幽远 發表於 2026-2-12 11:12:00

从递归到极致优化:树结构构建的性能演进

<h1 id="从递归到极致优化树结构构建的性能演进之路">从递归到极致优化:树结构构建的性能演进之路</h1>
<blockquote>
<p>一次简单的代码优化,性能提升 <strong>超千倍</strong>!本文通过实测数据,揭示树结构构建中隐藏的性能陷阱,并给出最佳实践。</p>
</blockquote>
<hr>
<h2 id="-前言">📖 前言</h2>
<p>在日常开发中,我们经常需要处理树形结构的数据:组织架构、菜单导航、商品分类、文件目录……这些场景都需要将扁平的数据库记录转换为层级树结构。</p>
<p>今天,让我们从最直观的<strong>递归实现</strong>开始,一步步优化到<strong>极致性能</strong>,看看这条优化之路上都有哪些坑。</p>
<p><strong>本文将回答:</strong></p>
<ul>
<li>✅ 构建树的常见方式有哪些?</li>
<li>✅ 为什么有些实现在大数据量下会崩溃?</li>
<li>✅ 如何用一行代码实现数百倍性能提升?</li>
<li>✅ <code>ILookup</code> 为什么这么快?</li>
</ul>
<p><strong>测试环境:</strong></p>
<ul>
<li>.NET 8.0</li>
<li>测试数据:10,000 ~ 100,000 个节点</li>
<li>硬件:普通笔记本电脑。11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz (2.42 GHz) | 16.0 GB</li>
</ul>
<hr>
<h2 id="第一章最直观的实现---递归方式">第一章:最直观的实现 - 递归方式</h2>
<h3 id="11-数据模型">1.1 数据模型</h3>
<p>首先定义我们的节点结构:</p>
<pre><code class="language-csharp">// 扁平数据(数据库记录)
public class Node
{
    public int Id { get; set; }
    public int ParentId { get; set; }// 0 表示根节点
    public string Name { get; set; }
}

// 树结构
public class TreeItem&lt;T&gt;
{
    public T Node { get; set; }
    public List&lt;TreeItem&lt;T&gt;&gt; Children { get; set; } = new();
}
</code></pre>
<h3 id="12-递归实现">1.2 递归实现</h3>
<p>最直观的思路:对每个节点,递归查找它的子节点。</p>
<pre><code class="language-csharp">public static TreeItem&lt;Node&gt; BuildTreeRecursive(List&lt;Node&gt; nodes)
{
    var root = nodes.First(n =&gt; n.ParentId == 0);
    return BuildNode(nodes, root);
}

private static TreeItem&lt;Node&gt; BuildNode(List&lt;Node&gt; allNodes, Node current)
{
    var item = new TreeItem&lt;Node&gt; { Node = current };
   
    // 🔴 关键:查找当前节点的所有子节点
    var children = allNodes.Where(n =&gt; n.ParentId == current.Id).ToList();
   
    foreach (var child in children)
    {
      item.Children.Add(BuildNode(allNodes, child));
    }
   
    return item;
}
</code></pre>
<p><strong>优点:</strong></p>
<ul>
<li>✅ 代码简洁、易理解</li>
<li>✅ 符合树的自然定义</li>
<li>✅ 适合小规模数据(&lt; 1,000 节点)</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ <strong>性能问题</strong>:时间复杂度 <code>O(n²)</code></li>
<li>❌ <strong>栈溢出风险</strong>:深度 &gt; 1,000 层会崩溃</li>
</ul>
<p>让我们先聊聊第二个问题。</p>
<hr>
<h2 id="第二章递归的陷阱---栈溢出">第二章:递归的陷阱 - 栈溢出</h2>
<h3 id="21-问题重现">2.1 问题重现</h3>
<p>测试一个<strong>链表型树</strong>(每个节点只有一个子节点,深度 = 节点数):</p>
<pre><code class="language-csharp">// 生成 10,000 个节点的链表树
var nodes = new List&lt;Node&gt;();
for (int i = 1; i &lt;= 10000; i++)
{
    nodes.Add(new Node
    {
      Id = i,
      ParentId = i - 1,// 形成链表:0 → 1 → 2 → ... → 10000
      Name = $"Node_{i}"
    });
}

var tree = BuildTreeRecursive(nodes);// 💥 StackOverflowException!
</code></pre>
<p><strong>结果:</strong></p>
<pre><code>System.StackOverflowException: Exception of type 'System.StackOverflowException' was thrown.
</code></pre>
<h3 id="22-为什么会栈溢出">2.2 为什么会栈溢出?</h3>
<p>C# 中每次函数调用都会在<strong>调用栈</strong>上分配内存(存储局部变量、返回地址等)。递归深度过大时,栈空间耗尽。</p>
<p><strong>典型限制:</strong></p>
<ul>
<li>Windows:~1,000 层递归(栈大小 1MB)</li>
<li>Linux:~10,000 层(栈大小可调整)</li>
</ul>
<h3 id="23-解决方案一深度保护">2.3 解决方案一:深度保护</h3>
<pre><code class="language-csharp">private const int MAX_DEPTH = 1000;

private static TreeItem&lt;Node&gt; BuildNode(List&lt;Node&gt; allNodes, Node current, int depth)
{
    if (depth &gt; MAX_DEPTH)
      throw new InvalidOperationException("树深度超过限制,建议使用迭代方式");
   
    var item = new TreeItem&lt;Node&gt; { Node = current };
    var children = allNodes.Where(n =&gt; n.ParentId == current.Id).ToList();
   
    foreach (var child in children)
    {
      item.Children.Add(BuildNode(allNodes, child, depth + 1));
    }
   
    return item;
}
</code></pre>
<p>这样至少能提前发现问题,而不是让程序直接崩溃。</p>
<hr>
<h2 id="第三章迭代方式---dfs-与-bfs">第三章:迭代方式 - DFS 与 BFS</h2>
<p>既然递归有深度限制,我们用<strong>迭代</strong>替代递归。</p>
<h3 id="31-dfs深度优先搜索--使用栈">3.1 DFS(深度优先搜索)- 使用栈</h3>
<pre><code class="language-csharp">public static TreeItem&lt;Node&gt; BuildTreeDFS(List&lt;Node&gt; nodes)
{
    var root = new TreeItem&lt;Node&gt;
    {
      Node = nodes.First(n =&gt; n.ParentId == 0)
    };
   
    var stack = new Stack&lt;TreeItem&lt;Node&gt;&gt;();
    stack.Push(root);

    while (stack.Count &gt; 0)
    {
      var current = stack.Pop();
      
      // 🔴 仍然是线性查找
      var children = nodes
            .Where(n =&gt; n.ParentId == current.Node.Id)
            .OrderByDescending(n =&gt; n.Id)
            .ToList();
      
      foreach (var child in children)
      {
            var childItem = new TreeItem&lt;Node&gt; { Node = child };
            current.Children.Add(childItem);
            stack.Push(childItem);
      }
    }

    return root;
}
</code></pre>
<h3 id="32-bfs广度优先搜索--使用队列">3.2 BFS(广度优先搜索)- 使用队列</h3>
<pre><code class="language-csharp">public static TreeItem&lt;Node&gt; BuildTreeBFS(List&lt;Node&gt; nodes)
{
    var root = new TreeItem&lt;Node&gt;
    {
      Node = nodes.First(n =&gt; n.ParentId == 0)
    };
   
    var queue = new Queue&lt;TreeItem&lt;Node&gt;&gt;();
    queue.Enqueue(root);

    while (queue.Count &gt; 0)
    {
      var current = queue.Dequeue();
      
      // 🔴 仍然是线性查找
      var children = nodes
            .Where(n =&gt; n.ParentId == current.Node.Id)
            .OrderBy(n =&gt; n.Id)
            .ToList();
      
      foreach (var child in children)
      {
            var childItem = new TreeItem&lt;Node&gt; { Node = child };
            current.Children.Add(childItem);
            queue.Enqueue(childItem);
      }
    }

    return root;
}
</code></pre>
<h3 id="33-迭代-vs-递归">3.3 迭代 vs 递归</h3>
<table>
<thead>
<tr>
<th>特性</th>
<th>递归</th>
<th>DFS/BFS 迭代</th>
</tr>
</thead>
<tbody>
<tr>
<td>深度限制</td>
<td>❌ ~1000层</td>
<td>✅ 无限制</td>
</tr>
<tr>
<td>代码复杂度</td>
<td>✅ 简洁</td>
<td>⚠️ 稍复杂</td>
</tr>
<tr>
<td>调试难度</td>
<td>⚠️ 较难</td>
<td>✅ 容易</td>
</tr>
<tr>
<td>性能</td>
<td>稍慢(函数调用开销)</td>
<td>稍快</td>
</tr>
</tbody>
</table>
<p><strong>看起来问题解决了?并没有!</strong> 让我们做个性能测试。</p>
<hr>
<h2 id="第四章性能灾难---on-的真相">第四章:性能灾难 - O(n²) 的真相</h2>
<h3 id="41-性能测试">4.1 性能测试</h3>
<p>为了贴合实际情况,我分了三个场景来进行测试:深层树(层级很深,每层节点数量少),超宽树(层级很浅,但是每层节点数量多),平衡树(深度和宽度都相对平均,整体结构有点类似平衡二叉树)。节点数量:10万个(节点少了也看不出什么效果)。<br>
直接贴图看结果(<strong>基于RELEASE模式编译</strong>):</p>
<p><img src="https://img2024.cnblogs.com/blog/1553709/202602/1553709-20260212095539333-1945439353.png"></p>
<blockquote>
<p>核心算法代码上面已经贴出,具体数据构建以及测试代码,这里就不贴出来了,如果有想看的朋友,可以留言。</p>
</blockquote>
<p><strong>🚨 注意:</strong></p>
<ul>
<li>10 万节点用了 <strong>40-50秒</strong>!</li>
<li>递归方式早已栈溢出</li>
</ul>
<h3 id="42-性能分析为什么这么慢">4.2 性能分析:为什么这么慢?</h3>
<p>让我们看看这段代码:</p>
<pre><code class="language-csharp">while (stack.Count &gt; 0)// 遍历所有节点:n 次
{
    var current = stack.Pop();
   
    // 每次都要扫描整个列表!
    var children = nodes.Where(n =&gt; n.ParentId == current.Node.Id);// O(n)
    // ...
}
</code></pre>
<p><strong>时间复杂度分析:</strong></p>
<ul>
<li>外层循环:<code>O(n)</code> - 遍历所有节点</li>
<li>内层 <code>Where</code>:<code>O(n)</code> - 每次扫描整个列表</li>
<li><strong>总复杂度:<code>O(n²)</code></strong></li>
</ul>
<p><strong>具体计算:</strong></p>
<ul>
<li>50,000 个节点 → 需要 <strong>25 亿次</strong>比较操作!</li>
<li>100,000 个节点 → 需要 <strong>100 亿次</strong>比较操作!</li>
</ul>
<p>这就是性能杀手!</p>
<h3 id="43-核心问题">4.3 核心问题</h3>
<pre><code class="language-csharp">// 🔴 问题代码:在循环中使用 Where
foreach (var node in nodes)// n 次
{
    var children = allNodes.Where(n =&gt; n.ParentId == node.Id);// 每次 O(n)
    // 总复杂度:O(n²)
}
</code></pre>
<p><strong>这是实际项目中最常见的性能陷阱!</strong></p>
<hr>
<h2 id="第五章极致优化---ilookup-的魔法">第五章:极致优化 - ILookup 的魔法</h2>
<h3 id="51-核心思路空间换时间">5.1 核心思路:空间换时间</h3>
<p>既然每次都要查找 <code>ParentId == xxx</code> 的节点,为什么不<strong>提前建立索引</strong>?</p>
<pre><code class="language-csharp">// 🔴 原来:每次查找都要扫描整个列表
var children = nodes.Where(n =&gt; n.ParentId == parentId);// O(n)

// ✅ 现在:提前建立索引,查找变成 O(1)
var lookup = nodes.ToLookup(n =&gt; n.ParentId);// 一次性建立索引 O(n)
var children = lookup;// 直接查找 O(1)
</code></pre>
<h3 id="52-什么是-ilookup">5.2 什么是 ILookup?</h3>
<p><code>ILookup&lt;TKey, TElement&gt;</code> 是 LINQ 提供的一个接口,类似于 <code>Dictionary&lt;TKey, List&lt;TValue&gt;&gt;</code>,但:</p>
<ol>
<li><strong>一键多值</strong>:自动处理一对多关系</li>
<li><strong>不会抛异常</strong>:查询不存在的键返回空集合</li>
<li><strong>基于哈希表</strong>:查找时间 <code>O(1)</code></li>
<li><strong>只读不可变</strong>:线程安全</li>
</ol>
<p><strong>语法:</strong></p>
<pre><code class="language-csharp">// 创建
ILookup&lt;int, Node&gt; lookup = nodes.ToLookup(n =&gt; n.ParentId);

// 查询(即使 999 不存在也不会异常)
IEnumerable&lt;Node&gt; children = lookup;// 返回空集合

// 检查是否存在
bool exists = lookup.Contains(999);

// 遍历所有分组
foreach (var group in lookup)
{
    Console.WriteLine($"ParentId: {group.Key}, Count: {group.Count()}");
}
</code></pre>
<h3 id="53-优化后的代码">5.3 优化后的代码</h3>
<p>只需改<strong>一行</strong>!</p>
<pre><code class="language-csharp">public static TreeItem&lt;Node&gt; BuildTreeDFSOptimized(IEnumerable&lt;Node&gt; nodes)
{
    var nodeList = nodes.ToList();
   
    // ✅ 核心优化:提前建立索引
    var lookup = nodeList.ToLookup(n =&gt; n.ParentId);// 👈 只加了这一行!
   
    var root = new TreeItem&lt;Node&gt;
    {
      Node = nodeList.First(n =&gt; n.ParentId == 0)
    };
   
    var stack = new Stack&lt;TreeItem&lt;Node&gt;&gt;();
    stack.Push(root);

    while (stack.Count &gt; 0)
    {
      var current = stack.Pop();
      
      // ✅ 从 O(n) 变成 O(1)
      var children = lookup// 👈 改这里
            .OrderByDescending(n =&gt; n.Id)
            .ToList();
      
      foreach (var child in children)
      {
            var childItem = new TreeItem&lt;Node&gt; { Node = child };
            current.Children.Add(childItem);
            stack.Push(childItem);
      }
    }

    return root;
}
</code></pre>
<p><strong>改动对比:</strong></p>
<pre><code class="language-diff">public static TreeItem&lt;Node&gt; BuildTreeDFS(List&lt;Node&gt; nodes)
{
+   var lookup = nodes.ToLookup(n =&gt; n.ParentId);
    // ...
    while (stack.Count &gt; 0)
    {
      var current = stack.Pop();
-       var children = nodes.Where(n =&gt; n.ParentId == current.Node.Id);
+       var children = lookup;
    }
}
</code></pre>
<p>就这么简单!</p>
<hr>
<h2 id="第六章性能对决---实测数据">第六章:性能对决 - 实测数据</h2>
<h3 id="61-完整测试">6.1 完整测试</h3>
<p>测试代码:</p>
<pre><code class="language-csharp">internal class TreeBuilder
{
    private const int MAX_RECURSION_DEPTH = 1000; // 最大递归深度限制

    #region 1. 递归方式构建树(原始 - 每次线性查找,带深度保护)
    public static TreeItem&lt;Node&gt; BuildTreeRecursive(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      return BuildRecursiveNode(nodeList, rootNode, 0);
    }

    private static TreeItem&lt;Node&gt; BuildRecursiveNode(List&lt;Node&gt; allNodes, Node currentNode, int depth)
    {
      // 深度保护:超过限制时抛出异常,避免真正的栈溢出
      if (depth &gt; MAX_RECURSION_DEPTH)
      {
            throw new StackOverflowException($"递归深度超过限制 ({MAX_RECURSION_DEPTH})");
      }

      var treeItem = new TreeItem&lt;Node&gt; { Node = currentNode };
      
      // O(n) 线性查找子节点
      var children = allNodes.Where(n =&gt; n.ParentId == currentNode.Id).ToList();
      
      foreach (var child in children)
      {
            treeItem.Children.Add(BuildRecursiveNode(allNodes, child, depth + 1));
      }
      
      return treeItem;
    }
    #endregion

    #region 2. DFS方式构建树(原始 - 使用栈,每次线性查找)
    public static TreeItem&lt;Node&gt; BuildTreeDFS(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      var root = new TreeItem&lt;Node&gt; { Node = rootNode };
      var stack = new Stack&lt;TreeItem&lt;Node&gt;&gt;();
      stack.Push(root);

      while (stack.Count &gt; 0)
      {
            var current = stack.Pop();
            
            // O(n) 线性查找子节点
            var children = nodeList.Where(n =&gt; n.ParentId == current.Node.Id).OrderByDescending(n =&gt; n.Id).ToList();
            
            foreach (var child in children)
            {
                var childItem = new TreeItem&lt;Node&gt; { Node = child };
                current.Children.Add(childItem);
                stack.Push(childItem);
            }
      }

      return root;
    }
    #endregion

    #region 3. BFS方式构建树(原始 - 使用队列,每次线性查找)
    public static TreeItem&lt;Node&gt; BuildTreeBFS(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      var root = new TreeItem&lt;Node&gt; { Node = rootNode };
      var queue = new Queue&lt;TreeItem&lt;Node&gt;&gt;();
      queue.Enqueue(root);

      while (queue.Count &gt; 0)
      {
            var current = queue.Dequeue();
            
            // O(n) 线性查找子节点
            var children = nodeList.Where(n =&gt; n.ParentId == current.Node.Id).OrderBy(n =&gt; n.Id).ToList();
            
            foreach (var child in children)
            {
                var childItem = new TreeItem&lt;Node&gt; { Node = child };
                current.Children.Add(childItem);
                queue.Enqueue(childItem);
            }
      }

      return root;
    }
    #endregion

    #region 4. 优化的递归方式(使用字典索引 - O(n),带深度保护)
    public static TreeItem&lt;Node&gt; BuildTreeRecursiveOptimized(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var lookup = nodeList.ToLookup(n =&gt; n.ParentId);
      
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      return BuildRecursiveOptimizedNode(lookup, rootNode, 0);
    }

    private static TreeItem&lt;Node&gt; BuildRecursiveOptimizedNode(ILookup&lt;int, Node&gt; lookup, Node currentNode, int depth)
    {
      // 深度保护
      if (depth &gt; MAX_RECURSION_DEPTH)
      {
            throw new StackOverflowException($"递归深度超过限制 ({MAX_RECURSION_DEPTH})");
      }

      var treeItem = new TreeItem&lt;Node&gt; { Node = currentNode };
      
      // O(1) 通过索引查找子节点
      var children = lookup;
      
      foreach (var child in children)
      {
            treeItem.Children.Add(BuildRecursiveOptimizedNode(lookup, child, depth + 1));
      }
      
      return treeItem;
    }
    #endregion

    #region 5. 优化的DFS(使用字典索引 - O(n))
    public static TreeItem&lt;Node&gt; BuildTreeDFSOptimized(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var lookup = nodeList.ToLookup(n =&gt; n.ParentId);
      
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      var root = new TreeItem&lt;Node&gt; { Node = rootNode };
      var stack = new Stack&lt;TreeItem&lt;Node&gt;&gt;();
      stack.Push(root);

      while (stack.Count &gt; 0)
      {
            var current = stack.Pop();
            
            // O(1) 通过索引查找子节点
            var children = lookup.OrderByDescending(n =&gt; n.Id).ToList();
            
            foreach (var child in children)
            {
                var childItem = new TreeItem&lt;Node&gt; { Node = child };
                current.Children.Add(childItem);
                stack.Push(childItem);
            }
      }

      return root;
    }
    #endregion

    #region 6. 优化的BFS(使用字典索引 - O(n))
    public static TreeItem&lt;Node&gt; BuildTreeBFSOptimized(IEnumerable&lt;Node&gt; nodes)
    {
      var nodeList = nodes.ToList();
      var lookup = nodeList.ToLookup(n =&gt; n.ParentId);
      
      var rootNode = nodeList.FirstOrDefault(n =&gt; n.ParentId == 0) ?? throw new InvalidOperationException("No root node found");
      
      var root = new TreeItem&lt;Node&gt; { Node = rootNode };
      var queue = new Queue&lt;TreeItem&lt;Node&gt;&gt;();
      queue.Enqueue(root);

      while (queue.Count &gt; 0)
      {
            var current = queue.Dequeue();
            
            // O(1) 通过索引查找子节点
            var children = lookup.OrderBy(n =&gt; n.Id).ToList();
            
            foreach (var child in children)
            {
                var childItem = new TreeItem&lt;Node&gt; { Node = child };
                current.Children.Add(childItem);
                queue.Enqueue(childItem);
            }
      }

      return root;
    }
    #endregion      
}
</code></pre>
<h3 id="62-测试结果">6.2 测试结果</h3>
<p><img src="https://img2024.cnblogs.com/blog/1553709/202602/1553709-20260212103744350-1431402698.png"></p>
<h3 id="63-性能分析">6.3 性能分析</h3>
<p><strong>时间复杂度对比:</strong></p>
<table>
<thead>
<tr>
<th>实现方式</th>
<th>建索引</th>
<th>查找子节点</th>
<th>总复杂度</th>
</tr>
</thead>
<tbody>
<tr>
<td>原始方式</td>
<td>-</td>
<td>O(n)</td>
<td><strong>O(n²)</strong></td>
</tr>
<tr>
<td>优化方式</td>
<td>O(n) 一次性</td>
<td>O(1)</td>
<td><strong>O(n)</strong></td>
</tr>
</tbody>
</table>
<p><strong>内存占用:</strong></p>
<ul>
<li>原始方式:<code>O(n)</code> - 只存储节点</li>
<li>优化方式:<code>O(n)</code> - 节点 + Lookup 索引(额外约 20-30%)</li>
</ul>
<pre><code class="language-text">为什么集中算法的最终内存都是一样的?
1.✅ 输入数据相同:所有算法都使用同一个 nodes 列表(100,000 个 Node)
2.✅ 输出结构相同:所有算法都构建相同的树结构(TreeItem 对象)
3.✅ 最终内存相同:树的总内存 = 节点数 × 每个 TreeItem 的大小

// 构建后的树内存
100,000 个 TreeItem&lt;Node&gt; 对象
├─ 每个 TreeItem: ~80 字节
│    ├─ Node 引用: 8 字节
│    └─ Children List: 72 字节(包含数组、计数等)
└─ 总计: ~7.8 MB
</code></pre>
<p><strong>结论:</strong></p>
<ul>
<li>✅ 用 20-30% 内存换取 <strong>超过千倍</strong>的性能提升</li>
<li>✅ 绝对值得!</li>
</ul>
<hr>
<h2 id="第七章深入理解-ilookup">第七章:深入理解 ILookup</h2>
<h3 id="71-为什么-ilookup-这么快">7.1 为什么 ILookup 这么快?</h3>
<p><strong>内部实现原理(简化版):</strong></p>
<pre><code class="language-csharp">// ToLookup 内部大致做了这些事
public static ILookup&lt;TKey, TElement&gt; ToLookup&lt;TKey, TElement&gt;(
    this IEnumerable&lt;TElement&gt; source,
    Func&lt;TElement, TKey&gt; keySelector)
{
    // 1. 创建哈希表
    var groups = new Dictionary&lt;TKey, List&lt;TElement&gt;&gt;();
   
    // 2. 遍历一次,建立索引 - O(n)
    foreach (var item in source)
    {
      var key = keySelector(item);
      
      if (!groups.ContainsKey(key))
            groups = new List&lt;TElement&gt;();
      
      groups.Add(item);
    }
   
    // 3. 返回只读的 Lookup
    return new Lookup&lt;TKey, TElement&gt;(groups);
}

// 查找时 - O(1)
public IEnumerable&lt;TElement&gt; this
{
    get
    {
      return groups.ContainsKey(key)
            ? groups
            : Enumerable.Empty&lt;TElement&gt;();// 不存在返回空集合
    }
}
</code></pre>
<p><strong>关键点:</strong></p>
<ol>
<li>使用 <code>Dictionary</code> 作为底层存储(哈希表)</li>
<li>查找时间:<code>O(1)</code> - 直接通过哈希值定位</li>
<li>不存在的键返回空集合,不抛异常</li>
</ol>
<h3 id="72-ilookup-vs-dictionary-vs-groupby">7.2 ILookup vs Dictionary vs GroupBy</h3>
<pre><code class="language-csharp">var nodes = GetNodes();

// 1. GroupBy - 每次查询都重新分组(慢!)
var groups = nodes.GroupBy(n =&gt; n.ParentId);
var children1 = groups.FirstOrDefault(g =&gt; g.Key == 100)?.ToList();// O(n) 每次都分组

// 2. Dictionary - 需要手动处理一对多
var dict = new Dictionary&lt;int, List&lt;Node&gt;&gt;();
foreach (var node in nodes)
{
    if (!dict.ContainsKey(node.ParentId))
      dict = new List&lt;Node&gt;();
    dict.Add(node);
}
var children2 = dict.ContainsKey(100) ? dict : new List&lt;Node&gt;();// O(1) 但需要判断

// 3. Lookup - 一行搞定,自动处理一对多(快!)
var lookup = nodes.ToLookup(n =&gt; n.ParentId);
var children3 = lookup;// O(1) 且不需要判断
</code></pre>
<p><strong>对比表:</strong></p>
<table>
<thead>
<tr>
<th>特性</th>
<th>GroupBy</th>
<th>Dictionary</th>
<th><strong>ILookup</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>建立索引</td>
<td>延迟执行</td>
<td>手动构建</td>
<td>✅ 一行代码</td>
</tr>
<tr>
<td>一键多值</td>
<td>✅</td>
<td>❌ 需手动</td>
<td>✅</td>
</tr>
<tr>
<td>查找速度</td>
<td>O(n) 每次分组</td>
<td>O(1)</td>
<td>✅ O(1)</td>
</tr>
<tr>
<td>键不存在</td>
<td>返回 null</td>
<td>抛异常</td>
<td>✅ 返回空集合</td>
</tr>
<tr>
<td>代码简洁性</td>
<td>⚠️</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>推荐度</strong></td>
<td>临时分组</td>
<td>一对一映射</td>
<td>✅ <strong>父子关系</strong></td>
</tr>
</tbody>
</table>
<hr>
<h2 id="第八章实战应用">第八章:实战应用</h2>
<h3 id="81-常见场景">8.1 常见场景</h3>
<h4 id="场景1订单-订单项">场景1:订单-订单项</h4>
<pre><code class="language-csharp">// ❌ 慢
foreach (var order in orders)
{
    order.Items = orderItems.Where(oi =&gt; oi.OrderId == order.Id).ToList();// O(n²)
}

// ✅ 快
var itemsLookup = orderItems.ToLookup(oi =&gt; oi.OrderId);
foreach (var order in orders)
{
    order.Items = itemsLookup.ToList();// O(n)
}
</code></pre>
<h4 id="场景2部门-员工">场景2:部门-员工</h4>
<pre><code class="language-csharp">var empLookup = employees.ToLookup(e =&gt; e.DepartmentId);
foreach (var dept in departments)
{
    dept.Employees = empLookup.ToList();
}
</code></pre>
<h4 id="场景3分类-商品">场景3:分类-商品</h4>
<pre><code class="language-csharp">var prodLookup = products.ToLookup(p =&gt; p.CategoryId);
foreach (var category in categories)
{
    category.Products = prodLookup.ToList();
}
</code></pre>
<h3 id="82-识别性能陷阱">8.2 识别性能陷阱</h3>
<p><strong>搜索你的代码库,找这些模式:</strong></p>
<pre><code class="language-csharp">// 🔍 危险信号
foreach + Where
for + FirstOrDefault
while + Any

// ✅ 优化建议
ToLookup +
ToDictionary +
</code></pre>
<p><strong>CodeReview 清单:</strong></p>
<ul class="contains-task-list">
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"><label> 循环中是否有 <code>Where</code> 查询?</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"><label> 是否多次遍历同一个集合?</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"><label> 数据量是否可能超过 1,000?</label></li>
<li class="task-list-item"><input class="task-list-item-checkbox" disabled="" type="checkbox"><label> 能否用 <code>ToLookup</code> 或 <code>ToDictionary</code> 优化?</label></li>
</ul>
<hr>
<h2 id="第九章完整对比---六种实现">第九章:完整对比 - 六种实现</h2>
<h3 id="91-所有实现方式">9.1 所有实现方式</h3>
<table>
<thead>
<tr>
<th>实现方式</th>
<th>时间复杂度</th>
<th>深度限制</th>
<th>代码复杂度</th>
<th>推荐场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>递归(原始)</td>
<td>O(n²)</td>
<td>❌ ~1000层</td>
<td>⭐⭐⭐⭐⭐</td>
<td>❌ 不推荐</td>
</tr>
<tr>
<td>递归(优化)</td>
<td>O(n)</td>
<td>❌ ~1000层</td>
<td>⭐⭐⭐⭐⭐</td>
<td>小规模(&lt;1000节点)</td>
</tr>
<tr>
<td>DFS(原始)</td>
<td>O(n²)</td>
<td>✅ 无限制</td>
<td>⭐⭐⭐⭐</td>
<td>❌ 不推荐</td>
</tr>
<tr>
<td>DFS(优化)</td>
<td>O(n)</td>
<td>✅ 无限制</td>
<td>⭐⭐⭐⭐</td>
<td>✅ 深树、链表</td>
</tr>
<tr>
<td>BFS(原始)</td>
<td>O(n²)</td>
<td>✅ 无限制</td>
<td>⭐⭐⭐⭐</td>
<td>❌ 不推荐</td>
</tr>
<tr>
<td>BFS(优化)</td>
<td>O(n)</td>
<td>✅ 无限制</td>
<td>⭐⭐⭐⭐</td>
<td>✅ 宽树、层级</td>
</tr>
</tbody>
</table>
<hr>
<h3 id="92--完整实现">9.2完整实现</h3>
<p>六种实现方式的代码上文已经给出,这里不再重复了。</p>
<h2 id="第十章总结与最佳实践">第十章:总结与最佳实践</h2>
<h3 id="101-核心收获">10.1 核心收获</h3>
<blockquote>
<p><strong>"优化查找,而非遍历"</strong></p>
</blockquote>
<ol>
<li><strong>性能陷阱:</strong> 循环中的 <code>Where</code> → <code>O(n²)</code></li>
<li><strong>优化武器:</strong> <code>ToLookup</code> → <code>O(n)</code></li>
<li><strong>一行改动:</strong> 性能提升数百倍</li>
<li><strong>递归限制:</strong> 深度 &gt; 1000 层会栈溢出</li>
</ol>
<h3 id="102-最佳实践">10.2 最佳实践</h3>
<pre><code class="language-csharp">// ✅ 推荐写法
var lookup = nodes.ToLookup(n =&gt; n.ParentId);
foreach (var node in nodes)
{
    var children = lookup;
    // ...
}

// ❌ 避免写法
foreach (var node in nodes)
{
    var children = allNodes.Where(n =&gt; n.ParentId == node.Id);// O(n²)
}
</code></pre>
<h3 id="103-何时使用-ilookup">10.3 何时使用 ILookup</h3>
<table>
<thead>
<tr>
<th>场景</th>
<th>是否使用</th>
</tr>
</thead>
<tbody>
<tr>
<td>一对多关系(父子、分类等)</td>
<td>✅ 强烈推荐</td>
</tr>
<tr>
<td>数据量 &gt; 1,000</td>
<td>✅ 必须使用</td>
</tr>
<tr>
<td>需要频繁查找</td>
<td>✅ 强烈推荐</td>
</tr>
<tr>
<td>临时分组(只用一次)</td>
<td>⚠️ 考虑 GroupBy</td>
</tr>
<tr>
<td>一对一映射</td>
<td>⚠️ 考虑 Dictionary</td>
</tr>
</tbody>
</table>
<h3 id="104-记忆口诀">10.4 记忆口诀</h3>
<blockquote>
<p><strong>循环里有 Where,性能就完蛋;<br>
提前建 Lookup,速度飞上天!</strong></p>
</blockquote>
<hr>
<h2 id="-参考资料">📚 参考资料</h2>
<ul>
<li>LINQ ToLookup 官方文档</li>
<li>算法时间复杂度分析</li>
</ul>
<hr>
<h2 id="一点题外话">一点题外话</h2>
<p>DFS(Depth-First Search 深度优先搜索) 和BFS(Breadth-First Search 广度优先搜索),这两个术语都来自图论,核心是搜索(在数据结构中寻找特定节点或路径)。<br>
但我们在构建树时,只是按深度优先(或者广度优先)的顺序访问节点并建立父子关系,并没有“搜索”任何目标。严格来说,这应该叫深度优先遍历(Depth-First Traversal)或深度优先构建(Depth-First Construction),广度优先同理。<br>
但为什么大家都习惯叫DFS/BFS?<br>
这是典型的算法术语泛化现象:</p>
<ul>
<li>起源:图论中的DFS确实是为了搜索/遍历</li>
<li>发展:人们发现“用栈处理树形结构”这种模式非常通用</li>
<li>泛化:任何使用栈来处理树/图结构的操作,都被口语化地称为“用DFS的方式”</li>
<li>固化:面试、技术文章、开源代码都这么用,形成了约定俗成</li>
</ul>
<p>类似例子还有很多:</p>
<ul>
<li>“用BFS爬取网页” → 其实只是广度优先遍历URL</li>
<li>“递归遍历文件夹” → 并不是在搜索什么</li>
<li>“二叉树的前/中/后序遍历” → 其实也是DFS的特例,但很少人说“DFS二叉树”</li>
</ul>
<p><strong>为什么没人纠正?</strong></p>
<p>因为<strong>沟通成本</strong>高于<strong>精确性成本</strong>。当你面试时说“我用DFS把列表转成树”,所有人都秒懂。如果说“我用深度优先遍历的迭代方式,借助栈后进先出的特性来构建层级结构”——太啰嗦了,反而影响沟通效率。</p>
<blockquote>
<p>但在实际开发中,“用DFS构建树”已经是<strong>行业黑话</strong>,大家都接受这个语义泛化后的含义了。</p>
</blockquote>
<h2 id="-结语">🎯 结语</h2>
<p>一次简单的代码优化,性能提升 <strong>超过千倍</strong>。</p>
<p>在实际项目中,这样的优化机会比比皆是。关键是:</p>
<ol>
<li><strong>测量</strong>:用 Stopwatch 测量,找到瓶颈</li>
<li><strong>分析</strong>:识别 O(n²) 的查找操作</li>
<li><strong>优化</strong>:用 ToLookup/ToDictionary 建立索引</li>
<li><strong>验证</strong>:再次测量,确认提升</li>
</ol>
<p><strong>记住:</strong></p>
<blockquote>
<p><strong>过早优化是万恶之源,但该优化时别手软!</strong></p>
</blockquote>
<hr><br><br>
来源:https://www.cnblogs.com/diamondhusky/p/19605488
頁: [1]
查看完整版本: 从递归到极致优化:树结构构建的性能演进