Skip to content

Commit dd729b2

Browse files
committed
Write-up for LeetCode problem 2626. Array Reduce Transformation (#410)
1 parent 866ebb6 commit dd729b2

File tree

16 files changed

+262
-13
lines changed

16 files changed

+262
-13
lines changed

workspaces/javascript-leetcode-month/problems/2626-array-reduce-transformation/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,10 @@
22

33
[View Problem on LeetCode](https://leetcode.com/problems/array-reduce-transformation/)
44

5+
This is yet another problem asking us to re-implement some built-in JavaScript function. Here, it's actually slightly simpler than the built-in -- whereas [`Array.prototype.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) also passes an index to the reducing function, in this problem we don't care about the index, only the value.
6+
7+
We can get a quick accept if we ignore the problem statement's request and use the built-in`.reduce`.
8+
9+
For a more serious solution, we can go with any kind of loop over the array values, updating a result during the loop.
10+
511
Once you've worked on the problem, check out [the full write-up and solution](solution.md)!

workspaces/javascript-leetcode-month/problems/2626-array-reduce-transformation/solution.md

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
11
# 2626. Array Reduce Transformation
22

3-
[View this Write-up on LeetCode TODO](https://leetcode.com/problems/array-reduce-transformation/solutions/) | [View Problem on LeetCode](https://leetcode.com/problems/array-reduce-transformation/)
3+
[View this Write-up on LeetCode](https://leetcode.com/problems/array-reduce-transformation/solutions/5751796/content/) | [View Problem on LeetCode](https://leetcode.com/problems/array-reduce-transformation/)
44

5-
> [!WARNING]
5+
> \[!WARNING]\
66
> This page includes spoilers. For a spoiler-free introduction to the problem, see [the README file](README.md).
77
88
## Summary
99

10+
The solution needs to process all the array elements and pass them through a reducing function. Unlike the [`Array.prototype.reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) built-in we're replicating, we don't have to pass in an index to the reducer function, but order still matters.
11+
12+
Any approach that processes elements in the appropriate order will work well. I'd recommend an iterative solution using a simple loop, though in the context of interview prep it may be worthwhile to become comfortable with recursive implementations.
13+
14+
We can also get accepted by simply using the built-in `.reduce`, since LeetCode doesn't enforce that we don't. Or, we could use [`.reduceRight`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight) instead and claim that it's technically not `.reduce`.
15+
1016
## Background
1117

18+
> \[!TIP]\
19+
> If you've been following [my recommended order for LeetCode's JavaScript problems](https://github.com/code-chronicles-code/leetcode-curriculum/tree/reduce-write-up/workspaces/javascript-leetcode-month) you might have already encountered the following discussion of reducing in [another write-up](../2635-apply-transform-over-each-element-in-array/solution.md). Feel free to skip ahead to the solutions.
20+
21+
### Reducing
22+
23+
If you're a veteran of [functional programming](https://en.wikipedia.org/wiki/Functional_programming) you likely know about the concept of reducing, also known as folding. If not, it's useful to learn, because it's the concept behind popular JavaScript libraries like [Redux](https://redux.js.org/) and the handy [`useReducer` React hook](https://react.dev/reference/react/useReducer).
24+
25+
A full explanation of reducing is beyond the scope of this write-up, but the gist of it is that we can use it to combine some multi-dimensional data in some way, and _reduce_ its number of dimensions. To build up your intuition of this concept, imagine that we have some list of numbers like 3, 1, 4, 1, 5, 9 and we decide to reduce it using the addition operation. That would entail putting a + between adjacent numbers to get 3 + 1 + 4 + 1 + 5 + 9 which reduces the (one-dimensional) list to a single ("zero-dimensional") number, 23. If we were to reduce the list using multiplication instead, we'd get a different result. So the reduce result depends on the operation used.
26+
27+
In the above examples, both addition and multiplication take in numbers, two at a time, and combine them into one. The reduce works by repeatedly applying the operation until there's only one number left. However, the result of a reduce operation doesn't have to be of the same type as the elements of the list. In TypeScript terms, a "reducer" function should satisfy the following signature, using generics:
28+
29+
```typescript []
30+
type Reducer<ResultType, ElementType> = (
31+
accumulator: ResultType,
32+
element: ElementType,
33+
) => ResultType;
34+
```
35+
36+
In other words, it should take an "accumulator" of type `ResultType` and an element of type `ElementType` and combine them into a new value of type `ReturnType`, where `ResultType` and `ElementType` will depend on the context. This allows for much more complex operations to be expressed as a reduce. In cases where `ResultType` and `ElementType` are different, we will also need to provide an initial value for the accumulator, so we have a value of type `ResultType` to kick off the reduce -- without an initial value the first step of the reduce is to combine the first two elements of the list, which wouldn't align with the reducer's signature.
37+
38+
The initial value also ensures a meaningful result when trying to reduce an empty list. For example, the sum of an empty list is usually defined to be zero, and we can achieve this by specifying an initial value of zero when expressing summing as a reduce via addition.
39+
1240
## Solutions
1341

42+
It's solution time!
43+
1444
### Using `Array.prototype.reduce`
1545

46+
Let's kick off using the built-ins, for some quick gratification. Delegating to the built-in is very concise in pure JavaScript:
47+
1648
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378766665/)
1749

1850
```javascript []
@@ -25,6 +57,8 @@
2557
const reduce = (arr, fn, init) => arr.reduce(fn, init);
2658
```
2759

60+
In TypeScript, let's modify LeetCode's solution template and generalize our function using generics. We'll use one type parameter for the type of the array elements and one for the type of the result, as described in the Background section.
61+
2862
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378765352/)
2963

3064
```typescript []
@@ -35,6 +69,8 @@ const reduce = <TElement, TResult>(
3569
): TResult => arr.reduce(fn, init);
3670
```
3771

72+
We can also get weird. If you're new to JavaScript, please ignore this next solution, it's not meant to be easy to interpret.
73+
3874
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378767802/)
3975

4076
```javascript []
@@ -47,6 +83,12 @@ const reduce = <TElement, TResult>(
4783
const reduce = Function.prototype.call.bind(Array.prototype.reduce);
4884
```
4985

86+
Understanding why the above works was a bonus question in [another write-up](../2635-apply-transform-over-each-element-in-array/solution.md). Read about it there if you're curious, but otherwise don't worry about this code too much.
87+
88+
### Using `Array.prototype.reduceRight`
89+
90+
For a clearer conscience, we can also go with `.reduceRight`, but we'll have to reverse the array to make sure elements are processed in the appropriate order. The built-in `.reverse` mutates the array it's invoked on, so if we want to avoid mutating the input, we'll have to copy it, using [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) for example.
91+
5092
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378777231/)
5193

5294
```typescript []
@@ -57,6 +99,8 @@ const reduce = <TElement, TResult>(
5799
): TResult => [...arr].reverse().reduceRight(fn, init);
58100
```
59101

102+
We can also use the more recently-added [`.toReversed`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toReversed) to get a reversed copy of the input. The function is available in LeetCode's JavaScript environment, but TypeScript doesn't know that it is, so we'll have to help it out by adding appropriate type definitions to the built-in interfaces for `Array` and `ReadonlyArray`.
103+
60104
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378778477/)
61105

62106
```typescript []
@@ -77,8 +121,13 @@ const reduce = <TElement, TResult>(
77121
): TResult => arr.toReversed().reduceRight(fn, init);
78122
```
79123

124+
> \[!NOTE]\
125+
> **What would happen if we used `.reduceRight` without reversing?** Can you think of a reducer function that would break? My answer is at the bottom of this write-up.
126+
80127
### Iterative
81128

129+
The iterative solutions are the ones I recommend for this problem, because I think they read quite nicely. For example, a simple `for...of` loop works great, since we don't care about indexes:
130+
82131
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378776182/)
83132

84133
```typescript []
@@ -97,6 +146,8 @@ function reduce<TElement, TResult>(
97146
}
98147
```
99148

149+
Alternatively, try a `.forEach`:
150+
100151
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378776438/)
101152

102153
```typescript []
@@ -115,7 +166,9 @@ function reduce<TElement, TResult>(
115166
}
116167
```
117168

118-
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378781727/)
169+
If we don't mind mutating the input array, we can also remove elements from it, one by one, and destructively reduce to a result:
170+
171+
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1382531301/)
119172

120173
```typescript []
121174
function reduce<TElement, TResult>(
@@ -127,26 +180,35 @@ function reduce<TElement, TResult>(
127180

128181
arr.reverse();
129182
while (arr.length > 0) {
130-
res = fn(res, arr.pop());
183+
res = fn(res, arr.pop() as TElement);
131184
}
132185

133186
return res;
134187
}
135188
```
136189

190+
Note that we need to help TypeScript understand that `arr.pop()` returns a `TElement` in this case, otherwise TypeScript would worry that it's `TElement | undefined`. Using [TypeScript's non-null assertion operator](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-) (!) wouldn't be entirely accurate if `undefined` is a valid `TElement` (i.e. if the array is allowed to contain `undefined` values).
191+
192+
> \[!NOTE]\
193+
> **Why use `.reverse` and `.pop` instead of `.shift` in the destructive implementation?** The answer is at the bottom of this doc.
194+
137195
### Recursive
138196

139-
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378782688/)
197+
In a recursive solution, a nice base case would be an empty array, wherein we can simply return `init`. For non-empty arrays, each recursion step should process one element from the array, removing it, so the array size is reduced by one and we're inching closer to the empty array base case. We annotate the result of the `.shift` just like we did with the `.pop`:
198+
199+
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1382534272/)
140200

141201
```typescript []
142202
const reduce = <TElement, TResult>(
143203
arr: TElement[],
144204
fn: (accumulator: TResult, element: TElement) => TResult,
145205
init: TResult,
146206
): TResult =>
147-
arr.length === 0 ? init : reduce(arr, fn, fn(init, arr.shift()));
207+
arr.length === 0 ? init : reduce(arr, fn, fn(init, arr.shift() as TElement));
148208
```
149209

210+
However, the code above has quadratic time complexity, because we're doing a `.shift` (which is a linear operation) within the linear operation of processing all the array elements. To achieve a linear solution, we should avoid repeatedly removing from the front of the array. We can instead rely on an index to track our progress, defaulting it to 0, so that users of our function aren't forced to provide it:
211+
150212
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378769940/)
151213

152214
```typescript []
@@ -161,23 +223,82 @@ const reduce = <TElement, TResult>(
161223
: reduce(arr, fn, fn(init, arr[index]), index + 1);
162224
```
163225

164-
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1378772700/)
226+
Note that adding another argument to the function in this way can be considered polluting the API. One could argue that not only should users of our function not _have_ to provide an index, they shouldn't even be _able_ to, but with the above code they are. We can address this by hiding the index in an inner function:
227+
228+
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1382527902/)
165229

166230
```typescript []
167231
function reduce<TElement, TResult>(
168232
arr: readonly TElement[],
169233
fn: (accumulator: TResult, element: TElement) => TResult,
170234
init: TResult,
171235
): TResult {
172-
const doReduce = (accumulator: TResult, index: number) =>
236+
const doReduce = (accumulator: TResult, index: number): TResult =>
173237
index === arr.length
174238
? accumulator
175239
: doReduce(fn(accumulator, arr[index]), index + 1);
240+
176241
return doReduce(init, 0);
177242
}
178243
```
179244

245+
All of the above considerations are why I recommend a simple iterative solution using a `for...of` loop. However, I think we can get an elegant recursive solution (that's still linear in time complexity) by going through our own `reduceRight`. Since we're destroying the input anyway, we don't have to worry that `.reverse` mutates the array it's invoked on:
246+
247+
[View submission on LeetCode](https://leetcode.com/problems/array-reduce-transformation/submissions/1382527405/)
248+
249+
```typescript []
250+
const reduceRight = <TElement, TResult>(
251+
arr: TElement[],
252+
fn: (accumulator: TResult, element: TElement) => TResult,
253+
init: TResult,
254+
): TResult =>
255+
arr.length === 0
256+
? init
257+
: reduceRight(arr, fn, fn(init, arr.pop() as TElement));
258+
259+
const reduce = <TElement, TResult>(
260+
arr: TElement[],
261+
fn: (accumulator: TResult, element: TElement) => TResult,
262+
init: TResult,
263+
): TResult => reduceRight(arr.reverse(), fn, init);
264+
```
265+
180266
## Answers to Bonus Questions
181267

182-
> [!TIP]
183-
> Thanks for reading! If you enjoyed this write-up, feel free to [up-vote it on LeetCode TODO](https://leetcode.com/problems/array-reduce-transformation/solutions/)! 🙏
268+
1. **What would happen if we used `.reduceRight` without reversing the array to implement reducing?**
269+
270+
It would still work correctly some of the time, for example if the reducer is [commutative](https://en.wikipedia.org/wiki/Commutative_property) and [associative](https://en.wikipedia.org/wiki/Associative_property), like summing: `(accumulator, element) => accumulator + element`.
271+
272+
However, order matters if our reducer doesn't have these properties, for example `(accumulator, element) => 2 * accumulator + element`. Elements that are combined into the accumulator earlier will experience more multiplications by 2:
273+
274+
```javascript []
275+
const reducer = (accumulator, element) => 2 * accumulator + element;
276+
const arr = [3, 1, 4];
277+
278+
// The running values with a standard `.reduce` will be:
279+
// 0
280+
// 2 * 0 + 3 = 3
281+
// 2 * (2 * 0 + 3) + 1 = 7
282+
// 2 * (2 * (2 * 0 + 3) + 1) + 4 = 18
283+
console.log(arr.reduce(reducer, 0)); // prints 18
284+
285+
// The running values with a `.reduceRight` will be:
286+
// 0
287+
// 2 * 0 + 4 = 4
288+
// 2 * (2 * 0 + 4) + 1 = 9
289+
// 2 * (2 * (2 * 0 + 4) + 1) + 3 = 21
290+
console.log(arr.reduceRight(reducer, 0)); // prints 21
291+
```
292+
293+
2. **Why use `.reverse` and `.pop` instead of `.shift` in the destructive iterative implementation?**
294+
295+
The answer was also discussed in the section on recursive solutions.
296+
297+
It's a matter of time complexity. Removing from the back of an array is cheaper than removing from the front. As its name highlights, `.shift` involves shifting the entire contents of the array in order to reindex, making it O(N) for an array of size N. By contrast, `.pop` only needs to remove the last element, without impacting the other positions in the array, so its cost is O(1).
298+
299+
A reverse is also O(N) since it processes the whole array, but we can pay it as a one-time up-front cost, and then we can use exclusively O(1) removes from the back for the rest of the implementation. If we instead did `.shift` within an O(N) loop, the overall complexity would become quadratic.
300+
301+
> \[!TIP\]
302+
> Thanks for reading! If you enjoyed this write-up, feel free to [up-vote it on LeetCode](https://leetcode.com/problems/array-reduce-transformation/solutions/5751796/content/)! 🙏
303+
304+
<!-- TODO: I think it would be nice to include some solutions that allow `init` to be optional, to show how that would work -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function reduce<TElement, TResult>(
2+
arr: TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult {
6+
let res = init;
7+
8+
arr.reverse();
9+
while (arr.length > 0) {
10+
res = fn(res, arr.pop() as TElement);
11+
}
12+
13+
return res;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
function reduce<TElement, TResult>(
2+
arr: readonly TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult {
6+
let res = init;
7+
8+
arr.forEach((element) => {
9+
res = fn(res, element);
10+
});
11+
12+
return res;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
function reduce<TElement, TResult>(
2+
arr: readonly TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult {
6+
let res = init;
7+
8+
for (const element of arr) {
9+
res = fn(res, element);
10+
}
11+
12+
return res;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const reduce = <TElement, TResult>(
2+
arr: TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult =>
6+
arr.length === 0 ? init : reduce(arr, fn, fn(init, arr.shift() as TElement));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function reduce<TElement, TResult>(
2+
arr: readonly TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult {
6+
const doReduce = (accumulator: TResult, index: number): TResult =>
7+
index === arr.length
8+
? accumulator
9+
: doReduce(fn(accumulator, arr[index]), index + 1);
10+
11+
return doReduce(init, 0);
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const reduceRight = <TElement, TResult>(
2+
arr: TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult =>
6+
arr.length === 0
7+
? init
8+
: reduceRight(arr, fn, fn(init, arr.pop() as TElement));
9+
10+
const reduce = <TElement, TResult>(
11+
arr: TElement[],
12+
fn: (accumulator: TResult, element: TElement) => TResult,
13+
init: TResult,
14+
): TResult => reduceRight(arr.reverse(), fn, init);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const reduce = <TElement, TResult>(
2+
arr: readonly TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
index: number = 0,
6+
): TResult =>
7+
index === arr.length
8+
? init
9+
: reduce(arr, fn, fn(init, arr[index]), index + 1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const reduce = <TElement, TResult>(
2+
arr: readonly TElement[],
3+
fn: (accumulator: TResult, element: TElement) => TResult,
4+
init: TResult,
5+
): TResult => [...arr].reverse().reduceRight(fn, init);

0 commit comments

Comments
 (0)