使用 LINQ 进行高效图遍历 - 消除递归

2024-03-11

今天我打算实现一种方法来遍历任意深度的图并将其展平为单个可枚举。相反,我先做了一些搜索,发现了这个:

public static IEnumerable<T> Traverse<T>(this IEnumerable<T> enumerable, Func<T, IEnumerable<T>> recursivePropertySelector)
{
    foreach (T item in enumerable)
    {
        yield return item;

        IEnumerable<T> seqRecurse = recursivePropertySelector(item);

        if (seqRecurse == null) continue;
        foreach (T itemRecurse in Traverse(seqRecurse, recursivePropertySelector))
        {
            yield return itemRecurse;
        }
    }
}

从理论上讲,这看起来不错,但在实践中,我发现它的性能比使用等效的手写代码(当情况出现时)来浏览图表并执行任何需要完成的操作要差得多。我怀疑这是因为在这个方法中,对于它返回的每个项目,堆栈都必须展开到某个任意深度的级别。

我还怀疑,如果消除递归,该方法的运行效率会更高。我也不太擅长消除递归。

有谁知道如何重写这个方法来消除递归?

谢谢你的帮助。

编辑: 非常感谢所有详细的回复。我尝试对原始解决方案与 Eric 的解决方案进行基准测试,对比不使用枚举器方法而是使用 lambda 进行递归遍历,奇怪的是,lambda 递归比其他两种方法中的任何一种都快得多。

class Node
{
    public List<Node> ChildNodes { get; set; } 

    public Node()
    {
        ChildNodes = new List<Node>();
    }
}

class Foo
{
    public static void Main(String[] args) 
    {
        var nodes = new List<Node>();
        for(int i = 0; i < 100; i++)
        {
            var nodeA = new Node();
            nodes.Add(nodeA);
            for (int j = 0; j < 100; j++)
            {
                var nodeB = new Node();
                nodeA.ChildNodes.Add(nodeB);
                for (int k = 0; k < 100; k++)
                {
                    var nodeC = new Node();
                    nodeB.ChildNodes.Add(nodeC);
                    for(int l = 0; l < 12; l++)
                    {
                        var nodeD = new Node();
                        nodeC.ChildNodes.Add(nodeD);
                    }
                }
            }
        }            

        nodes.TraverseOld(node => node.ChildNodes).ToList();
        nodes.TraverseNew(node => node.ChildNodes).ToList();

        var watch = Stopwatch.StartNew();
        nodes.TraverseOld(node => node.ChildNodes).ToList();
        watch.Stop();
        var recursiveTraversalTime = watch.ElapsedMilliseconds;
        watch.Restart();
        nodes.TraverseNew(node => node.ChildNodes).ToList();
        watch.Stop();
        var noRecursionTraversalTime = watch.ElapsedMilliseconds;

        Action<Node> visitNode = null;
        visitNode = node =>
        {
            foreach (var child in node.ChildNodes)
                visitNode(child);
        };

        watch.Restart();
        foreach(var node in nodes)
            visitNode(node);
        watch.Stop();
        var lambdaRecursionTime = watch.ElapsedMilliseconds;
    }
}

其中TraverseOld是原始方法,TraverseNew是Eric的方法,显然lambda就是lambda。

在我的机器上,TraverseOld 需要 10127 毫秒,TraverseNew 需要 3038 毫秒,lambda 递归需要 1181 毫秒。

与立即执行相比,枚举器方法(带有yield 返回)花费的时间是通常的 3 倍,这是典型的情况吗?还是这里发生了其他事情?


First off, you are absolutely correct; if the graph has n nodes of average depth d then the naive nested iterators yield a solution which is O(n*d) in time, and O(d) in stack. If d is a large fraction of n then this can become an O(n2) algorithm, and if d is large then you can blow the stack entirely.

如果您对嵌套迭代器的性能分析感兴趣,请参阅前 C# 编译器开发人员 Wes Dyer 的博客文章:

http://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators http://blogs.msdn.microsoft.com/wesdyer/2007/03/23/all-about-iterators

dasblinkenlight 的解决方案是标准方法的变体。我通常会这样编写程序:

public static IEnumerable<T> Traverse<T>(
    T root, 
    Func<T, IEnumerable<T>> children)
{
    var stack = new Stack<T>();
    stack.Push(root);
    while(stack.Count != 0)
    {
        T item = stack.Pop();
        yield return item;
        foreach(var child in children(item))
            stack.Push(child);
    }
}

然后如果你有多个根:

