首页 > 编程语言 >C#性能优化-树形结构递归优化

C#性能优化-树形结构递归优化

时间:2023-08-07 20:55:17浏览次数:35  
标签:TreeNode 递归 C# 节点 树形 堆栈 new 优化 Children

前言

大家好,我是wacky,最近在工作中遇到一个有趣的问题,同事反馈说WPF中有一个树形结构的集合,在加载时会直接报堆栈溢出,一直没时间(懒得)看,导致很久了也没人解决掉。于是,组长就把这个"艰巨"的任务交给了我。作为新人中的"高手",必然要义不容辞地接受挑战喽,废话不多说,走起。

分析

由于同事此前已经定位到出现问题的代码段,所以到我手中时要省掉不少功夫。打开代码后看了下,原来是这个树形结构使用了典型的递归操作来对每个节点的数据进行更新,在数据量一般时一切正常,但是当数据量达到几万个节点后,这段代码会直接报堆栈溢出的错误。

代码示例如下所示,已简化:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tree
{
    internal class TreeNode
    {
        public int Value { get; set; }
        public List<TreeNode> Children { get; set; }   

        public TreeNode(int value)
        {
            Value = value;
            Children = new List<TreeNode>();
        }
    }
}
// See https://aka.ms/new-console-template for more information
// 创建一个树形结构
using Tree;

internal class Program
{
    static void Main(string[] args)
    {
        TreeNode root = new TreeNode(1);
        TreeNode node2 = new TreeNode(2);
        TreeNode node3 = new TreeNode(3);
        TreeNode node4 = new TreeNode(4);
        TreeNode node5 = new TreeNode(5);
        TreeNode node6 = new TreeNode(6);

        root.Children.Add(node2);
        root.Children.Add(node3);
        node2.Children.Add(node4);
        node2.Children.Add(node5);
        node3.Children.Add(node6);

        PrintTreeNode(root);
        Console.Read();
    }

    static void PrintTreeNode(TreeNode node)
    {
        if (node == null)
        {
            return;
        }
        Console.WriteLine(node.Value);
        foreach (TreeNode child in node.Children)
        {
            PrintTreeNode(child);
        }
    }
}

上述代码我们定义了一个树形结构的类,并加入对应节点,然后使用递归的方式将所有节点输出,在数据量达到前文提到的数量级时就会发生堆栈溢出。

 既然是堆栈溢出,那么我们就需要考虑减少堆栈溢出的方式,也就是降低栈的深度。这里我们需要分析下为什么递归会导致堆栈溢出?顺便复习一下部分计算机基础知识点。

 在计算机中,函数调用是通过栈(stack)这种数据结构去实现的,每当程序在调用一次函数时,就会进行压栈(push),每当函数返回后,才会进行出栈(pop)。但是栈的大小本身并不是无限的,加上我们使用C# CLR给的默认分配也不会很大,通常是在1MB左右,这样就会出现函数调用次数过多时,超出栈本身的大小,导致堆栈溢出。

而递归调用,一般都是在到达最后的结束点时,才会一层一层返回每个函数执行的结果。在本次例子中,树形结构存在几万个父子节点,就会导致递归层数过深,函数在栈中无法及时出栈,进而报错。

 到这一步时,我们的思路就开始明朗了,既然递归会导致堆栈过深,那我们不妨把递归进行改写,使用其他方式来进行遍历。在通常的解法中,存在两种方式:尾递归优化和迭代。

尾递归优化

什么是尾递归优化?我们先说说什么是尾递归,尾递归是指在一个函数中,所有递归的调用都出现在函数的末尾,也就是递归的那一句在函数执行的最后,或代码路径在最后一句出现,我们就可以称之为尾递归。所以如果我们的递归调用本身不是尾递归的时候,可以通过改写,让它变成尾递归的方式。

 为什么尾递归可以进行优化?原因是堆栈需要保存每次调用的返回地址及当时所有的局部变量状态,期间堆栈空间是无法释放的。使用尾递归堆栈可以不用保存上次的函数返回地址/各种状态值,而方法遗留在堆栈上的数据完全可以释放掉,这是尾递归优化的核心思想。

回到我们本次的例子中来,我们的代码已经是尾递归的形式了,但还会导致溢出,那这时我们就需要使用另外一种方法迭代去解决问题了。

迭代

迭代,在本质上就是循环,由于我们已经提到了递归在函数调用的过程中不会对栈进行弹出,那么我们就可以用迭代来模拟入栈出栈的方式来对遍历做优化。我们可以先定义一个栈用来存放所有父子节点,然后对父节点进行压栈,并使用while循环来模拟所有遍历操作,当栈不为空时就一直执行。在循环中我们可以对已经压栈的数据进行弹栈,做完逻辑操作后,再对其子节点进行压栈,一直重复此过程,直到所有节点都弹栈完成。

相关代码如下所示:

// See https://aka.ms/new-console-template for more information
// 创建一个树形结构
using Tree;

internal class Program
{
    static void Main(string[] args)
    {
        TreeNode root = new TreeNode(1);
        TreeNode node2 = new TreeNode(2);
        TreeNode node3 = new TreeNode(3);
        TreeNode node4 = new TreeNode(4);
        TreeNode node5 = new TreeNode(5);
        TreeNode node6 = new TreeNode(6);

        root.Children.Add(node2);
        root.Children.Add(node3);
        node2.Children.Add(node4);
        node2.Children.Add(node5);
        node3.Children.Add(node6);

        IterativeTraversal(root);
        Console.Read();
    }

