前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >记一次带层级结构列表数据计算性能优化

记一次带层级结构列表数据计算性能优化

作者头像
guokun
发布于 2020-09-03 07:45:28
发布于 2020-09-03 07:45:28
63200
代码可运行
举报
文章被收录于专栏:销声匿迹销声匿迹
运行总次数:0
代码可运行

1、背景

  最近,负责一个类财务软件数据计算的性能优化工作。先说下=这项目的情况,一套表格,几十张表格,每张表格数据都是层级结构的,通过序号确定父子级关系,如1,1.1,1.1.1,1.1.2,1.1.3,1.2,1.2.1,1.2.2,1.3.。。。而且,列表数据带表内编辑功能,就跟Excel体验一样。没错,你猜对了,不出意外的,这是个CS项目,前端采用WPF,在计算之前,对应表格数据已经拉取到前端内存中,通过MVVM双向绑定到UI列表。计算公式分横向和纵向,叶子级的都是横向计算,如金额 = 单价 * 数量;父级的纵向计算,如 1.金额 = 1.1金额 + 1.2金额 + 1.3金额。。。很明显,只能先计算叶子级,再逐级往上计算父级,而且是自底向上的。

  自然而然的,你会想到递归,而且之前项目中也是这么整的,递归调用自底向上计算。问题是,每张表格数据量都很大,实际环境中,最多的出现了30W条。我们按照递归调用顺序去分析下这个过程:首先,从30W里找根级(虽然最终需要自底向上计算,但系统本身它是不知道谁是子级的,只能由父级往下去逐个找),找到之后,根据根级Id从30W数据中找到其所有子级,循环每个子级,根据每个子级ID,从30W数据找到该子级对应的子级。。。只到最终叶子级,可以计算了,该层递归出栈,计算其父级,父级完了计算父级的父级。。。

  那么,问题来了:首先,递归本身就是极耗空间的,这么大数据量,内存浪费更是了不得,而且,数据检索也把CPU给占尽了;更严重的,这还只是一张,系统有30多张表格。。。实际测试也发现,计算一开启,i5 CPU,8G的机器,CPU直接打满,内存也飙升(要不是Windows对进程内存做限制,我估计内存也打满了,实际测试出现过OutOfMemory异常。。。)。运气好,30W数据,花个大几分钟能算完,运气不好就等个大几分钟,OutOfMemory。。。这么搞,肯定是不行的,开发机都不行,更别提客户环境千差万别,有些客户机配置很恶劣,老旧XP,2G内存,32位。。。

2、方案

  一把辛酸一把泪,问题给出来了,自然是要解决。上述方案的问题在于,查找每个节点,都需要从30W数据里边遍历,能不能访问每个节点时候,不用去遍历这30W数据呢?本身,这30W数据就是一个树状结构,假如事先把这30W数据构造成一颗树,那么只需要按照后续遍历,岂不就避免了频繁的30W遍历?

  好,确定了用树遍历解决,那是用普通树,还是二叉树(是不是好奇,为什么会想到这个问题)?答案是,二叉树,因为最开始,我就用的普通树,但测试发现,虽然性能极大提升(几分钟到几十秒),但还是有点儿难以接受,用VS性能探查器发现,普通树需要跟踪某级别未访问节点(通俗点儿说就是,访问完某个节点,需要从同根的子级中遍历寻找下一个未访问的节点),这个特别耗时,假如该级节点特别多,则会遇到上述同样的问题,从大批量数据中检索,虽然这个数据范围已经比30W极大减少了。用二叉树,就左子树右子树,是不需要这个的。