public static IEnumerable<T> Traverse<T>(
    IEnumerable<T> roots, 
    Func<T, IEnumerable<T>> children)
{
    return from root in roots 
           from item in Traverse(root, children)
           select item ;
}

现在,请注意,遍历是not如果您有一个高度互连的图或循环图,这就是您想要的!如果您有一个带有向下箭头的图表:

          A
         / \
        B-->C
         \ /
          D

那么遍历是A,B,D,C,D,C,D。如果你有一个循环图或互连图,那么你想要的是传递闭包.

public static IEnumerable<T> Closure<T>(
    T root, 
    Func<T, IEnumerable<T>> children)
{
    var seen = new HashSet<T>();
    var stack = new Stack<T>();
    stack.Push(root);

    while(stack.Count != 0)
    {
        T item = stack.Pop();
        if (seen.Contains(item))
            continue;
        seen.Add(item);
        yield return item;
        foreach(var child in children(item))
            stack.Push(child);
    }
}

此变化仅产生之前未产生过的物品。

我也不太擅长消除递归。

我写了很多关于消除递归的方法以及一般的递归编程的文章。如果您对这个主题感兴趣,请参阅:

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/ http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

尤其:

http://blogs.msdn.com/b/ericlippert/archive/2005/08/01/recursion-part-two-unrolling-a-recursive-function-with-an-explicit-stack.aspx http://blogs.msdn.com/b/ericlippert/archive/2005/08/01/recursion-part-two-unrolling-a-recursive-function-with-an-explicit-stack.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/04/recursion-part- Three-building-a-dispatch-engine.aspx http://blogs.msdn.com/b/ericlippert/archive/2005/08/04/recursion-part-three-building-a-dispatch-engine.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx http://blogs.msdn.com/b/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 LINQ 进行高效图遍历 - 消除递归 的相关文章

  • .pdbs 会减慢发布应用程序的速度吗?

    如果 dll 中包含 pdb 程序调试 文件 则行号将出现在引发的任何异常的堆栈跟踪中 这会影响应用程序的性能吗 这个问题与发布与调试 即优化 无关 这是关于拥有 pdb 文件的性能影响 每次抛出异常时都会读取 pdb 文件吗 加载程序集时
  • 如何使用 C# 以编程方式编辑 Power BI Desktop 文档参数或数据源?

    我有一个在 Power BI Desktop 中内置的报告模板 并保存为 pbix 或 pbit 文件 该模板使用DirectQuery SQL数据库作为数据源 而服务器地址和数据库名称被提取到参数中 还有一个参数包含一个ReportId
  • 如何调整 Windows 窗体以适应任何屏幕分辨率?

    我知道这是重复的问题 但我检查了所有其他相关问题 他们的答案没有帮助 结果仍然与屏幕截图 2 中所示相同 我是 C Windows 窗体新手 如截图1所示 我有Form1有一些控件 每组控件都放在一个面板中 我在 PC1 中设计了应用程序
  • 关闭整数的最右边设置位

    我只需要关闭最右边的设置位即可 我的方法是找到最右边位的位置 然后离开该位 我编写这段代码是为了这样做 int POS int n int p 0 while n if n 2 0 p else break n n 2 return p i
  • 判断串口是普通COM还是SPP

    我正在寻找一种方法来确定 COM 是标准 COM 还是 SPP COM 也称为 COM 设备的电缆替换蓝牙适配器 我有一个可以在 USB COM gt USB 和蓝牙下工作的设备 并且蓝牙接口可以与 SPP 一起工作 我目前正在使用Syst
  • 使用 PHP 创建图表并导出为 PDF

    我正在寻找有关使用 PHP 创建图表的建议 我还希望能够将这些图表导出到 PDF 文档 我目前正在使用谷歌图表 但我不喜欢将我的所有信息发送到谷歌的想法 我更喜欢自己的托管解决方案 我见过很多 Flash 解决方案 但我不知道有什么方法可以
  • 检测 TextBox 中的 Tab 键按下

    I am trying to detect the Tab key press in a TextBox I know that the Tab key does not trigger the KeyDown KeyUp or the K
  • 为什么 std::function 不是有效的模板参数,而函数指针却是?

    我已经定义了名为的类模板CallBackAtInit其唯一目的是在初始化时调用函数 构造函数 该函数在模板参数中指定 问题是模板不接受std function作为参数 但它们接受函数指针 为什么 这是我的代码 include
  • “没有合适的默认构造函数可用”——为什么会调用默认构造函数?

    我已经查看了与此相关的其他一些问题 但我不明白为什么在我的情况下甚至应该调用默认构造函数 我可以只提供一个默认构造函数 但我想了解它为什么这样做以及它会产生什么影响 error C2512 CubeGeometry no appropria
  • 从点云检测平面集

    我有一组点云 我想测试3D房间中是否有角落 所以我想讨论一下我的方法 以及在速度方面是否有更好的方法 因为我想在手机上测试它 我将尝试使用霍夫变换来检测线 然后我将尝试查看是否有三条线相交 并且它们也形成了两个相交的平面 如果点云数据来自深
  • 在 C 语言中替换宏内的宏

    我正在尝试使代码部分可重用 我下面的评论片段没有达到我想要的效果 define NAME ABC define LOG SIZE NAME LEN 我想LOG SIZE决心ABC LEN 我尝试过使用 但没能让它发挥作用 LOG SIZE在
  • MSChart 控件中的自定义 X/Y 网格线

    我有一个带有简单 2D 折线图的 C Windows 窗体 我想向其中添加自定义 X 或 Y 轴标记 并绘制自定义网格线 例如 以突出显示的颜色 虚线 我查看了 customLabels 属性 但这似乎覆盖了我仍然想显示的默认网格 这是为了
  • 将 dll 注册到 GAC 或从 ASP.NET 中的 bin 文件夹引用它们是否更好

    如果答案是 视情况而定 您能否提供一个简短的解释 GAC 旨在包含以下组件跨多个应用程序共享 如果是这种情况 您应该对程序集进行强命名并向 GAC 注册 如果不是 请将程序集保留为私有程序集并将其作为项目 dll 引用进行引用 PS 没有真
  • WinForms - 加载表单时如何使用 PaintEventArgs 运行函数?

    我试图理解图形 在 Graphics FromImage 文档中 它有这样的示例 private void FromImageImage PaintEventArgs e Create image Image imageFile Image
  • 时间:2019-03-17 标签:c#TimerStopConfusion

    我想通过单击按钮时更改文本颜色来将文本框文本设置为 闪烁 我可以让文本按照我想要的方式闪烁 但我希望它在闪烁几次后停止 我不知道如何在计时器触发几次后让它停止 这是我的代码 public Form1 InitializeComponent
  • 在 C 中使用 #define 没有任何价值

    If a define没有任何价值地使用 例如 define COMMAND SPI 默认值是0吗 不 它的评估结果为零 从字面上看 该符号被替换为空 然而 一旦你有了 define FOO 预处理器条件 ifdef FOO现在将是真的 另
  • MSVC编译器下使用最大成员初始化联合

    我正在尝试初始化一个LARGE INTEGER在 C 库中为 0 确切地说是 C 03 以前 初始化是 static LARGE INTEGER freq 0 在 MinGW 下它产生了一个警告 缺少成员 LARGE INTEGER Hig
  • 如何在 Razor 编辑视图中显示选中的单选按钮 Asp net core mvc

    尽管 Razor 视图中的 Asp 网络核心代码 model List
  • Emacs C++,打开相应的头文件

    我是 emacs 新手 我想知道 是否有在头文件 源文件和相应的源文件 头文件之间切换的快捷方式 是否有像通用 emacs 参考卡那样的参考卡 Thanks There s ff find other file 您可以使用以下方法将其绑定到
  • IDisposable 的显式实现

    虽然有很多关于IDisposable在 SO 上找到 我还没有找到答案 我通常遵循这样的做法 当我的一个班级拥有一个IDisposable对象然后它也实现IDisposable并打电话Dispose在拥有的对象上 然而最近我遇到了一个类 它

