如何从一个对象数组中提取所有可能匹配的对象数组?
Posted
技术标签:
【中文标题】如何从一个对象数组中提取所有可能匹配的对象数组?【英文标题】:How to extract all possible matching arrays of objects from one array of objects? 【发布时间】:2017-02-11 02:04:05 【问题描述】:我有一个对象数组,例如
var arr = [
"a": "x",
"b": "0",
"c": "k",
"a": "nm",
"b": "765",
"ab": "i",
"bc": "x",
"ab": "4",
"abc": "L"
];
假设我只对键对应于var input = ["ab", "bc"]
的对象感兴趣。这意味着我想通过以下方式提取带有result[i].length == 2
的所有可能的子数组:
var result = [
["ab": "i", "bc": "x"],
["ab": "4", "bc": "x"] // or ["bc": "x", "ab": "4"]
];
——也就是说,子数组中对象的顺序绝对不重要:我只对每个子数组包含两个对象这一事实感兴趣——"ab": ...
和"bc": ...
。
如果我对var input = ["a","a","ab"]
感兴趣,结果应该是这样的:
var result = [
["a": "x", "a": "nm", "ab": "i"],
["a": "x", "a": "nm", "ab": "4"]
];
如果没有阶乘级别的计算量,我无法找到达到预期结果的方法(假设 input.length
可能远大于 2 或 3 — 甚至 15–20 可能还不够),这在物理上是不可能的.有没有办法获得一些合理的性能来解决这样的问题?重要提示:是的,显然,对于相对较大的 input.length
值,理论上可能有非常大的数字可能的组合,但在实践中,result.length
总是相当小(可能 100-200,我什至怀疑它能否达到 1000...)。但为了安全起见,我只想设置一些限制(比如 1000),这样只要result.length
达到这个限制,函数就会返回当前的result
并停止。
【问题讨论】:
@DavinTryon:第 1 步。检查arr
是否包含"ab":value
。如果是,获取下一个"bc":value
并将它们都放入result
。步骤 2. 检查 arr
是否包含 "bc":value
。如果是,请获取下一个"ab":value
并将它们都放入result
。依此类推,这需要一个阶乘级别的可能情况。
过于复杂。 IMO 你应该改变你的数据模型,这样你就不会遇到数据过滤/转换的问题。
您能否详细说明您的方法应该如何以及为什么应该为["a", "a", "ab"]
生成示例输出? “算法”应该如何决定一个值是第一个 a 还是后者的一部分?先扫描input
然后判断a不止1个,剩下的应该由后者接收?或者您是否真的在寻找每个键的已找到对象的乘积?
@Ilja Everilä:“算法”应该如何决定一个值是第一个 a 还是后者的一部分?先扫描输入,然后确定有不止1个a,后者应该收到其余的吗? // 输入中可能有重复的字符串这一事实根本不重要。 result[i+1]
与result[i]
不同吗?是的。这才是最重要的。
与["a": "x", "a": "nm", "ab": "4"]
和["a": "x", "a": "nm", "ab": "i"]
相比,["a": "nm", "a": "x", "ab": "4"]
不是“独一无二的”,还是您对订单不感兴趣?如果有超过 2 个带有键 a 的对象,输出应该是什么?您在寻找过滤值的集合吗?
【参考方案1】:
看到这个问题,它有点像笛卡尔积。事实上,如果在操作之前,对数据模型稍作修改,预期的结果几乎在所有情况下都是笛卡尔积。但是,有一个案例(您提供的第二个示例)需要特殊处理。这是我所做的:
-
稍微调整一下模型数据(这只会做一次),以获得适合应用笛卡尔积的东西。
处理具有多个参数请求相同字符串的“特殊情况”。
所有重要的逻辑都在cartessianProdModified
内。代码中的重要位已注释。希望它可以帮助您解决问题,或者至少给您一些想法。
这是fiddle,这是代码:
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']));
【讨论】:
您能否修改生成的cartessianProdModified(str1, str2, str3...)
函数,使其接受两个参数(第一个是数据源 (arr
),第二个应该是输入)?另一种选择:它接受一个参数,即字符串数组 (input
)? (我不知道哪个选项会更好,我只需要该函数接受一个字符串数组作为输入,而不是多个字符串作为单独的参数)
@lyricallywicked 当然,我已经更新了函数,以便它可以使用字符串数组而不是参数。我认为这种方法比另一种更好,因为这种将原始数组更改为新形式的方法只需执行一次。无论如何,改变它以接受数据数组也不会有任何问题。谢谢。【参考方案2】:
排序按字母顺序排列 arr
和 input
,即 O(nlogn),如果您能够在构建数组时做到这一点,您可能会受益。
让我用一个例子来解释我的想法:
var arr = [
"a": "x",
"ab": "i",
"ab": "4",
"abc": "L"
"bc": "x",
];
var input = ["ab", "bc"];
在arr
中搜索input[0]
(线性搜索甚至使用二分搜索来加速)。标记索引。
在arr
中搜索input[1]
,但只考虑arr
的子数组,从上一步中标记的索引到它的末尾。
如果找到input
的所有元素,则将其推送到results
(您可以为此保留一个临时对象)。
现在,我们必须再次搜索input[0]
,因为可能有两个或多个条目共享该键。您将存储我之前提到的那个索引,因此您将从该索引重新开始搜索,并且由于 arr
已排序,您将只需要检查下一个元素等等。
时间复杂度:
对数组进行排序(假设在构建数组时无法对其进行排序):O(nlogn),其中 n
是 arr
拥有的元素数。
在arr
中对input[0]
进行二分搜索:O(logn)
现在下一步的搜索(对于input[1]
)远小于arr
的长度,因此非常悲观 的界限是O(n)。实际上,它当然不会是 O(n),如果你愿意,你也可以对 input[1]
进行二进制搜索,这将花费 O(logm),其中 m
是 arr[index_stored: -1]
的大小.
此时,我们继续寻找下一个出现的input[0]
,当然,如果有的话,但是因为我们已经存储了索引,所以我们知道从哪里开始搜索,我们只需要检查下一个元素,这是一个常数成本,因此 O(1)。
然后我们对input[1]
做同样的事情,这又很便宜。
现在,这完全取决于input
的长度,也就是k
,看起来k < n
,以及你有多少次出现的键,对吧?
但假设正常平均情况,整个过程的时间复杂度为:
O(nlogn)
但是,请注意,您必须支付一些额外的内存来存储索引,这取决于键的出现次数。使用 brute force aglirotihm,它会更慢,您不需要为内存支付任何额外费用。
【讨论】:
当arr
包含至少 30 个具有随机键的不同对象时,您能否估计获得结果所需的时间(假设其长度有一些限制,如问题中所述),并且input
的长度至少是 10?
@lyricallywicked 理论分析是许多假设的主题,我更新了一个非常简单的假设。希望有帮助。我的意思是你问的是一个相当新的问题! :)【参考方案3】:
也许不是最理想的方式。我可能会使用一些库作为最终解决方案,但这里有一些步骤可以帮助你走上一条快乐的道路。稍后我会添加一些 cmets。
为源数组中的单个键生成一个映射(即在哪个索引处可以看到它,因为我们可能有多个条目)
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;
并且必须为您希望成为过滤一部分的所有键完成此映射
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;
然后你想构建所有可能的组合。我在这里使用递归,可能不是最优化的方式
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);
);
然后我需要过滤结果以排除双重条目,以及具有重复索引的条目
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;
然后我简单地将生成的过滤后的地图解码为原始数据
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;
包装器
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 )
用法:
var res = doItAll(arr, ["a","a","ab"])
【讨论】:
【参考方案4】:如果您能够使用 ES6 功能,则可以使用生成器来避免创建大型中间数组。您似乎想要一组排序集,其中行仅包含唯一值。正如其他人也提到的那样,您可以从与您的 input
键匹配的对象的 cartesian product 开始:
'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;
生成器仅保存迭代之间的索引并按需生成新行。要实际加入与您的 input
键匹配的对象,您首先必须例如创建一个查找:
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]));
然后您需要过滤掉包含重复值的行:
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;
还有全局唯一的行(set-of-sets 方面):
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;
把它全部捆绑起来:
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));
在行动:
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' ] ]
*/
还有必填的jsFiddle。
【讨论】:
【参考方案5】:您可以使用循环手动执行此操作,但您也可以使用内置函数 Array.prototype.filter() 过滤数组并使用 Array.prototype.indexOf 检查元素是否在另一个数组中:
var filtered = arr.filter(function(pair)
return input.indexOf(Object.keys(pair)[0]) != -1;
);
这将为您提供仅包含符合您的条件的对象的数组。
现在result
数组在数学语言中被称为“组合”。这正是你想要的,所以我不会在这里描述它。这里给出了一种生成所有数组(集合)组合的方法 - https://***.com/a/18250883/3132718
所以这里是如何使用这个功能:
// 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]
是一种无需迭代即可获取对象第一个键的方法 (https://***.com/a/28670472)
【讨论】:
我应该如何使用它?如何获得所需的result
(如问题中所述),例如,对于上述arr
和input = ["a","a","ab"]
?
什么是getCombinations
?这是 O(n!) 的某个函数吗?如果是,那将是不可接受的。
查看实现(答案中的链接)。在您的情况下,它可以简化,因为您的元素不是数组而是“单个”对象。
问题是这个getCombinations(filtered, input.length)
至少需要factorialOf(input.length)
计算。以上是关于如何从一个对象数组中提取所有可能匹配的对象数组?的主要内容,如果未能解决你的问题,请参考以下文章