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.

5
  • stackoverflow.com/questions/1960473/… Commented Aug 19, 2019 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. Commented Aug 19, 2019 at 19:12
  • @SephReed How about a Set combined with a hashing function? Commented Aug 19, 2019 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. Commented Aug 19, 2019 at 19:17
  • 2
    Have a look at How to compare arrays in JavaScript? and the close-to-duplicate Evaluating Javascript Arrays and Why Array.indexOf doesn't find identical looking objects Commented Aug 20, 2019 at 4:53

6 Answers 6

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);

Sign up to request clarification or add additional context in comments.

2 Comments

Much simpler: Array.from(new Set(arr.map(x=>JSON.stringify(x))), x=>JSON.parse(x))
[...new Set([1, 1, 1, 1, 2])] will return an array that looks like [1, 2]
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);

3 Comments

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?
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.
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.
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);

Comments

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).

4 Comments

what is the inner array is more than two elements?
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.
it doesn't require another loop. consider changing el[0]+""+el[1] to el.join("") see array.join.
.join("") doesn't work. Try x = [[12,0], [1,20]].
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.

5 Comments

solid answer, +1, but it doesn't work if the array contains objects whereas the json approaches do. also, self is reserved in javascript, ideally that should have a different name.
So you are saying it is not equivalent, because your version actually works?
@Iwrestledabearonce. self is totally fine as a local variable name.
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)?
@Iwrestledabearonce. This should work with objects, but they would have to be equal by identity, not just by value.
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)
  })
);

6 Comments

indexof works perfectly fine on arrays. jsbin.com/qerubadoqo/edit?js,console
not if its an array of arrays, you cant find index of an array within an array using indexOf
if you took 15 seconds to click on the link i gave you would see that that statement is 100% incorrect. indexof works perfectly fine with arrays.
you passed a reference in indexOf, not an array, try replacing 'a' with [2, 3] and see if it works
i removed my vote but your explanation is still not accurate. indexOf does "work" on arrays, but it doens't search for identical arrays, it's searches for the exact instance of the array you tell it to search for. you're on the right track using the word "reference" though.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.