3、实现

  首先,树节点的定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 /// <summary>
    /// 二叉树节点
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class TreeNode<T>
        where T : Data
    {
        public TreeNode()
        {
            this.Children = new List<T>();
        }

        public TreeNode(T data)
            : this()
        {
            this.Data = data;
        }

        /// <summary>
        /// 节点对应数据节点
        /// </summary>
        public T Data { get; set; }

        /// <summary>
        /// 树节点
        /// </summary>
        public TreeNode<T> Parent { get; set; }

        /// <summary>
        /// 左子树
        /// </summary>
        public TreeNode<T> Left { get; set; }

        /// <summary>
        /// 右子树
        /// </summary>
        public TreeNode<T> Right { get; set; }

        /// <summary>
        /// 该节点对应业务节点的子业务节点集合
        /// </summary>
        public List<T> Children { get; private set; }
    }

  节点,节点数据,左子树节点,右子树节点,父级节点,比较简单。这里唯一需要说明的是,节点对应的子级数据集合,因为原始数据,是一个普通树,最终我们是要把它转化为一个二叉树的,转化之后,我们需要记录某个数据节点它对应的原始子级数据集合是哪些,便于后续跟踪和计算。

  好,二叉树节点定义好了,对二叉树进行处理的前提,是先要构造二叉树。数据结构中,有一种普通树状结构转为二叉树的方式是,第一个子节点作为左子树,剩余兄弟节点,都作为上一个子节点的右子树存在,也就是说,左子树子节点,右子树兄弟节点。假如我们有这么几个数据节点:1,1.1,1.1.1,1.1.2,1.1.3,1.2,1.2.1,1.2.2,1.3,则构建完成之后,二叉树应该是这样子的:

  具体代码,怎么实现呢?这里先说下前提,系统中数据是按照对应序号排序的,比如1,1.1,1.1.1,1.1.2,1.1.3,1.2,1.2.1,1.2.2,1.3。那么,从一维列表构建二叉树的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <summary>
        /// 根据实体列表构建二叉树
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="list"></param>
        /// <returns></returns>
        public static TreeNode<T> GenerateTree<T>(List<T> list, Func<T, bool> rootCondition, Func<T, T, bool> parentCondition)
            where T : Data
        {
            if (!list.Any())
            {
                return null;
            }

            var rootData = list.FirstOrDefault(x => rootCondition(x));
            TreeNode<T> root = new TreeNode<T>(rootData);
            Stack<TreeNode<T>> stackParentNodes = new Stack<TreeNode<T>>();
            stackParentNodes.Push(root);
            foreach (var item in list)
            {
                if (item == rootData)
                {
                    continue;
                }

                TreeNode<T> parent = stackParentNodes.Peek();
                while (!parentCondition(item, parent.Data))
                {
                    stackParentNodes.Pop();

                    if (stackParentNodes.Count == 0)
                    {
                        stackParentNodes.Push(root);
                        parent = root;
                        break;
                    }

                    parent = stackParentNodes.Peek();
                }

                var currentNode = new TreeNode<T>(item);
                if (parent.Left == null)
                {
                    parent.Left = currentNode;
                    currentNode.Parent = parent;
                }
                else
                {
                    if (parent.Left.Right == null)
                    {
                        parent.Left.Right = currentNode;
                        currentNode.Parent = parent.Left;
                    }
                    else
                    {
                        parent.Left.Right.Parent = currentNode;
                        currentNode.Right = parent.Left.Right;
                        currentNode.Parent = parent.Left;
                        parent.Left.Right = currentNode;
                    }
                }

                parent.Children.Add(item);

                stackParentNodes.Push(currentNode);
            }

            return root;
        }

  这段代码,参考网上的,出处我已经找不到了,如果哪位网友看见了,麻烦告诉我,我注明出处。说下这段代码的核心思想,首先有个父级栈,用来记录上次遍历的节点及其父节点,然后开始遍历数据列表中每条记录,在这过程中,从父节点栈中找该节点对应的父节点,不匹配的元素直接出栈,只到找到对应父节点。找到之后,如果父节点左子树不存在,直接将当前节点挂在左子树,如果左子树存在,则该节点是当前左子树的兄弟节点,需要作为该左子树的右子树去挂。这时候有个问题,如果左子树的右子树不存在,直接挂在左子树的右子树就可以,如果存在,则需要将其挂为右子树,左子树的原右子树变成当前节点的右子树。因为遍历时候,是按照顺序来的,这么一来,则兄弟节点在树上挂的顺序,是逆序的,最终效果会如下:

  有点儿拧,大家知道是那么回事儿就行了。树构建好了,接下来就是遍历计算。很明显,对于这种计算,是需要后续遍历的,则实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <summary>
        /// 后续遍历二叉树进行计算
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="root"></param>
        /// <param name="leaveCompute"></param>
        /// <param name="branchCompute"></param>
        public static void Compute<T>(TreeNode<T> root, Action<T> leafCompute, Action<T, List<T>> branchCompute)
             where T : Data
        {
            if (root == null)
            {
                return;
            }

            TreeNode<T> currentNode = null,
                preNode = null;
            Stack<TreeNode<T>> stackParentNodes = new Stack<TreeNode<T>>();
            stackParentNodes.Push(root);
            while (stackParentNodes.Any())
            {
                currentNode = stackParentNodes.Peek();
                if ((currentNode.Left == null && currentNode.Right == null)
                    || (preNode != null) && (preNode == currentNode.Left || preNode == currentNode.Right))
                {
                    preNode = currentNode;
                    if (currentNode.Children.Any())
                    {
                        currentNode.Data.LeavesCount = currentNode.Children.Sum(x => x.LeavesCount);
                        branchCompute?.Invoke(currentNode.Data, currentNode.Children);
                    }
                    else
                    {
                        currentNode.Data.LeavesCount = 1;
                        leafCompute?.Invoke(currentNode.Data);
                    }
                    stackParentNodes.Pop();
                }
                else
                {
                    if (currentNode.Right != null)
                    {
                        stackParentNodes.Push(currentNode.Right);
                    }
                    if (currentNode.Left != null)
                    {
                        stackParentNodes.Push(currentNode.Left);
                    }
                }
            }
        }

  核心思想是记录遍历过程中的父级节点及上次遍历的节点。当前节点需要被访问的条件是,当前节点左子树右子树都为空(叶子节点)或者上次访问的节点是本节点的子节点,否则当前节点不应该被访问,而是将其右子树左子树进栈以备考察。这个是全量计算的方式。还有一种情况是,改变了其中某个单元格,例如上述,我改了1.1.3其中的单价,则这时候也需要计算,但计算应该仅限于本级节点及父节点,你非要全量计算也没问题,无非性能低点儿。那么,计算本级和父级的功能,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 /// <summary>
        /// 计算指定节点极其父级
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="node"></param>
        /// <param name="branchCompute"></param>
        public static void ComputeParent<T>(TreeNode<T> node, Action<T> leafCompute, Action<T, List<T>> branchCompute)
            where T : Data
        {
            if (node == null)
            {
                return;
            }

            TreeNode<T> currentNode = node;

            if (node.Children.Any())
            {
                currentNode.Data.LeavesCount = currentNode.Children.Sum(x => x.LeavesCount);
                branchCompute?.Invoke(currentNode.Data, currentNode.Children);
            }
            else
            {
                currentNode.Data.LeavesCount = 1;
                currentNode.Data.IsLeaf = true;
                leafCompute?.Invoke(currentNode.Data);
            }

            while (currentNode != null)
            {
                var parentNode = currentNode.Parent;
                if (parentNode != null && parentNode.Left == currentNode)
                {
                    branchCompute?.Invoke(parentNode.Data, parentNode.Children);
                }

                currentNode = parentNode;
            }
        }

  核心思想是,首先计算当前节点,然后,根据树节点中保存的parent节点信息,逐级向上计算其父节点。比较简单,不多说。后续遍历计算有了,还有一种情况,就是要从树里边查找某个节点,这里明显是要前序遍历的,因为扎到某个节点我就直接返回了,犯不着每个节点都过一遍及保留中途父节点信息。实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <summary>
        /// 查找符合指定条件的节点
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static TreeNode<T> FindNode<T>(TreeNode<T> node, Func<T, bool> condition)
             where T : Data
        {
            if (node == null)
            {
                return null;
            }

            Stack<TreeNode<T>> stackParentsNodes = new Stack<TreeNode<T>>();
            TreeNode<T> currentNode = node;
            while (currentNode != null || stackParentsNodes.Any())
            {
                if (currentNode != null)
                {
                    if (condition(currentNode.Data))
                    {
                        return currentNode;
                    }

                    stackParentsNodes.Push(currentNode);
                    currentNode = currentNode.Left;
                }
                else
                {
                    currentNode = stackParentsNodes.Pop().Right;
                }
            }

            return null;
        }

  典型的前序遍历,比较简单,不多说。

