【力扣】2629. 复合函数——函数组合
文章目录
- 【力扣】2629. 复合函数——函数组合
- 题目
- 解决方案
- 概述
- 方法 1:使用迭代的函数组合
- 概述
- 算法
- 实现
- 复杂度分析
- 方法 2:使用 Array.reduceRight() 的函数组合
- 概述
- 算法
- 实现
- 复杂度分析
- 附加考虑
- 处理 this 上下文
- 使用外部库
题目
请你编写一个函数,它接收一个函数数组 [f1, f2, f3,…, fn]
,并返回一个新的函数 fn
,它是函数数组的 复合函数 。
[f(x), g(x), h(x)]
的 复合函数 为 fn(x) = f(g(h(x)))
。
一个空函数列表的 复合函数 是 恒等函数 f(x) = x
。
你可以假设数组中的每个函数接受一个整型参数作为输入,并返回一个整型作为输出。
示例 1:
输入:functions = [x => x + 1, x => x * x, x => 2 * x], x = 4
输出:65
解释:
从右向左计算......
Starting with x = 4.
2 * (4) = 8
(8) * (8) = 64
(64) + 1 = 65
示例 2:
输入:functions = [x => 10 * x, x => 10 * x, x => 10 * x], x = 1
输出:1000
解释:
从右向左计算......
10 * (1) = 10
10 * (10) = 100
10 * (100) = 1000
示例 3:
输入:functions = [], x = 42
输出:42
解释:
空函数列表的复合函数就是恒等函数
提示:
-1000 <= x <= 1000
0 <= functions.length <= 1000
- 所有函数都接受并返回一个整型
解决方案
概述
函数组合是函数式编程中的一个概念,即一个函数的输出被用作另一个函数的输入。换言之,它是将两个或更多函数链接在一起,以使一个函数的结果成为下一个函数的输入的过程。
例如,这里有两个函数,f(x) 和 g(x):
const f = x => x + 2;
const g = x => x * 3;
这两个函数的组合,表示为 (f ∘ g)(x)
,即首先应用函数 g(x)
,然后使用g(x)
的结果作为f(x)
的输入。在这种情况下,(f ∘ g)(x)
就是:
const composedFunc = x => f(g(x)); // f(g(x)) = f(3x) = 3x + 2
因此,当我们组合函数f(x)
和g(x)
时,得到的函数(f ∘ g)(x)
接受一个输入 x
,将其乘以 3
(使用 g(x)
),然后再加上 2 (使用 f(x)
)。
const composedFunc = x => f(g(x)); // f(g(x)) = f(3x) = 3x + 2
在这个问题中,你需要根据给定的函数数组,创建一个表示给定函数数组组合的单一函数。这里的挑战在于你需要了解如何将函数链接在一起并将一个函数的输出传递给下一个函数的输入。
在函数数组为空的情况下,组合函数应该充当标识函数,即 f(x) = x
。换言之,函数应返回传递给它的任何内容且不进行任何修改。
∘符号(f ∘ g)(x)
在数学中用于表示函数组合。它读作 “f
与 g
的组合" 或 “g
的 f
”。f
和g
之间的小圆圈 (∘)
是组合运算符。此符号用来表示首先将函数g(x)
应用,然后使用结果作为函数f(x)
的输入值。换句话说,首先应用 g(x)
,然后将结果用作f(x)
的输入。
方法 1:使用迭代的函数组合
概述
函数组合是一个概念,其中我们按指定的顺序将一系列函数应用于输入值。在这个问题中,我们需要组合数组中给定的函数,并创建一个表示它们组合的新函数。应用这些函数的顺序是从右到左的,当函数数组为空时,我们应该返回标识函数,它返回的输入值保持不变。
为了解决这个问题,我们可以从数组的最后一个函数开始反向迭代,并依次将每个函数应用于输入值。我们将从输入值 x 开始,应用数组中的最后一个函数。然后,我们将使用结果作为前一个函数的输入,并继续这个过程,直到我们到达数组中的第一个函数。在应用所有函数后,我们将返回最终的结果。
算法
- 在
compose
函数内部,返回另一个接受输入值x
的函数。 - 检查函数数组的长度是否为零;如果是,则返回标识函数(即返回
x
)。 - 使用值
x
初始化变量input
。 - 从最后一个索引到第一个索引遍历函数数组。
- 对数组中的每个函数,将其应用于
input
值并使用结果更新input
值。 - 在遍历所有函数之后,将最终输入值作为组合函数的输出返回。
实现
以下是使用常规for
循环的实现:
var compose = function (functions) {
return function (x) {
if (functions.length === 0) return x;
let input = x;
for (let i = functions.length - 1; i >= 0; i--) {
const currFunc = functions[i];
input = currFunc(input);
}
return input;
};
};
使用for ... of
循环的实现:
var compose = function (functions) {
return function (x) {
if (functions.length === 0) return x;
let input = x;
for (const func of functions.reverse()) {
input = func(input);
}
return input;
};
};
复杂度分析
设 N 为数组中的函数数量。
时间复杂度:O(N)
。假设每个函数的时间复杂度是常数,那么数组中的每个函数都将被调用一次。
空间复杂度:O(1)
。迭代方法使用单个变量input
来存储中间结果,不需要额外的空间。
方法 2:使用 Array.reduceRight() 的函数组合
概述
在第一种方法中,我们使用迭代的方式从右到左应用函数。不过,我们可以利用Array.reduceRight()
方法来实现相同的结果。reduceRight()
方法将一个函数应用于累加器和数组中的每个元素(从右到左),以将它们还原为单个值。在这种情况下,我们的累加器将是输入值 x,函数将是数组中函数的组合。
使用reduceRight()
简化了代码,并提供了更函数式编程风格的解决方案。关键是理解Array.reduceRight()
方法的工作原理以及如何将其应用于此问题。
算法
- 在
compose
函数内部,返回另一个接受输入值 x 的函数。 - 使用
Array.reduceRight()
方法从右到左迭代函数。 - 对数组中的每个函数,将其应用于累加器(初始为 x)并使用结果更新累加器。
- 在迭代所有函数后,将最终累加器值作为组合函数的输出返回。
实现
var compose = function(functions) {
return x => functions.reduceRight((acc, f) => f(acc), x);
};
关键在于理解Array.reduceRight()
方法的工作原理。
Array.reduceRight()
是内置的 JavaScript 数组方法,用于对数组的每个元素应用一个函数,从右到左进行迭代。它需要两个参数:一个reducer
函数和一个可选的累加器的初始值。
reducer
函数本身有四个参数:累加器、当前值、当前索引和正在处理的数组。累加器是一个在每次迭代中建立的值,最终在过程结束时返回。在我们的情况下,累加器表示应用组合函数中的函数时的中间结果。
以下是在 compose
函数上下文中Array.reduceRight()
的工作方式的详细说明:
compose
函数接收一个包含函数的数组,并返回一个新的函数,该函数接受输入值 x。- 当使用输入值 x 调用组合函数时,它在函数数组上调用
Array.reduceRight()
。 reducer
函数对数组中的每个函数进行迭代,从最右边的元素开始向左移动。累加器最初包含输入值 x。- 在每次迭代中,
reducer
函数将当前函数应用于累加器,并使用结果更新累加器。 - 一旦所有函数都已应用,将返回累加器的最终值。
为了说明这个过程,让我们考虑一个简单的例子:
const functions = [x => x * 2, x => x + 1];
const composedFn = compose(functions);
const result = composedFn(3); // 结果应该是 (3 + 1) * 2 = 8
compose
函数接收一个包含两个函数的数组:x => x * 2
和x => x + 1
。- 当使用输入值
3
调用composedFn
时,它在函数数组上调用Array.reduceRight()
。 reducer
函数从最右边的函数x => x + 1
开始,将它应用于累加器(最初为3
)。累加器变为3 + 1 = 4
。reducer
函数然后移到下一个函数x => x * 2
,并将其应用于累加器(现在为4
)。累加器变为4 * 2 = 8
。- 累加器的最终值
8
被返回作为组合函数的结果。
总之,通过使用 Array.reduceRight()
,我们可以用轻松简洁的方式应用函数组合。
复杂度分析
设 N 为数组中的函数数量。
时间复杂度:O(N)
。假设每个函数的时间复杂度是常数,那么数组中的每个函数都将被调用一次。
空间复杂度:O(1)
。reduceRight
方法使用累加器来存储中间结果,不需要额外的空间。
附加考虑
在处理函数组合时,一个专业的实现需要考虑更多的事情。
处理 this 上下文
在 JavaScript 中,函数内的this
值取决于如何调用函数。在使用函数组合时,重要的是考虑如何保留原始函数的this
上下文。尽管提供的测试用例可能不会明确测试这一点,但在实际场景中,正确处理原始函数的this
上下文可能非常关键。
处理 this
上下文的一种方法是在调用组合函数时使用 cal
l 或apply
方法。这允许你在调用函数时显式设置this
的值。例如:
const composedFn = function(x) {
let result = x;
for (let i = functions.length - 1; i >= 0; i--) {
result = functions[i].call(this, result);
}
return result;
};
这可以确保原始函数的this
上下文在作为组合函数的一部分被调用时得以保留。为了更彻底的理解,请你考虑这样一种情况:你合成的函数是对象上的方法,它们依赖于this
来访问该对象上的其他属性或方法。如果在组合这些方法时没有正确保存this
上下文,它们可能无法正常运行。
const obj = {
value: 1,
increment: function() { this.value++; return this.value; },
double: function() { this.value *= 2; return this.value; },
};
// 组合方法而不保留 `this`
const badCompose = function(functions) {
return function(x) {
let result = x;
for (let i = functions.length - 1; i >= 0; i--) {
result = functions[i](result);
}
return result;
};
};
const badComposedFn = badCompose([obj.increment, obj.double]);
console.log(badComposedFn(1)); // 这将返回 NaN,因为 `this` 不是 `obj` 内部的 `increment` 和 `double`
为了处理这种情况,你可以在调用组合函数时使用 call
或apply
方法,这样就可以显式设置函数调用时的this
值。
const obj = {
value: 1,
increment: function() { this.value++; return this.value; },
double: function() { this.value *= 2; return this.value; },
};
// 在保留 `this` 的同时编写方法
const goodCompose = function(functions, context) {
return function(x) {
let result = x;
for (let i = functions.length - 1; i >= 0; i--) {
result = functions[i].call(context, result);
}
return result;
};
};
const goodComposedFn = goodCompose([obj.increment, obj.double], obj);
console.log(goodComposedFn(1)); // 这是预期之中的,因为`this`是`increment`和`double`中的`obj`
使用外部库
你可以考虑使用提供组成实现功能的外部库而不是自己编写。诸如Ramda
和Lodash
之类的库提供各种实用函数,包括函数组合。通过使用知名库,你可以获得以下好处:
- 稳健性:这些库通过了广泛的测试,并被许多开发人员使用,可以确保其实现是可靠的并处理各种边缘情况。
- 性能:这些库经过性能优化,可能具有比自定义实现更好的性能特性。
- 可读性:使用流行库可以提高代码的可读性,因为其他开发人员更有可能熟悉常用外部库的函数及其行为。
- 文档:知名库通常具有全面的文档。这可以极大地简化开发过程,因为你可以快速查阅文档以获取函数解释、使用示例等信息。此外,许多现代代码编辑器都支持例如在函数上悬停以显示简要描述和指向更详细文档链接的功能。这对于方便的了解库函数的预期行为非常有帮助,而且无需不断切换到 Web 浏览器。
以下是使用Ramda
的compose
函数的示例:
import { compose } from 'ramda';
const composedFn = compose(...functions);
以及使用Lodash
的flowRight
函数的示例:
import { flowRight } from 'lodash';
const composedFn = flowRight(...functions);