如何使用回溯来启发式匹配价值集合?

时间:2017-05-27 19:22:53

标签: javascript

我有一个有趣的问题,我不确定如何解决。我精通回溯类型的功能,但我被以下难题困扰:

假设我们有一个对象集合,每个对象都定义了一个url部分数组,如['/root', '/nested', '/nested', '/leaf']。给定一组url部分,找到与url部分完全匹配的对象。这很简单,但这里是踢球者:对象定义的url部分也可以是通配符,如['/root', '/:id', '/nested', ':name', '/leaf']。现在它变得有趣了,因为完全匹配比通配符匹配更重要,并且应尽可能长时间地遵循完全匹配,即使它将以最多通配符结束。

我想用一个启发式函数来评估每个路由匹配需要一些递归。但我不确定如何开始或如何确保始终找到有效匹配。

这就是我要找的东西:



var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);

function matchByUrlParts(urlParts) {
  return 'not implemented';
}




感谢任何帮助让我开始。

2 个答案:

答案 0 :(得分:1)

您可以为每个对象构建一个模式字符串,每个对象的每个URL部分都有一个字符。所以这样的模式的字符串长度将是urlPart数组的大小。每个字符都是“0”或“1”。如果相应的URL部分是通配符,则它应为“1”。

示例:鉴于此对象:

{ id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },

相应的模式是:

"0101"

...其中“1”字符表示通配符的位置。

搜索首先尝试非通配符部分的要求,可以通过按字母顺序查找模式首先出现的匹配来实现。

由于与对象关联的这种模式(成本)不依赖于输入,而只依赖于对象数组,因此可以在处理任何输入之前计算它,并使用这些模式扩展对象。然后,您可以按该模式对对象进行排序。

有了它,您只需找到输入的第一个匹配项,您就会知道它是最佳匹配项(因为您对对象进行了排序)。

这个ES6函数实现了这个想法:

function preprocess() {
    objects.forEach( o => 
        o.pattern = o.urlParts.map( part => +part.startsWith("/:") ).join('')
    );
    objects.sort( (a, b) => a.pattern.localeCompare(b.pattern) );
}

function matchByUrlParts(input) {
    const found = objects.find( ({id, urlParts, pattern}) => {
        return urlParts.length == input.length
            && input.every( (part, i) => urlParts[i] == part || pattern[i] == "1") 
    });
    return found && found.id || null;
}

var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

preprocess();

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);
.as-console-wrapper { max-height: 100% !important; top: 0; }

解释

在预处理期间,part.startsWith("/:")的结果将转换为带有一元+的数字,产生0或1. map函数返回一个0和1位的数组,然后将它们连接成模式字符串。此模式存储在每个原始对象的新pattern属性中。然后,这些对象使用sort()回调函数按该新属性进行排序。

第二个函数非常简单:它使用find遍历排序的对象数组,检查每个对象的两个条件:

  • 它应该具有与输入
  • 一样多的URL部分
  • 每个部分应匹配或相应的对象部分应为通配符
当找到匹配项时,

find将停止迭代并返回相应的对象。然后,该函数将返回该对象的id属性,如果没有匹配则返回null

关于模式

零和一的模式可能会变得很长。如果对象和输入有100个URL部分,那么这个模式系统就没问题了:一个字符串很容易长100个字符并进行比较。但是,如果您使用数字实现此操作,则会遇到准确性问题(因为浮点精度有限)。

答案 1 :(得分:0)

这是我自己的算法版本,似乎可以解决问题:



function matchByUrlParts(urlParts) {
  let lowest = objects.reduce((lowest, obj) => {
      var cost = obj.urlParts.length == urlParts.length && appraiseRouteCost(obj.urlParts, urlParts);
      var foundMatch = typeof cost == 'number' && !isNaN(cost);
      return (foundMatch && (!lowest || cost < lowest.cost) && {'obj':obj,'cost':cost}) || lowest;
    }, null);
  return lowest && lowest.obj.id;
}

function appraiseRouteCost(a, b, depth = 0) {
  var cost = a[depth] == b[depth] ? 0 : a[depth].startsWith('/:') && 10 ^ (a.length - depth) || undefined;
  return cost + (depth < a.length-1 ? appraiseRouteCost(a, b, depth + 1) : 0);
}

var objects =  [
  { id: 1, urlParts: ['/base', '/nested'] },
  { id: 2, urlParts: ['/base', '/nested', '/nested'] },
  { id: 3, urlParts: ['/base', '/nested', '/:name'] },
  { id: 4, urlParts: ['/base', '/other', '/:name'] },
  { id: 5, urlParts: ['/base', '/:name', '/nested', '/leaf'] },
  { id: 6, urlParts: ['/base', '/:id', '/nested', '/:leaf'] },
  { id: 7, urlParts: ['/base', '/a_name', '/nested', '/leaf'] },
  { id: 8, urlParts: ['/base', '/a_name', '/nested', '/:leaf'] }
];

console.log(matchByUrlParts(['/base']) == null);
console.log(matchByUrlParts(['/base', '/nested']) == 1);
console.log(matchByUrlParts(['/base', '/nested', '/nested']) == 2);
console.log(matchByUrlParts(['/base', '/nested', '/other']) == 3);
console.log(matchByUrlParts(['/base', '/other']) == null);
console.log(matchByUrlParts(['/base', '/other', '/other']) == 4);
console.log(matchByUrlParts(['/base', '/another_name', '/nested', '/leaf']) == 5);
console.log(matchByUrlParts(['/base', '/1234', '/nested', '/variable']) == 6);
console.log(matchByUrlParts(['/base', '/a_name', '/whoops', '/leaf']) == null);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/leaf']) == 7);
console.log(matchByUrlParts(['/base', '/a_name', '/nested', '/variable']) == 8);
&#13;
&#13;
&#13;

我发现这个解决方案比其他答案更容易理解(当然这是主观的),而且这个解决方案也执行slightly better