随机推荐

  • 我可以有一个指向可分配数组组件中的项目的指针吗?

    我有一个用户定义的类型vector 在另一种类型中 我有一个可分配的向量数组 我想要一个指向这个可分配数组中的单个向量的指针 所以我想我会这样做 type another type type vector allocatable targe
  • 我可以使用一个登录页面通过 Spring 3.0 Security 重定向不同的页面吗? [复制]

    这个问题在这里已经有答案了 可能的重复 使用 spring security 根据用户角色设置自定义登录后目标 https stackoverflow com questions 2818055 setting custom post lo
  • 在同一事务中插入和删除?

    我有一个包含一些数据的 Temp Table 根据 Temp Table 中的数据 我将从其他表中删除相关行 然后将 Temp table 中的所有数据插入到 table1 中 就像下面的例子一样 我可以以什么方式在 Server2 Tab
  • 如何使用 SSH 在另一台服务器上运行 PHP 中的 CLI 命令?

    我正在尝试在 PHP 中运行 CLI 命令 但在不同的服务器上 为了在另一台服务器上运行命令 我使用的是linuxssh命令 为了在 PHP 中运行 CLI 命令 我使用exec 这有效 output exec cut d f1 etc p
  • 虽然places.getLatLng()返回null,但places.getName()不返回null

    我一直在尝试从自动完成中单击一个位置后获取经纬度 奇怪的是places getName 工作正常但是place getLatLng 返回空值 我应该怎么做才能解决这个问题我是谷歌地图和地点 API 的新手 protected void on
  • 使用 launch4j 和 maven 包装 java 命令行应用程序

    我想使用 maven 和 launch4j 将基于 java 的命令行应用程序及其所有依赖项包装到单个 exe 文件中 现在我已经阅读了所有类似的问题 比如this one https stackoverflow com questions
  • 更改 Typescript 映射类型中的属性名称

    我有一个 Typescript 对象的集合 如下所示 SomeData prop1 string prop2 number 我需要最终得到一些如下所示的对象 type SomeMoreData prop1Change string prop
  • C# 字符串大于或等于代码字符串

    我试图让我的代码能够比较字符串是否大于或小于 10 但它无法正常工作 即使该值小于 10 它也会写入 10 或更多 int result string1 CompareTo 10 if result lt 0 Console WriteLi
  • 设置 UILocalNotification 的超时时间(一段时间后将其从锁屏和通知中心删除)

    我想设置一个UILocalNotification五分钟后 它将从锁定屏幕和通知中心消失 如果用户不点击它 我可以设置通知超时吗 或者也许会触发另一个通知来删除它 我相信 Facebook 是通过发送无声推送通知 http www g8pr
  • 正则表达式和冒号 (:)

    我有以下代码 这个想法是检测整个单词 bool contains Regex IsMatch Hello1 Hello2 bHello b yields false bool contains Regex IsMatch Hello Hel
  • 如何对 ObservableCollection 进行排序? [复制]

    这个问题在这里已经有答案了 我试过了 Persons from i in Persons orderby i Age select i 但我无法转换 LinqsSystem Linq IOrderedEnumerable to Observ
  • 减少玻璃鱼原木的线宽

    有谁知道如何减少玻璃鱼原木上每条线的宽度 它似乎包含很多我不需要的信息 下面是一行的示例 2012 03 04T16 00 09 537 0000 INFO oracle glassfish3 1 javax enterprise syst
  • Google 跟踪代码管理器不跟踪图像和图标上的链接点击

    在 Google 跟踪代码管理器中 我将其设置为跟踪包含特定类的元素的点击数据 并在 Google Analytics 中记录事件 它似乎适用于文本链接 但如果链接内有另一个用于图像 图标等的标签 我就会遇到问题 例如 以下内容可以正常工作
  • 前端计算价格不安全?

    我想知道是否可以操纵在前端完成的价格计算 我读了很多关于 JavaScript 价格计算器的文章 其中的业务逻辑仅在客户端 但对安全性却一无所知 考虑以下场景 React 应用程序有一个组件 表单 它根据其子组件 表单输入 的状态 用户交互
  • 请求映射中的双星号

    请求映射中出现双星号意味着什么 例如 RequestMapping value welcome method RequestMethod GET public ModelAndView welcomePage 一般来说 星号 通配符角色 意
  • DIO 响应解码问题

    我在用Dio为了使HTTP request var dio Dio var response await dio get URL final responseBody json decode response data final stat
  • 如何使用 Android 中的加速度计值计算特定轴的旋转速率

    我正在开发一个简单的游戏 其中角色仅沿 Y 轴上下移动 目前我正在使用加速度计读数来更改角色的 Y 速度 游戏运行良好 但最大的问题是你必须保持设备水平才能正常玩游戏 我真正想要的是仅当沿 Y 轴的旋转速率发生变化时才更改角色的 Y 速度
  • Azure SQL 频繁连接超时

    我们在 Azure 上运行一个 Web 应用程序 2 个实例 由 SQL Azure 数据库支持 在任何给定时间都有 50 150 个用户使用该网站 数据库以 S2 性能级别运行 DTU 平均约为 20 然而 每天都有几次我的日志中突然出现
  • 缓存从 pcap 捕获的数据包

    这是对此的后续问题 重建数据包以通过 pcap 注入 https stackoverflow com questions 8193281 rebuilding a packet to inject via pcap 我想要实现的目标 fun
  • 使用 LINQ 进行高效图遍历 - 消除递归

    今天我打算实现一种方法来遍历任意深度的图并将其展平为单个可枚举 相反 我先做了一些搜索 发现了这个 public static IEnumerable