I use the loop-every-single-list-item approach to filter out unique elements in a given list, which tends to be very inefficient way as a list grow in size, or as function call frequency increases. Lately I was working on event handling patch and needed fast method for filtering out unique function handlers in a callback lists which got to be run quite frequently.
I've put together couple of methods I often use, and quick performance tests for unsorted, ~1k size list scanned 1k times, and compared it against underscore.js's version (which performed quite badly, ~20x slower; probably because it's trying to accomplish too much in one go).
Is there a general, more efficient way to do this task?
var uniques =
// use named function to reference added .findindex() function inside
function uniques (list) {
for (
var
// empty list to store unique items in
uniqls = [],
it = -1,
// aliases to speed up access
length = list.length,
fi = uniques.findindex;
++it < length;
// if indexes are the same it's unique item
// add the element to uniqls[]
// (subtracting indexes here speeds up the function significantly)
(fi(list, list[it]) - it) || uniqls.push(list[it]));
return uniqls;
};
// add .findindex() iterator member to .uniques()
Object.defineProperty(uniques, 'findindex' {
// make it wide open
'configurable': true,
'enumerable' : true,
'writable' : true,
'value' : function (ls, node) {
// loop until provided element is found
// skip 'update' and 'process' parts here, do just 'break test'
// (basic `for ...` loop seems to perform well here)
for (var it = -1, length = ls.length; (++it < length) && (node !== ls[it]););
// if it hit list's end no such item exists in it
// return default `-1` flag, or item's index (`it`)
return (length - it) ? it : -1;
}});
// this one is about ~2x slower then previous
// despite the fact it uses fancy 'ES5 magic'
Array.prototype.unique = (function () {
// main Array#unique method
var uni = function uni () {
return this.filter(uni.x);
};
// attach a helper for resolving unique elements
// if element is at current position, not before,
// it's unique one, pass `true` flag to .filter()
uni.x = function (node, pos, ls) {
return pos === ls.indexOf(node);
};
// save
return uniq;
})();
// and underscore performed ~20x slower because all of that functionality built in
// (they should break the method down to couple of more specialized components)
// snippet from:
// [underscore.js](https://github.com/jashkenas/underscore.git)
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator, context) {
if (array == null) return [];
if (_.isFunction(isSorted)) {
context = iterator;
iterator = isSorted;
isSorted = false;
}
if (iterator) iterator = lookupIterator(iterator, context);
var result = [];
var seen = [];
for (var i = 0, length = array.length; i < length; i++) {
var value = array[i];
if (isSorted) {
if (!i || seen !== value) result.push(value);
seen = value;
} else if (iterator) {
var computed = iterator(value, i, array);
if (_.indexOf(seen, computed) < 0) {
seen.push(computed);
result.push(value);
}
} else if (_.indexOf(result, value) < 0) {
result.push(value);
}
}
return result;
};
//
// and test all three functions:
//
// sample list:
// generate ~1K long list of integers:
// get the keys of string object of length 32,
// map every item to key-list itself,
// flatten, shuffle..
var ls =
Array.prototype.concat.apply([],
Object.keys(new String('1'.repeat(32)))).
map(function (node, pos, list) { return list; }).
sort(function () { return Math.random() < Math.random(); });
// run each function 1K times fetching unique values
for (
var
it = -1,
l = 1000,
// record iteration start
tm = Date.now();
++it < l;
// execute sequentualy:
uniques(ls)
// ls.unique()
// _.uniq(ls)
);
console.log(Date.now() - tm);
//
// 1. .uniques() : 400 - 600 ms
// 2. Array#unique : ~900ms
// 3. underscore _.uniq() : ~10s
//
// eof