7

I have an array like this:

let x = [[1, 2], [3, 4], [1, 2], [2, 1]];

What should I do to retrieve an array without the duplicates?

[[1, 2], [3, 4], [2, 1]];

I would like to use the filter method. I tried this but it doesn't work:

x.filter((value,index,self) => (self.indexOf(value) === index))

EDIT: as I specified to use the filter method, I don't think this question is a duplicate. Also, I got several interesting answers.

Snorlite
  • 327
  • 1
  • 3
  • 12
  • https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates?rq=1 – Peter Aug 19 '19 at 19:11
  • Oof. This is a hard one. I can think of a few different ways to force it, but nothing eloquent. First heavy handed idea, is to not use filter and instead use a for loop. – Seph Reed Aug 19 '19 at 19:12
  • @SephReed How about a Set combined with a hashing function? – user1984 Aug 19 '19 at 19:14
  • If we don't use a hashing function and a Set, we have to loop over the whole array for every element. This would be slower when the array elements increase. – user1984 Aug 19 '19 at 19:17
  • 2
    Have a look at [How to compare arrays in JavaScript?](https://stackoverflow.com/q/7837456/1048572) and the close-to-duplicate [Evaluating Javascript Arrays](https://stackoverflow.com/q/2592305/1048572) and [Why Array.indexOf doesn't find identical looking objects](https://stackoverflow.com/q/12604062/1048572) – Bergi Aug 20 '19 at 04:53

6 Answers6

12

Try converting the inner arrays to a string, then filter the dupes and parse the string again.

let x = [[1, 2], [3, 4], [1, 2]];

var unique = x.map(ar=>JSON.stringify(ar))
  .filter((itm, idx, arr) => arr.indexOf(itm) === idx)
  .map(str=>JSON.parse(str));

console.log(unique);
I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
  • 5
    Much simpler: `Array.from(new Set(arr.map(x=>JSON.stringify(x))), x=>JSON.parse(x))` – Bergi Aug 20 '19 at 04:04
  • 1
    ```[...new Set([1, 1, 1, 1, 2])]``` will return an array that looks like `[1, 2]` – b26 Jan 13 '20 at 20:35
9

Filter just causes things to get into O(n^2).

The currently accepted answer uses .filter((itm, idx, arr) => arr.indexOf(itm) === idx) which will cause the array to be iterated each time during each iteration... n^2.

Why even go there? Not only that, you need to parse in the end. It is a lot of excess.

There is no real good way to use filter without hitting O(n^2) here, so if performance is the goal is should probably be avoided.


Instead, just use reduce. It is very straightforward and fast easily accomplishing O(n).

"Bin reduce the set to unique values."

let x = [[1, 2], [3, 4], [1, 2], [2, 1]];
let y = Object.values(x.reduce((p,c) => (p[JSON.stringify(c)] = c,p),{}));
console.log(y);

In case it isn't as clear, here is a more readable version of the bin reduction.

// Sample Data
let dataset = [[1, 2], [3, 4], [1, 2], [2, 1]];

// Create a set of bins by iterating the dataset, which
// is an array of arrays, and structure the bins as
//     key: stringified version of the array
//     value: actual array
let bins = {};

// Iteration
for(let index = 0; index < dataset.length; index++){
 // The current array, from the array of arrays
 let currentArray = dataset[index];
 
 // The JSON stringified version of the current array
 let stringified = JSON.stringify(currentArray);
 
 // Use the stringified version of the array as the key in the bin,
 // and set that key's value as the current array
 bins[stringified] = currentArray;
}

// Since the bin keys will be unique, so will their associated values. 
// Discard the stringified keys, and only take the set of arrays to
// get the resulting unique set.
let results = Object.values(bins);

console.log(results);

If you were to have to go the route of filter, then n^2 must be used. You can iterate each item looking for existence using every.

"Keep every element which does not have a previous duplicate."

let x = [
  [1, 2],
  [3, 4],
  [1, 2],
  [2, 1]
];
let y = x.filter((lx, li) =>
  x.every((rx, ri) =>
    rx == lx ||
    (JSON.stringify(lx) != JSON.stringify(rx) || li < ri))
);
console.log(y);
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • Thank you for providing an alternate approach and explaining why it is more efficient. I really want to implement this in something I am working on but as a new JavaScript learner, I can't do so until I understand how it works. I get lost when trying to understand how the accumulator array's element is getting set: (p[JSON.stringify(c)] = c,p). What's happening on the right side of the function ( = c,p )? And why is it imperative to use an empty object {} as the initial value of the reduce method? – dreamwork Dec 18 '20 at 23:59
  • The initial value of `{}` is used to ensure that a lookup can be used during the iteration. As an object key is unique, the lookup is O(1) and that is how you can get away from being forced into O(n^2). I used the arrow function shorthand notation since that seemed to be what most people are interested in. – Travis J Dec 19 '20 at 00:17
  • In the expression `(p[JSON.stringify(c)] = c,p)`, the first half assigns the string value of the array as the key in our object initially referenced, and assigns the array itself as the value to that key, the second half, `,p)` is shorthand which evaluates simply to `p` and that is what is returned. `p` in this case being the initial object that we are modifying. – Travis J Dec 19 '20 at 00:17
  • Thanks for the lesson! Lots to learn here! – onit Nov 03 '22 at 13:17
4

Okay, the string hash idea is brilliant. Props to I wrestled a bear once. I think the code itself could be a bit better though, so here's how I tend to do this type of thing:

let x = [[1, 2], [3, 4], [1, 2]];
const map = new Map();
x.forEach((item) => map.set(item.join(), item));
console.log(Array.from(map.values()));

And if you want an ugly one liner:

let x = [[1, 2], [3, 4], [1, 2]];
const noRepeats = Array.from((new Map(x.map((item) => [item.join(), item]))).values());
console.log(noRepeats);
Seph Reed
  • 8,797
  • 11
  • 60
  • 125
1

This is a solution with time complexity of O(n) where n is the number of elements in your array.

Using the filter method as the OP wants it:

    const x = [[1, 2], [3, 4], [1, 2], [2, 1]];
    const s = new Set();


    const res = x.filter(el => {
      if(!s.has(el.join(""))) {
        s.add(el.join(""));
        return true;
      }
      return false
    })

    console.log(res)

My personal preference here is to use ForEach as it looks more readable.

const x = [[1, 2], [3, 4], [1, 2], [2, 1]];
const s = new Set();
const res = [];

x.forEach(el => {
  if(!s.has(el.join(""))) {
    s.add(el.join(""));
    res.push(el)
  }
})

console.log(res);

We are using a Set and a simple combination of the elements of the array to make sure they are unique. Otherwise this would become O(n^2).

user1984
  • 5,990
  • 2
  • 13
  • 32
  • what is the inner array is more than two elements? – I wrestled a bear once. Aug 19 '19 at 19:35
  • if it's dynamic, we have to add another level of loop but this is still faster than a nested loop that works on the whole array twice. This would become O(n*m) where n === arr.length and m===arr[0].length, assuming that they are of the same length. – user1984 Aug 19 '19 at 19:38
  • 1
    it doesn't require another loop. consider changing `el[0]+""+el[1]` to `el.join("")` see [`array.join`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join). – I wrestled a bear once. Aug 19 '19 at 19:40
  • 2
    `.join("")` doesn't work. Try `x = [[12,0], [1,20]]`. – Bergi Aug 20 '19 at 04:05
0

indexOf does not work on identical instances of arrays/objects type elements within an array, as such arrays just hold references.

In filter function instance you get via parameter v (in below code) is not the same instance as stored in array, making indexOf unable to return the index of it.

In below code, by converting objects to strings we can use indexOf to find duplicates.

let x = [[1, 2], [3, 4], [1, 2], [2, 1]];

console.log(x.
  map(function(v){
    return JSON.stringify(v)
  })
  .filter(function(v, i, o) {
    return o.length == i ? true : o.slice(i + 1).indexOf(v) == -1;
  })
  .map(function(v) {
    return JSON.parse(v)
  })
);
Akash Shrivastava
  • 1,365
  • 8
  • 16
0

The equivalent to

x.filter((value,index,self) => (self.indexOf(value) === index))

would be

x.filter((v,i,self) => {
for1:
  for (let j = 0; j < self.length; j++) {
    if (i == j) {
      return true;
    }
    if (self[j].length != v.length) {
      continue;
    }
    for (let k = 0; k < v.length; k++) {
      if (self[j][k] != v[k]) {
        continue for1;
      }
    }
    return false;
  }
  return true;
})

Unlike some of the other answers, this does not require a conversion to string and can thus work with more complex values. Use === instead of == if you want.

The time complexity is not great, of course.

Andrea
  • 19,134
  • 4
  • 43
  • 65
  • 1
    solid answer, +1, but it doesn't work if the array contains objects whereas the json approaches do. also, [`self`](https://developer.mozilla.org/en-US/docs/Web/API/Window/self) is reserved in javascript, ideally that should have a different name. – I wrestled a bear once. Aug 19 '19 at 19:52
  • So you are saying it is *not* equivalent, because your version actually works? – Bergi Aug 20 '19 at 04:07
  • 1
    @Iwrestledabearonce. `self` is totally fine as a *local* variable name. – Bergi Aug 20 '19 at 04:08
  • Why use these overcomplicated loops instead of simply exchanging `self.indexOf(value)` for `self.findIndex(x => x[0]==value[0] && x[1]==value[1])` (or even using `every` inside there if you want to support arbitrary-length arrays)? – Bergi Aug 20 '19 at 04:09
  • @Iwrestledabearonce. This should work with objects, but they would have to be equal by identity, not just by value. – Andrea Aug 20 '19 at 07:58