Update
I misread the specifications originally. Below this section I detail several solutions to a different, but related, problem. Here is a solution to the requested one:
const rec = (arr, uniqs = new Set, dups = new Set, [x, ...xs] = arr) =>
arr.length == 0
? [[...uniqs], [...dups]]
: uniqs.has(x) || dups.has(x)
? rec(xs, (uniqs.delete(x), uniqs), dups.add(x))
: rec(xs, uniqs.add(x), dups)
console.log(rec([2, "str", "arr", "str", 2, "obj", "arg"]));
console.log(rec(["ref", "val", "val", "val", "el", "el"]));
Note that there were two changes from the final original answer:
We switched to a Set for the dups
as well, with the changes required by that.
This line now deletes from the uniqs
Set rather than just passing it along :
? rec(xs, (uniqs.delete(x), uniqs), dups.add(x))
This actually points to an API issue with Set
. It's really useful that add
returns the set. It's too bad that delete
doesn't also do so. While the boolean return might occasionally be helpful, it's quite easy to get that with has
. It's far too late to fix this, but it really is a shame.
Original Answer
Here is one possible approach. ES6 features such as rest parameters and default parameters alongside a Symbol
makes for a fairly elegant implementation.
const None = Symbol()
const rec = ([x = None, ...xs], uniqs = [], dups = []) =>
x == None
? [uniqs, dups]
: uniqs.includes(x)
? rec(xs, uniqs, dups.concat(x))
: rec(xs, uniqs.concat(x), dups)
console.log(rec([2, "str", "arr", "str", 2, "obj", "arg"]));
console.log(rec(["ref", "val", "val", "val", "el", "el"]));
One real help for recursion in modern JS is the ability to use default parameters to allow you to avoid additional helper functions.
The use of the Symbol
here is an interesting way to combine our spread of the input array into a head and a tail and still allow us to test if it's empty. An alternative means is shown below.
const rec = (arr, uniqs = [], dups = [], [x, ...xs] = arr) =>
arr.length == 0
? [uniqs, dups]
: uniqs.includes(x)
? rec(xs, uniqs, dups.concat(x))
: rec(xs, uniqs.concat(x), dups)
console.log(rec([2, "str", "arr", "str", 2, "obj", "arg"]));
console.log(rec(["ref", "val", "val", "val", "el", "el"]));
This version feels slightly less clean than the original, but it does not require defining a helper symbol just to check for emptiness.
There is still something wrong with this; it could have performance problems. Because we have to call the O(n)
includes
for every element, the total solution is somthing like O(n^2)
, depending on the ration of unique values to duplicates. This might not be a problem, and I would only fix it in one of two scenarios:
- I've tested and found that this code is an actual bottleneck in my application
- I have a more performant alternative version that sacrifices little code clarity.
In this case, I do have such a version:
const rec = (arr, uniqs = new Set(), dups = [], [x, ...xs] = arr) =>
arr.length == 0
? [[...uniqs], dups]
: uniqs.has(x)
? rec(xs, uniqs, dups.concat(x))
: rec(xs, uniqs.add(x), dups)
console.log(rec([2, "str", "arr", "str", 2, "obj", "arg"]));
console.log(rec(["ref", "val", "val", "val", "el", "el"]));
(We could also use the Symbol
alternative in this version.)
Here, we switch to using a Set
, which is precisely the data type designed for working with a collection of unique elements. That necessitates doing a little unwrapping of the set, with [...uniqs]
-- alternatively Array.from(uniqs)
-- but that is a very minor increase in complexity. The other changes just have to do with switching from an array to a set.