    static void IterativeTraversal(TreeNode root)
    {
        if (root == null)
        {
            return;
        }
        //定义一个栈,存放所有的树节点
        Stack<TreeNode> stack = new Stack<TreeNode>();
        //把根节点压栈
        stack.Push(root);
        while (stack.Count > 0)
        {
            TreeNode node = stack.Pop();
            Console.WriteLine(node.Value);
            //遍历完父节点后,将子节点压栈
            for (int i = node.Children.Count - 1; i >= 0; i--)
            {
                stack.Push(node.Children[i]);
            }
        }
    }
}

在这种方式中,我们每遍历一层节点,都会对栈进行释放,这样就保证了已经在栈中的层级不会太深,进而解决了堆栈溢出的问题。

总结

探寻好思路后,我和同事做了尝试,将代码改写完成后,遍历几万个节点一切正常,且不会出现卡死之类的其他问题,完美解决!虽然我们本次性能优化的思路并不复杂,代码写起来也相对简单,但背后其实蕴含着比较深刻的计算机原理知识。我们在日常工作中也需要多重视基础知识,包括数据结构和算法,这样才可以在遇到难以解决的问题时游刃有余,诸君共勉!

 

本文首发于我的公众号【wacky的碎碎念】,喜欢的话可以微信扫码关注哟,我们一起来聊聊技术,谈谈职场和人生~

 

标签:TreeNode,递归,C#,节点,树形,堆栈,new,优化,Children
From: https://www.cnblogs.com/wackysoft/p/17612699.html

相关文章

  • CSP模拟15
    TheMorningStar统计$x,y,x-y,x+y$开$longlong$Ntarsis'Set考场降智删除数实质是降低排名.显然答案有单调性,直接二分答案.每次减小排名.判断是否合法.Code#include<cstdio>#defineintlonglongusingnamespacestd;constintN=2e5+5;inlineintrea......
  • 从 Word 复制表格到 Excel 如何保留多行单元格
    思路是在Word中将换行符替换为一串自定义的文本,然后将其粘贴到Excel中,再在Excel中将特殊文本替换回换行。具体可参考下面的链接。Retainmulti-linecellswhenpastingWordtableintoExcel-MicrosoftCommunityHowdoIcopyWordtablesintoExcelwithoutsplitt......
  • TextBrewer:融合并改进了NLP和CV中的多种知识蒸馏技术、提供便捷快速的知识蒸馏框架、
    TextBrewer:融合并改进了NLP和CV中的多种知识蒸馏技术、提供便捷快速的知识蒸馏框架、提升模型的推理速度,减少内存占用TextBrewer是一个基于PyTorch的、为实现NLP中的知识蒸馏任务而设计的工具包,融合并改进了NLP和CV中的多种知识蒸馏技术,提供便捷快速的知识蒸馏框架,用于以较低的......
  • c#集合去重&排序常用方法
     list与数组转Hashset&SortedSet//创建hashset去重varhashSet=newHashSet<int>(){1,1,2,2};Console.WriteLine("HashSet:"+String.Join(",",hashSet));//HashSet:1,2//创建list包含重复元素varints=newList<int>{1,1,3,3,2,2};//创建数组转......
  • 【WCH蓝牙系列芯片】-基于CH582开发板按键控制LED灯
    ---------------------------------------------------------------------------------------------------------------------------------------本文主要介绍CH582的GPIO的基础外设的使用,并且利用GPIO外设点亮LED灯和按键扫描功能。将两者结合,实现按键控制LED灯的状态。<控制LED......
  • CSP-J/S第一轮初赛 ~持续更新~
    CSP-J/S初赛2022更新的初赛知识汇总基础算法链表插入删除数据,操作数据O(1),遍历是O(n),可以进行动态调整。指针指向的是上下节点,链表储存数据下一个节点上一个节点。动态调整:插入一定量的节点,进行调整。插入节点:考虑信息覆盖(这些信息后面是否会再被用到)。寻找和读取比较慢......
  • ceph-deploy部署ceph集群 nautilus 14.2.22
    规划主机名IP地址系统ceph版本ceph硬盘大小组件规划master192.168.1.60CentOS7.9ceph-15.2.10sdb100GOSD、MOD、MDS、MGR主节点node01192.168.1.70CentOS7.9ceph-15.2.10sdb100GOSD从节点node02192.168.1.80CentOS7.9ceph-15.2.10sdb100......
  • Ajax技术、MTV和MVC的概念
    一、Ajax技术1、AJAX(AsynchronousJavascriptAndXML)翻译成中文就是“异步Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML,现在更多使用json数据)。局部刷新、一步提交2、作用前端技术,把前端的数据提交到后端的。Ajax......
  • QSurface
    QSurface#include<QSurface> PublicTypesenumSurfaceClass {Window,Offscreen}enumSurfaceType {RasterSurface,OpenGLSurface,RasterGLSurface,OpenVGSurface,VulkanSurface}PublicFunctionsvirtual~QSurface()virtualQSur......
  • Windows c++检测笔记本是否处于睡眠状态
    最近遇到一个问题,程序需要检测电脑是否处于睡眠状态。一开始使用的方式是在WindowProc里监听WM_POWERBROADCAST消息,对PBT_APMSUSPEND``PBT_APMRESUMEAUTOMATIC消息做处理。但是实际测试中发现,这种方法在台式机中运行良好,但是放到笔记本电脑里就不行,系统休眠时监听不到WM_POWERBRO......