C#性能优化-树形结构递归优化
前言
大家好,我是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的碎碎念】,喜欢的话可以微信扫码关注哟,我们一起来聊聊技术,谈谈职场和人生~

C#性能优化-树形结构递归优化的更多相关文章
- MySQL 性能优化--优化数据库结构之优化数据类型
MySQL性能优化--优化数据库结构之优化数据类型 By:授客 QQ:1033553122 优化数字数据(Numeric Data) l 对于唯一ID或其它可用字符串或数字表示的值,选择 ...
- MySQL 性能优化--优化数据库结构之优化数据大小
MySQL性能优化--优化数据库结构之优化数据大小 By:授客 QQ:1033553122 尽量减少表占用的磁盘空间.通常,执行查询期间处理表数据时,小表占用更少的内存. 表列 l 尽可能使 ...
- mysql数据优化--数据库结构的优化
1,比如存时间类型的就使用int类型 其中mysql的两个函数可以拿来使用 unix_timestamp 将时间日期转化为时间戳
- 递归、嵌套for循环、map集合方式实现树形结构菜单列表查询
有时候, 我们需要用到菜单列表,但是怎么样去实现一个菜单列表的编写呢,这是一重要的问题. 比如我们需要编写一个树形结构的菜单,那么我们可以使用JQuery的zTree插件:http://www.tre ...
- 如何快速优化手游性能问题?从UGUI优化说起
WeTest 导读 本文作者从自身多年的Unity项目UI开发及优化的经验出发,从UGUI,CPU,GPU以及unity特有资源等几个维度,介绍了unity手游性能优化的一些方法. 在之前的文 ...
- Android 性能优化:使用 Lint 优化代码、去除多余资源
前言 在保证代码没有功能问题,完成业务开发之余,有追求的程序员还要追求代码的规范.可维护性. 今天,以“成为优秀的程序员”为目标的拭心将和大家一起精益求精,学习使用 Lint 优化我们的代码. 什么是 ...
- ORACLE性能优化之SQL语句优化
版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 操作环境:AIX +11g+PLSQL 包含以下内容: 1. SQL语句执行过程 2. 优化器及执行计划 3. 合 ...
- PLSQL_性能优化系列04_Oracle Optimizer优化器
2014-09-25 Created By BaoXinjian
- 性能调优之SQL优化
poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:908821478,咨询电话010-845052 ...
- 一:MySQL数据库的性能的影响分析及其优化
MySQL数据库的性能的影响分析及其优化 MySQL数据库的性能的影响 一. 服务器的硬件的限制 二. 服务器所使用的操作系统 三. 服务器的所配置的参数设置不同 四. 数据库存储引擎的选择 五. 数 ...
随机推荐
- nginx配置文件编写及日志文件相关操作
nginx配置文件编写及日志文件相关操作 目录 nginx配置文件编写及日志文件相关操作 nginx主配置文件扩展详解 部署nginx网站 注意事项 Nginx虚拟主机 nginx配置虚拟主机的三种方 ...
- 2023-03-13:给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j], 这样的坡的宽度为 j - i。 找出 A 中的坡的最大宽度,如果不存在,返回 0
2023-03-13:给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j], 这样的坡的宽度为 j - i. 找出 A 中的坡的最大宽度,如果不存在 ...
- 2022-10-26:以下go语言代码输出什么?A:1 3 2;B:1 2 3;C:3 1 2;D:3 2 1。 package main import “fmt“ type temp struc
2022-10-26:以下go语言代码输出什么?A:1 3 2:B:1 2 3:C:3 1 2:D:3 2 1. package main import "fmt" type te ...
- 在 ASP.NET Core Web API 中处理 Patch 请求
一.概述 PUT 和 PATCH 方法用于更新现有资源. 它们之间的区别是,PUT 会替换整个资源,而 PATCH 仅指定更改. 在 ASP.NET Core Web API 中,由于 C# 是一种静 ...
- MVC 三层架构案例详细讲解
MVC 三层架构案例详细讲解 @ 目录 MVC 三层架构案例详细讲解 每博一文案 1. MVC 概述 2. MVC设计思想 3. 三层架构 4. MVC 与 三层架构的关系: 5. 案例举例:用户账户 ...
- 代码随想录算法训练营Day14 二叉树
代码随想录算法训练营 代码随想录算法训练营Day14 二叉树|理论基础 递归遍历 基础知识 二叉树都是通过栈来实现的. 二叉树的种类 在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树. 满 ...
- drf——jwt
jwt原理 使用jwt认证和使用session认证的区别 三段式 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm ...
- 美女同事的烦恼:如何配置 Apache SkyWalking 告警?
小婉 技术部基本上是一个和尚庙,女生非常少,即使有女生也略微有点抽象,小婉就不一样,她气质绝佳. 上午,同事小婉刚才从老板办公室里出来,看上去一脸不悦的样子.为了表示对同事的关(ba)心(gua),我 ...
- Spring配置动态数据库
前言 本文主要介绍使用spring boot 配置多个数据库,即动态数据库 开始搭建 首先创建一个SpringWeb项目--dynamicdb(spring-boot2.5.7) 然后引入相关依赖lo ...
- 揭秘Spring依赖注入和SpEL表达式
摘要:在本文中,我们深入探讨了Spring框架中的属性注入技术,包括setter注入.构造器注入.注解式属性注入,以及使用SpEL表达式进行属性注入. 本文分享自华为云社区<Spring高手之路 ...