How to extract all possible matching arrays of objects from one array of objects?

I have an array of objects, for example.

var arr = [ {"a": "x"}, {"b": "0"}, {"c": "k"}, {"a": "nm"}, {"b": "765"}, {"ab": "i"}, {"bc": "x"}, {"ab": "4"}, {"abc": "L"} ]; 

Let's say I'm only interested in objects whose keys match var input = ["ab", "bc"] . This means that I want to highlight all possible subarrays using result[i].length == 2 as follows:

 var result = [ [{"ab": "i"}, {"bc": "x"}], [{"ab": "4"}, {"bc": "x"}] // or [{"bc": "x"}, {"ab": "4"}] ]; 

- that is, the order of objects in subarrays is absolutely not important: I am only interested in the fact that each subarray contains two objects - {"ab": ...} and {"bc": ...} .

If I'm interested in var input = ["a","a","ab"] , the result should be like this:

 var result = [ [{"a": "x"}, {"a": "nm"}, {"ab": "i"}], [{"a": "x"}, {"a": "nm"}, {"ab": "4"}] ]; 

I can’t find a way to achieve the desired result (assuming input.length can be much more than 2 or 3 - even 15-20 may not be enough) without the number of calculations at the factorial level, which is physically impossible, is there a way to get reasonable performance for solutions to such a problem?
Important note : yes, obviously, for relatively large input.length values, it input.length theoretically be possible to have a very large number of possible combinations, but in practice, result.length will always be quite small (maybe 100-200, I even doubt that it can reach 1000 ...). But for security, I would just like to set some limit (for example, 1000), so as soon as result.length reaches this limit, the function will simply return the current result and stop.

+7
javascript arrays algorithm search sub-array
source share
5 answers

Seeing the problem, it looks like a card product. In fact, if before starting work the data model is slightly modified, the expected result in almost all cases is a product for maps. However, there is a case (the second example you provided) that needs special treatment. Here is what I did:

  • Modify the model data slightly (this will be done only once) in order to have something suitable for using the caisson product.
  • Refer to the “special case” for more than one parameter requesting the same string.

All important logic is within cartessianProdModified . Important bits in the code are commented out. Hope it helps you solve your problem or at least gives you some ideas.

Here's the fiddle and here is the code:

 var arr = [ {"a": "x"}, {"b": "0"}, {"c": "k"}, {"a": "nm"}, {"b": "765"}, {"ab": "i"}, {"bc": "x"}, {"ab": "4"}, {"abc": "L"}, {"dummy": "asdf"} ]; // Utility function to be used in the cartessian product function flatten(arr) { return arr.reduce(function (memo, el) { return memo.concat(el); }, []); } // Utility function to be used in the cartessian product function unique(arr) { return Object.keys(arr.reduce(function (memo, el) { return (memo[el] = 1) && memo; }, {})); } // It'll prepare the output in the expected way function getObjArr(key, val, processedObj) { var set = function (key, val, obj) { return (obj[key] = val) && obj; }; // The cartessian product is over so we can put the 'special case' in an object form so that we can get the expected output. return val !== 'repeated' ? [set(key, val, {})] : processedObj[key].reduce(function (memo, val) { return memo.concat(set(key, val, {})); }, []); } // This is the main function. It'll make the cartessian product. var cartessianProdModified = (function (arr) { // Tweak the data model in order to have a set (key: array of values) var processedObj = arr.reduce(function (memo, obj) { var firstKey = Object.keys(obj)[0]; return (memo[firstKey] = (memo[firstKey] || []).concat(obj[firstKey])) && memo; }, {}); // Return a function that will perform the cartessian product of the args. return function (args) { // Spot repeated args. var countArgs = args.reduce(function (memo, el) { return (memo[el] = (memo[el] || 0) + 1) && memo; }, {}), // Remove repeated args so that the cartessian product works properly and more efficiently. uniqArgs = unique(args); return uniqArgs .reduce(function (memo, el) { return flatten(memo.map(function (x) { // Special case: the arg is repeated: we have to treat as a unique value in order to do the cartessian product properly return (countArgs[el] > 1 ? ['repeated'] : processedObj[el]).map(function (y) { return x.concat(getObjArr(el, y, processedObj)); }); })); }, [[]]); }; })(arr); console.log(cartessianProdModified(['a', 'a', 'ab'])); 
+1
source share

Alphabetically arr and input , which is O (nlogn), and if you can do this when creating arrays, it might be useful for you.

Let me explain my idea with an example:

 var arr = [ {"a": "x"}, {"ab": "i"}, {"ab": "4"}, {"abc": "L"} {"bc": "x"}, ]; var input = ["ab", "bc"]; 

Find input[0] in arr (linearly or even with binary search to speed it up). Mark the index.

Find input[1] in arr , but consider only the arr subarray, by the index noted in the previous step, to the end.

If you find all input elements, then click on results (for this you can save a temporary object for it).

Now we need to search again for input[0] , as it may be that two or more entries share this key. You saved the index that I mentioned earlier, so that you start looking for this index again, and since arr sorted, you only need to check the very next element, etc.


Lead time:

Sort your arrays (if you could not sort them when creating them): O (nlogn), where n is the number of arr elements.

Arr binary search for input[0] : O (logn)

Now the next search step (for input[1] ) is much shorter than the length arr , so O (n) will be a very pessimistic estimate. In practice, it will not be O (n), of course, and if you want, you can also do a binary search for input[1] , which will cost O (logm), where m is the size of arr[index_stored: -1] .

At this point, we proceed to search for the next occurrence of input[0] , if it is finite, but since we have saved the index, we know exactly where to start the search, and we only need to check the next element that the constant cost is, therefore, O ( one).

And then we do the same for input[1] , as indicated above, which is again cheap.

Now it all depends on the length of the input , which is k , and it seems that k < n and how many occurrences of the key you have, right?

But if we assume that the situation is normal, then the whole procedure has time, supplementing:

About (NlogN)

However, note that you need to pay a little extra memory to store indexes, which depend on the number of occurrences that the key has. With brute force aglrotihm, which will be slower, you will not need to pay anything extra for memory.

+1
source share

Perhaps this is not the best way. I would probably use some library for the final solution, but here are a few steps that could do the trick for a happy journey. In the near future I will add some comments.

Create a map for one key in the source array (i.e. which indexes it shows, since we can have several records)

  function getKeyMap( src, key ){ var idx_arr = []; src.forEach(function(pair,idx){ if(Object.keys(pair)[0] === key){ idx_arr.push(idx)} }); return idx_arr; } 

And this mapping should be done for all keys that you want to be part of filtering.

 function getKeysMap( src, keys ){ var keys_map = []; keys.forEach(function(aKey){ var aMap = getKeyMap(src,aKey); if( aMap.length ){ keys_map.push(aMap); } }); // if keys map lenght is less then keys length then you should throw an exception or something return keys_map; } 

Then you want to create all possible combinations. I use recursion here, perhaps not in the most optimal way.

 function buildCombos( keys_map, carry, result ){ if( keys_map.length === 0){ result.push(carry); return; } var iter = keys_map.pop(); iter.forEach(function(key){ var cloneMap = keys_map.slice(0); var clone = carry.slice(0); clone.push(key); buildCombos(cloneMap, clone, result); }); } 

Then I need to filter the result to exclude duplicate entries, and entries with duplicate indices

 function uniqueFilter(value, index, self) { return self.indexOf(value) === index; } function filterResult( map ){ var filter = {}; map.forEach(function(item){ var unique = item.filter( uniqueFilter ); if(unique.length === item.length){ filter[unique.sort().join('')]=true;} }); return filter; } 

And then I just decode the resulting filtered map into the original data

 function decodeMap( map,src ){ var result = []; Object.keys(map).forEach(function(item){ var keys = item.split(''); var obj = []; keys.forEach(function( j ){ obj.push( src[j]) }); result.push(obj); }); return result; } 

Wrapper

 function doItAll(arr, keys){ // Get map of they keys in terms of numbers var maps = getKeysMap( arr, keys); // build combinations out of key map var combos = []; buildCombos(maps,[],combos); // filter results to get rid of same sequences and same indexes ina sequence var map = filterResult(combos); // decode map into the source array return decodeMap( map, arr ) } 

Using:

 var res = doItAll(arr, ["a","a","ab"]) 
+1
source share

If you can use ES6 functions, you can use generators to avoid creating large intermediate arrays. It would seem that you want to have a set of sets with strings containing only unique values. As already mentioned, you can approach this by starting with the Cartesian product of the objects matching your input keys:

 'use strict'; function* product(...seqs) { const indices = seqs.map(() => 0), lengths = seqs.map(seq => seq.length); // A product of 0 is empty if (lengths.indexOf(0) != -1) { return; } while (true) { yield indices.map((i, iseq) => seqs[iseq][i]); // Update indices right-to-left let i; for (i = indices.length - 1; i >= 0; i--) { indices[i]++; if (indices[i] == lengths[i]) { // roll-over indices[i] = 0; } else { break; } } // If i is negative, then all indices have rolled-over if (i < 0) { break; } } } 

The generator contains only indexes between iterations and generates new rows on demand. To actually join the objects that match your input keys, you first need to create a search:

 function join(keys, values) { const lookup = [...new Set(keys)].reduce((o, k) => { o[k] = []; return o; }, {}); // Iterate over array indices instead of objects them selves. // This makes producing unique rows later on a *lot* easier. for (let i of values.keys()) { const k = Object.keys(values[i])[0]; if (lookup.hasOwnProperty(k)) { lookup[k].push(i); } } return product(...keys.map(k => lookup[k])); } 

Then you need to filter the rows containing duplicate values:

 function isUniq(it, seen) { const notHadIt = !seen.has(it); if (notHadIt) { seen.add(it); } return notHadIt; } function* removeDups(iterable) { const seen = new Set(); skip: for (let it of iterable) { seen.clear(); for (let x of it) { if (!isUniq(x, seen)) { continue skip; } } yield it; } } 

And also globally unique strings (picking aspect):

 function* distinct(iterable) { const seen = new Set(); for (let it of iterable) { // Bit of a hack here, produce a known order for each row so // that we can produce a "set of sets" as output. Rows are // arrays of integers. const k = it.sort().join(); if (isUniq(k, seen)) { yield it; } } } 

To tie it all together:

 function* query(input, arr) { for (let it of distinct(removeDups(join(input, arr)))) { // Objects from rows of indices yield it.map(i => arr[i]); } } function getResults(input, arr) { return Array.from(query(input, arr)); } 

In action:

 const arr = [ {"a": "x"}, {"b": "0"}, {"c": "k"}, {"a": "nm"}, {"b": "765"}, {"ab": "i"}, {"bc": "x"}, {"ab": "4"}, {"abc": "L"} ]; console.log(getResults(["a", "a", "ab"], arr)); /* [ [ { a: 'x' }, { a: 'nm' }, { ab: 'i' } ], [ { a: 'x' }, { a: 'nm' }, { ab: '4' } ] ] */ 

And the required jsFiddle .

+1
source share

You can do this manually using loops, but you can also use the built-in functions Array.prototype.filter () to filter the array and Array.prototype.indexOf to check if the element is inside another array:

 var filtered = arr.filter(function(pair){ return input.indexOf(Object.keys(pair)[0]) != -1; }); 

This gives you an array only with objects that match your criteria.

Now a thing with a result array in a mathematical language is called "combinations". This is exactly what you want, so I will not describe here. Here's a way to generate all array combinations (set) - fooobar.com/questions/841974 / ...

So here is how to use this function:

 // function assumes each element is array, so we need to wrap each one in an array for(var i in filtered) { filtered[i] = [filtered[i]]; } var result = getCombinations(filtered, input.length /* how many elements in each sub-array (subset) */); 

Object.keys(pair)[0] is a way to get the first key of an object without iteration ( fooobar.com/questions/38005 / ... )

0
source share

All Articles