尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Excel公式中的LAMBDA与REDUCE:递归与循环的实战指南

Excel公式中的LAMBDA与REDUCE:递归与循环的实战指南 在 Excel 公式里实现递归这件事在过去听起来像天方夜谭。你有一组数据需要反复套用同一个逻辑但公式世界里没有 for 循环也没有 while你想定义一个自己的函数但函数列表永远是那几百个内置函数。于是项目只要遇到一点分治、迭代、嵌套结构就不得不切到 VBA或者铺十几条辅助列硬扛。直到LAMBDA与REDUCE出现。Excel 365 和 WPS 新版本把它们带进了普通公式让用户终于可以在单元格里写出“自己调用自己”的函数也可以像写 reduce 一样把整个数组压成一个值。很多人看到这两个函数时会产生一个很有趣的问题REDUCE 和 LAMBDA 递归到底谁是盟主我的判断是这根本算不上同层级的竞争。REDUCE是折叠数组的循环工具LAMBDA是让一段计算获得名字与自引用能力的入口。如果只是单纯重复几千次REDUCE 往往更稳如果要做树形遍历、整数转字符串、分治算法你绕不开被命名后的 LAMBDA 递归。而真正决定递归成败的是那个藏在IF里的终止条件。下面我会把 LAMBDA、REDUCE、递归、终止条件、循环深度放到同一个坐标系里拆开讲并带几个可以直接复制到 Excel/WPS 名称管理器里的实战函数整数转字符串、各位数字求和、阶乘。看完你会清楚什么时候该用 REDUCE什么时候该用 LAMBDA 递归以及遇到#NUM!和卡顿时第一步该去查什么。1. 这篇文章真正要解决的问题很多 Excel 用户学会SUM、IF、VLOOKUP之后会进入一个瓶颈期能处理 80% 的日常表但遇到需要“重复处理同一件事”的场景就卡住了。比如这样几个问题把一个整数20240607转成字符串20240607不借助文本函数之外的操作纯函数式怎么写把一列数字逐位相加直到结果为个位数比如12345 → 1234515 → 156公式怎么写有一批订单号结构是“前缀 不定长数字”需要把它们按规则重新拼装怎么办传统解决方案无非三种辅助列、迭代计算、VBA。辅助列会污染表格结构迭代计算开启后全局生效容易引入隐藏 bugVBA 又要开启宏在许多企业环境里受限。LAMBDA和REDUCE改变了这个局面。它们让 Excel/WPS 公式第一次拥有了“函数式编程”的完整能力匿名函数、高阶函数、递归、累积器。这意味着你可以在一个单元格里完成过去需要 VBA 才能实现的逻辑而且公式可以保存到名称管理器里像内置函数一样复用。但这里也有一个需要提前说明的现实LAMBDA 递归和 REDUCE 并不总是等价替换。两者的底层思路不一样适用的场景也不一样。如果不理解终止条件和循环深度很容易写出一个“看起来正确、但一运行就溢出”的公式。这篇文章适合三类读者已经熟悉SUM、IF、VLOOKUP等常用函数想进阶函数式写法的 Excel/WPS 用户。对 Java/Python 中lambda和reduce有了解想知道 Excel 里对应实现方式的开发者。正在评估“能不能用纯公式替代 VBA/辅助列”的办公自动化实践者。2. 基础概念LAMBDA、REDUCE、递归、终止条件、循环深度2.1 LAMBDA让公式拥有“函数名”LAMBDA的语法非常直接LAMBDA(参数1, 参数2, ..., 计算表达式)它本质上就是一个匿名函数。比如LAMBDA(x, x * 2)(21)运行结果是42。括号里的21是传给参数x的实际值。如果只是这样LAMBDA 还没有太大威力。关键在于你可以在名称管理器中给它定义一个名字。例如新建一个名称为Double的条目引用位置填写LAMBDA(x, x * 2)保存之后你在工作表中写Double(21)会得到42。这就是“自定义函数”的真实实现方式。递归必须依赖“函数拥有名字”这个前提。一个匿名函数没有办法调用自身因为它在定义时还没有名字但一旦在名称管理器中命名为F函数体里就可以写F(...)来引用自己。这是整个 LAMBDA 递归的基石。2.2 REDUCE数组折叠器REDUCE的语法是REDUCE([初始值], 数组, LAMBDA(累加器, 当前值, 计算表达式))它的作用是把一个数组从左到右扫描一遍不断地用“累加器”和“当前值”计算出一个新累加器最后只返回一个值。看一个最简单的例子REDUCE(0, {1;2;3;4;5}, LAMBDA(acc, v, acc v))运算过程是初始acc 0取v 1acc 0 1 1取v 2acc 1 2 3取v 3acc 3 3 6取v 4acc 6 4 10取v 5acc 10 5 15最终结果是15。这个模式对应编程语言里的fold/reducefunctools.reduce、JavaStream.reduce、JavaScriptArray.reduce都是同一类东西。注意REDUCE里的LAMBDA不需要名称管理器它是内联的匿名函数。REDUCE引擎会负责迭代你不需要自己写递归调用所以它天然规避了“自引用”问题也不需要显式写终止条件——数组遍历完过程自然结束。2.3 递归自己调用自己但必须会停下来递归是一种解决问题的方法函数在执行过程中调用自身把一个大问题拆成更小的同构子问题直到某个最小问题可以直接给出答案。在 Excel/WPS 公式里一个递归过程必须同时满足三个条件基础情况有一个参数取值能直接返回结果不再调用自身。递归调用调用自身时参数要朝基础情况收敛。终止条件判断通常用IF区分基础情况和递归情况。没有终止条件的递归相当于一个没有出口的循环最终会耗尽计算资源。Excel/WPS 会返回错误或卡死。2.4 循环深度递归能叠多少层循环深度指递归调用最多嵌套多少层也叫递归深度或调用栈深度。普通REDUCE的“层数”由数组长度决定引擎内部自动折返而 LAMBDA 递归的层数由你调用自己的次数决定每层都占用一份栈空间。不同版本、不同计算引擎对递归深度支持的极限不一样。有的版本能跑到几百层有的版本可能几十层就报错。掌握这个概念你才能判断一个递归公式能否落地。下面用一个表格对比循环、迭代、递归在 Excel 公式中的表现维度REDUCE 循环LAMBDA 递归函数是否需要名称不需要需要在名称管理器中命名终止条件数组遍历结束自动停止必须用 IF 显式设计典型场景累加、拼接、过滤后折叠分治、树的遍历、数学递归循环深度风险与数组长度相关相对稳定与递归次数相关更容易溢出可读性接近函数式编程需要画递归树来理解对新手友好度更高需要更多练习把这两个函数区别清楚之后我们就可以进入实际操作了。3. 环境准备先确认你的 Excel/WPS 能不能跑3.1 版本与可用性检查LAMBDA和REDUCE并不是所有版本都支持。打开你的 Excel 或 WPS在任意单元格输入下面两个公式进行探测LAMBDA(a, a)(1)REDUCE(0, {1;2;3}, LAMBDA(a, b, a b))如果都能正常返回1和6说明当前版本支持这两个函数。如果提示名称错误或显示#NAME?则说明当前版本不具备该能力需要考虑升级或者继续使用辅助列。WPS 的支持情况跟随版本变化较快不同时期的版本体验可能不同。稳妥的判断方式是以官方更新日志为准并在自己的 WPS 版本里跑一下上面的最小示例。3.2 名称管理器LAMBDA 递归的“户口本”LAMBDA 递归必须通过名称管理器给函数命名。具体位置Excel公式→名称管理器→新建WPS公式→名称管理器→新建在名称管理器中一条 LAMBDA 递归定义通常包含两个字段名称例如NumToStr引用位置例如LAMBDA(n, IF(...))引用位置必须是等号开头。名称管理器保存后你可以在任意单元格直接使用NumToStr(2024)就像使用内置函数一样。这里有一个新手容易踩的坑在单元格里直接写LAMBDA(n, IF(n10, TEXT(n,0), n ))是能运行的但函数体内部如果再写NumToStr(...)Excel/WPS 会因为找不到这个名字而报错。必须先到名称管理器里新建NumToStr然后函数体里才能引用它。4. 第一回合用 REDUCE 实现循环折叠我们先用 REDUCE 处理一批典型的“循环折叠”任务感受它作为循环工具的能力和边界。4.1 累加求和累加是最经典的 reduce 应用。在单元格输入REDUCE(0, {1;2;3;4;5;6;7;8;9;10}, LAMBDA(acc, v, acc v))结果应该是55。这个例子对应 Python 的from functools import reduce result reduce(lambda acc, v: acc v, range(1, 11), 0) print(result) # 55也对应 Java 的import java.util.List; int result List.of(1,2,3,4,5,6,7,8,9,10) .stream() .reduce(0, Integer::sum); // 55可以清楚地看到Excel 的REDUCE和编程语言里的reduce是同一个思路初始值 数组 合并逻辑。4.2 字符串拼接REDUCE 也擅长把多个单元格内容拼起来。比如把一组字母拼成单词REDUCE(, {J;A;V;A}, LAMBDA(acc, v, acc v))结果是JAVA。如果有一个区域是A1:A5内容是A、B、C、D、E可以这样写REDUCE(, A1:A5, LAMBDA(acc, v, acc v))在旧版 Excel 里这种操作通常需要CONCATENATE或TEXTJOIN。但TEXTJOIN只能按指定分隔符拼接无法在拼接过程中对每个元素做额外逻辑。REDUCE 可以你想在拼接前判断、格式化、过滤都不需要辅助列。4.3 带分支的累积只累加偶数REDUCE 的 LAMBDA 参数中可以使用IF所以它可以做“带条件”的循环。比如对1 到 10只累加偶数REDUCE(0, {1;2;3;4;5;6;7;8;9;10}, LAMBDA(acc, v, IF(MOD(v, 2) 0, acc v, acc)))结果应该是30也就是2 4 6 8 10。这个公式里IF负责控制累积行为满足条件才加不满足就保留原累加器。这相当于编程语言里if (v % 2 0) sum v;的逻辑。通过这几个示例你应该已经感受到REDUCE 适合处理“对整个数组做一遍规则明确、可累积处理”的场景。它是循环层的工具不需要递归自引用因此也不存在终止条件的设计问题。5. 第二回合用 LAMBDA 递归实现分治与转化有些问题不适合用“遍历数组”来理解而更适合用“把大问题拆成小问题”。这类问题需要递归。5.1 阶乘最朴素的递归入门阶乘的定义是F(0) 1F(n) n * F(n-1)打开名称管理器新建名称MYFACT引用位置填写LAMBDA(n, IF(n 1, 1, n * MYFACT(n - 1)))单元格输入MYFACT(5)得到120。把这个函数拆开看终止条件IF(n 1, 1, ...)当n小于等于 1 时直接返回 1。递归调用n * MYFACT(n - 1)每次n都减 1最终必然触达n 1。如果去掉IF直接写LAMBDA(n, n * MYFACT(n - 1))那么当n为 5 时会无限调用MYFACT(4-3-2-1-0--1-...)最终导致计算溢出。5.2 递归法将一个整数 n 转换成字符串这是递归里非常典型的一道题对应我们常见的“整数转字符串”需求。常规思路是取n的最后一位数字再递归处理除掉最后一位后的部分。新建名称NumToStr引用位置LAMBDA(n, IF(n 10, TEXT(n, 0), NumToStr(INT(n / 10)) TEXT(MOD(n, 10), 0)))单元格输入NumToStr(20240607)返回结果20240607注意结果是字符串不是数字。因为TEXT返回文本也是文本拼接运算。这段公式的执行过程可以展开成NumToStr(20240607) NumToStr(2024060) 7 (NumToStr(202406) 0) 7 ((NumToStr(20240) 6) 0) 7 ... 2 0 2 4 0 6 0 7 20240607这里的终止条件就是n 10当数字只剩一位时直接返回它的文本形式。每次递归都通过INT(n / 10)把数字缩到原来的十分之一所以一定会收敛。这个示例也说明了一个通用套路递归处理数字时最常用的两个操作是INT去掉低位、MOD取出低位。它天然适合从右往左逐位拆解。5.3 处理负数的递归增强版上面的NumToStr只适用于非负整数。如果n是负数比如-123n 10成立但TEXT(-123, 0)会直接返回-123后续拼接逻辑会乱掉。一个更健壮的写法是先处理负号再递归处理绝对值部分。新建名称NumToStr2LAMBDA(n, IF(n 0, - NumToStr2(-n), IF(n 10, TEXT(n, 0), NumToStr2(INT(n / 10)) TEXT(MOD(n, 10), 0))))这个公式里有两个终止分支n 0先加负号再递归到正数分支。n 10数字只剩一位时直接返回。你可以在工作表中验证NumToStr2(-2024)结果-2024这里想强调一个递归设计的要点分支越多越要保证每个分支都朝“基础情况”收敛。负号分支把负数转为正数正数分支再通过INT、MOD不断缩小数字最终一定能落到n 10。5.4 数字各位求和递归的二次收敛“各位数字求和直到一位数”也是一个很适合递归的小题。它的规则是如果数字小于 10直接返回否则把所有位的数字相加然后再递归判断。新建名称DigitSumLAMBDA(n, IF(n 10, n, DigitSum(INT(n / 10) MOD(n, 10))))这里和前面不同递归参数不是单纯缩小一位数字而是“把当前数字的各位加总”之后再次进入函数。比如DigitSum(12345) DigitSum(1234 5) DigitSum(1239) DigitSum(123 9) DigitSum(132) DigitSum(13 2) DigitSum(15) DigitSum(1 5) DigitSum(6) 6最终得到6。这个递归里终止条件同样是n 10但收敛方式更漂亮每次调用都把数字长度缩短同时数值也在不断变小。5.5 REDUCE 能替代这些递归吗如果非要用 REDUCE 实现整数转字符串一种思路是先拆出每一位数字到一个数组再用手服务。这里就能看到递归的不可替代性了。REDUCE 天生处理“已经存在的数组”而递归处理“如何生成这个数组/区间/层次”的问题。两者并不谁替代谁而是分工不同已经有数组做折叠合并用 REDUCE。问题需要逐层拆解、规模递减用 LAMBDA 递归。6. 终止条件递归能不能活下来的关键6.1 终止条件的三个必要条件Excel/WPS 的 LAMBDA 递归在设计时至少要考虑三件事第一是否存在基础情况。所谓基础情况就是某个参数值下直接返回结果不再调用自身。阶乘里的n 1、整数转字符串里的n 10都是基础情况。第二递归是否朝基础情况收敛。每次调用自身时参数必须变得更接近基础情况。例如MYFACT(n - 1)让n递减NumToStr(INT(n / 10))让数字位数减少。如果递归参数不收敛就会无限循环。第三IF 条件是否覆盖所有输入。如果函数允许输入负数、小数、空值而你只设计了正整数的分支那么在特殊值上可能出现不可预期的结果。要么提前丢弃非法输入要么像NumToStr2一样增加负号分支。6.2 没有终止条件会发生什么如果把阶乘写错成这样LAMBDA(n, n * MYFACT(n - 1))那么MYFACT(5)会一直调用MYFACT(4)、MYFACT(3)……直到计算引擎的递归深度或栈空间被耗尽。现象通常是单元格变成一个等待状态或者直接报#NUM!甚至整个工作簿都卡顿很久。更隐蔽的错误是“终止条件看起来有但收敛速度太慢”。比如两个递归分支互相来回转换参数的数值在一段时间内反复震荡。这种设计比直接没有终止条件更难排查因为你会看到它确实调用了几次但最终依然溢出。6.3 怎么验证终止条件没有问题一个实用的方法是先用小参数做最小验证。阶乘先测MYFACT(1)、MYFACT(2)。整数转字符串先测NumToStr(9)、NumToStr(10)、NumToStr(99)。数字求和先测DigitSum(9)、DigitSum(99)、DigitSum(123)。如果最小的几个数据都正确再慢慢加大规模。不要一开始就把100000丢进递归函数。递归公式的调试远比普通公式难因为你没办法在公式里加断点。控制变量、从小数据开始是最经济的手段。7. 循环深度递归的物理上限7.1 什么是调用栈每次调用一个函数计算引擎都要为这次调用保存当前的状态包括参数、返回值位置、局部变量。这些信息存放在一块叫“栈”的内存区域。递归每深入一层栈就多一层。当递归层次过深栈空间用尽就会溢出。普通非递归公式不容易触发这个限制因为 Excel 的求值引擎会尽量简化中间过程。但 LAMBDA 递归是真正的“函数调用”每层递归都可能消耗栈空间。因此循环深度是设计递归公式时绕不开的工程约束。REDUCE 有所区别它的迭代过程在引擎内部实现你可以把它理解成“伪递归”。虽然 LAMBDA 表达式在概念上每处理一个元素都会“调用一次”但引擎通常会把它优化成稳定的循环结构所以它比手写递归的栈负担更小。7.2 测试自己环境的递归上限不同版本的 Excel/WPS以及不同操作系统的 Excel递归深度上限都可能不同。与其在网上查一个写死的数字不如自己在环境里测一下。新建名称DepthTestLAMBDA(n, IF(n 0, OK, DepthTest(n - 1)))然后分别在单元格输入DepthTest(10) DepthTest(50) DepthTest(100) DepthTest(200) DepthTest(500)哪个开始报#NUM!或卡顿你就知道自己当前环境大概能承受多少层。注意这是一个极其耗资源的测试方式建议一次性从小数字向上逐步测不要直接输入一个非常大的数字。7.3 不同编程语言的递归深度对比递归深度并不是 Excel 独有的问题。几乎所有主流语言都有类似限制Python 默认递归深度约为 1000 层超过会抛出RecursionError。Java 的递归深度受 JVM 栈大小影响默认栈通常能支持几千层但深了同样会StackOverflowError。JavaScript 中过深递归最终会触发“Maximum call stack size exceeded”。这说明一个问题递归的数学表达很漂亮但工程落地时必须关心栈资源。Excel/WPS 公式不像 VBA 那样可以调整线程或栈大小所以更要遵守“能迭代就不递归能递归就控制层次”的原则。7.4 深度限制下的工程对策遇到深度过深如何改造第一种是增加终止条件阈值。比如把n 1改成n 10然后把基础值提前算好。但这只是小规模优化治标不治本。第二种是“转迭代”。当你明显不需要递归结构只是重复同样的逻辑时优先用 REDUCE 或辅助列。例如求和、拼接、条件累积REDUCE 都能稳定完成不需要自己维护递归栈。第三种是“改分治”。把一个大问题切成两个小问题递归深度从 O(n) 降到 O(log n)。最典型的就是归并排序、二分查找。虽然 Excel 公式实现分治语法比较繁琐但思路完全一致。这也是为什么“递归二路归并排序”这类题目在算法学习中极度重要——它不只训练递归更训练你控制递归深度。如果你的工作簿已经逼近深度上限还有一个简单粗暴但工程上很有效的方式拆步骤。用辅助列或中间结果把一次递归拆成两段。虽然失去了“纯公式”的优雅但胜在稳定不会把整个工作簿拖垮。8. 常见问题与排查思路下面把 LAMBDA 递归和 REDUCE 使用中常见的问题整理成一个排查表问题现象可能原因排查方式解决方案单元格报#NAME?当前版本不支持 LAMBDA/REDUCE或名称未正确定义先输入LAMBDA(a,a)(1)和REDUCE(0,{1;2},LAMBDA(a,b,ab))做检测升级版本确认名称管理器中名称已保存公式能够输入但返回#NUM!递归深度超出当前环境上限或缺少终止条件用DepthTest公式测试当前环境的深度上限检查 IF 分支是否收敛增加终止条件改用 REDUCE用分治降低递归深度公式卡顿长时间不返回递归没有终止条件或参数没有朝基础情况收敛检查递归参数变化先输入小参数测试修正递归收敛逻辑避免使用大参数直接测试名称管理器中定义后单元格仍找不到名称名称拼写错误或名称管理器作用域不对打开名称管理器检查名称是否存在输入名称时等待自动补全提示删除多余空格重新命名复制名称管理器中的完整路径在 WPS 中打开文件LAMBDA 相关函数全部失效WPS 版本不支持或解析差异在 WPS 中运行最小示例LAMBDA(a,a)(1)以 WPS 官方版本支持为准无法使用则退回辅助列/VBA递归结果在小数据正确大数据出错超出递归深度上限或计算资源不足逐步增加参数观察哪个数量级开始失效改用 REDUCE 迭代拆步骤控制数组规模MOD对负数结果与预期不一致负数取模在不同环境规则不同单独测试MOD(-7,10)的返回结果在递归中先统一处理符号如IF(n0, ...)排查时有一个总原则先把异常范围缩小到最小。如果公式出错先用常量数组而不是区域引用测试如果递归出错先用 0、1、2 这种小参数测试。这样可以快速区分是逻辑错误还是环境限制。9. 最佳实践与工程建议9.1 命名规范要清晰名称管理器里会积累很多 LAMBDA 递归函数命名不清晰会非常痛苦。建议统一前缀例如用FN_开头表示自定义函数FN_FACTFN_NUM_TO_STRFN_DIGIT_SUM这样在工作表中输入FN_时自动补全列表会很整齐别人接手工作簿时也能一眼辨认出哪些是自定义函数。9.2 能用 REDUCE 就优先 REDUCE凡是“已经有一批数据要做规则统一的累积处理”优先使用 REDUCE不要为了炫技而手写递归。原因有两点一是 REDUCE 不需要名称管理器公式整体内联复制到其他工作簿不容易丢失。 二是 REDUCE 的迭代流程由引擎控制通常不会因为栈空间不足而失败。递归留给那些真正体现“自然分层”的问题树形结构、数字拆解、分治算法。9.3 先写终止条件再写递归体编写 LAMBDA 递归时我建议的顺序是先确定基础情况是什么写成 IF 的第一个分支。再写递归调用并确认参数一步一步接近基础情况。最后补充边界值比如负数、0、空字符串。顺序颠倒很容易导致“递归体写得漂亮但终止条件漏了”这是最常见也最可怕的错误。9.4 分批测试永远从小数据开始递归公式没法下断点所以测试策略特别重要。建议准备一个专门的测试区域输入几组有代表性的数据MYFACT(0) MYFACT(1) MYFACT(5) NumToStr(9) NumToStr(2024) DigitSum(9) DigitSum(999)如果这些数据全部正确再尝试大规模数据。永远不要把一个需要几十层递归的大数一把丢进公式除非你已经确认当前环境的深度上限。9.5 避免易变函数与隐藏依赖LAMBDA 递归公式里尽量不要使用RAND()、NOW()、TODAY()这类易变函数。递归过程本身计算量就大如果在每一层递归里都调用一次易变函数整个工作簿的性能会急剧下降而且公式结果不稳定难以排查。9.6 做好工作簿文档化名称管理器里放一堆函数时间久了谁都记不住参数含义。建议在工作簿里专门建一个“函数说明”工作表记录每个名称的功能、参数、返回结果、注意事项FN_NUM_TO_STR(n) 功能把非负整数转换为字符串 参数n 为非负整数 示例FN_NUM_TO_STR(20240607) - 20240607 注意负数请使用 FN_NUM_TO_STR2这一步看似简单但对长期维护和工作簿交接帮助极大。10. 总结谁是盟主下一步学什么回到最初的问题Reduce 与 Lambda 递归谁是盟主我的结论是如果把“递归”看作一种能力盟主只能是被命名后的 LAMBDA。因为只有它能让你实现真正的自引用也只有它能在公式世界里模拟“分而治之”的思维。REDUCE 是一个非常优秀的循环折叠工具适合处理确定数组上的批量重复任务但它并不具备真正的递归建模能力。不过更准确的判断是递归的真正命门不是某个函数而是终止条件。没有终止条件LAMBDA 递归只会耗尽栈空间有了终止条件并保证收敛你才能写出可靠的递归函数。循环深度则决定这个递归能否在现实工作簿里落地。所以你在项目中的选择应该是要折叠一个数组、累积一个结果用 REDUCE。要按数学定义处理分层问题用名称管理器里的 LAMBDA 递归。无论选哪种先设计终止条件再设计主体逻辑。下一步你可以尝试把文章里的几个例子改造成自己的需求。比如把FN_NUM_TO_STR改成“金额转中文大写”的递归版把DigitSum改成判断某个数字能否被 3 整除的递归版再进一步可以研究“递归二路归并排序”思考如何用 Excel 数组公式配合 LAMBDA 实现分治排序的流程。LAMBDA 和 REDUCE 给 Excel/WPS 带来的不只是两个新函数而是一整套函数式编程的思维方式。当你开始习惯用“终止条件 递归调用 累积器”去思考公式时就会发现很多过去只能靠 VBA 的问题现在真的能用纯公式跑通了。
返回列表