4、总结

  这么一套解决方案下来,全套30多张表格的计算,由原来的十几分钟,改进到几十秒。好了,本次分享就到这里,希望能帮助到大家。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-03-10 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
寻找二叉树的下一个节点
已知一个包含父节点引用的二叉树和其中的一个节点,如何找出这个节点中序遍历序列的下一个节点?
神奇的程序员
2022/04/10
2590
寻找二叉树的下一个节点
DFS最难也就这样了
之前我们已经已经把DFS的核心思想讲清楚了,也就这么回事儿,也再次向大家宣扬了一种循序渐进的思想,从基本解法向外去击破。
写代码的阿宗
2020/08/24
5090
JavaScript数据结构-树
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
奋飛
2019/08/15
4360
二叉搜索树 (BST) 的创建以及遍历
二叉搜索树(Binary Search Tree) : 属于二叉树,其中每个节点都含有一个可以比较的键(如需要可以在键上关联值), 且每个节点的键都大于其左子树中的任意节点而小于右子树的任意节点的键。
用户2434869
2018/09/12
7730
二叉搜索树 (BST) 的创建以及遍历
【愚公系列】2023年11月 七大查找算法(五)-树查找
在编程语言中,查找算法是指在一个数据集合中查找某个元素是否存在的算法。常见的查找算法包括:
愚公搬代码
2023/11/18
2700
使用JS 实现二叉查找树(Binary Search Tree)
任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 任意节点的左、右子树也分别为二叉查找树; 没有键值相等的节点。二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为O(log n)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、multiset、关联数组等。
前端迷
2019/12/05
1.2K0
使用JS 实现二叉查找树(Binary Search Tree)
Dimple在左耳听风ARTS打卡(二十二)
这周身体有点抱恙,扛过炎热的7月,没想到在8月之初,竟然华丽丽的中暑,哈哈。好在还没想过放弃打卡,等哪天想放弃了,估计还有点舍不得。
程序员小跃
2019/12/25
4250
二叉排序树
数组和链表在增删改查数据时,都有各自的缺点,比如数组要在中间插入数据,就要让后面的数据整体都移动,而链表检索数据很慢。之前说二叉树时,说到树这种结构就是就是为了弥补数组和链表的缺点而诞生的,二叉排序树(Binary search tree,简称BST),更是体现了这一点。二叉排序树有以下特点:
贪挽懒月
2022/05/13
2670
二叉排序树
二叉树遍历基础 -- 递归与非递归的实现方法
之前也写过不少关于二叉树的东西了,但是总体来说,二叉树还是一个很绕的东西,所以单独择出来写一篇笔记,之前也没计划什么的,就想到什么写什么吧。不过该篇文章的主要内容是关于二叉树的三种遍历(前序、中序、后序)不同的实现方式(递归与非递归)。
WeiMLing
2019/08/23
9020
二叉树遍历基础  --  递归与非递归的实现方法
node+ts完成课程设计
问题描述:建立身份证信息管理系统,能够进行身份证信息的录入、查找,保存,要求考虑查找效率,用二叉排序树存储信息。 具体功能有:
玖柒的小窝
2021/12/08
5780
node+ts完成课程设计
数据结构小记【Python/C++版】——树与二叉树篇
2.有些树的每个节点的子节点之间可以是无序的,两个子节点之间甚至可以交换位置。而(有序)二叉树中,每个节点的子节点之间需要区分是左子节点还是右子节点,即使整棵树就两个节点。
Coder-ZZ
2023/02/23
4160
数据结构小记【Python/C++版】——树与二叉树篇
2022前端常考手写面试题总结
此方法属于黑魔法,极易容易被xss攻击,还有一种new Function大同小异。
helloworld1024
2022/11/10
4240
【图解数据结构】二叉查找树
二叉查找树定义 每棵子树头节点的值都比各自左子树上所有节点值要大,也都比各自右子树上所有节点值要小。 二叉查找树的中序遍历序列一定是从小到大排列的。 二叉查找树节点定义 /// <summary> /// 二叉查找树节点 /// </summary> public class Node { /// <summary> /// 节点值 /// </summary> public int Data { get; set; } /// <summary> /// 左
撸码那些事
2018/06/21
5220
【数据结构】非线性表----树详解
树是一种非线性结构,它是由**n(n>=0)**个有限结点组成一个具有层次关系的集合。具有层次关系则说明它的结构不再是线性表那样一对一,而是一对多的关系;随着层数的增加,每一层的元素个数也在不断变化(由上一层和该层的对应关系决定)。
Skrrapper
2024/08/05
1060
【数据结构】非线性表----树详解
【数据结构】二叉树链式结构——感受递归的暴力美学
链式结构,使用链表来表示一颗二叉树,即用链来指示二叉树中元素的逻辑关系。通常的就是链表中每个节点右三个域组成,数据域和左右指针域,左右指针分别指向该节点的左孩子节点和右孩子节点的存储地址。
星辰与你
2024/10/17
980
【数据结构】二叉树链式结构——感受递归的暴力美学
前端面试中的常见的算法问题
虽说我们很多时候前端很少有机会接触到算法。大多都交互性的操作,然而从各大公司面试来看,算法依旧是考察的一方面。实际上学习数据结构与算法对于工程师去理解和分析问题都是有帮助的。如果将来当我们面对较为复杂的问题,这些基础知识的积累可以帮助我们更好的优化解决思路。下面罗列在前端面试中经常撞见的几个问题吧。 Q1 判断一个单词是否是回文? 回文是指把相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的情趣,叫做回文,也叫回环。比如 mamam redivider . 很多人拿到这样的题目非常容易想到用for
前朝楚水
2018/04/03
6830
前端面试中的常见的算法问题
作为程序员,难道你心里没点“B树”?
顺序存储的特点是各个存储单位在逻辑和物理内存上都是相邻的,典型的就是代表就是数组,物理地址相邻因此我们可以通过下标很快的检索出一个元素
xcbeyond
2020/03/25
3990
作为程序员,难道你心里没点“B树”?
平衡二叉树
之前学习了二叉排序树,假如现有数列:1,2,3,4,5,要用这个数列创建一棵二叉排序树,结果是这样的:
贪挽懒月
2022/05/13
2710
平衡二叉树
Algorithms_二叉树的前序遍历、中序遍历、后续遍历(深度优先)
注意我们这里用的是二分搜索树来演示二叉树的这个遍历,才会有中序遍历的那个排序的特征。
小小工匠
2021/08/17
7560
手撕数据结构之二叉树
树是⼀种⾮线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
凯子坚持C
2024/09/23
2190
手撕数据结构之二叉树
相关推荐
寻找二叉树的下一个节点
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档