我想实现一个自动提示组件。对于每个用户输入,组件应提供零个或多个建议。
例如,如果用户输入'park'
,建议可能是:['Parkville', 'Parkwood', 'Bell Park']
。
要求很简单:
'park'
,'PARK'
和'PaRk'
提供相同的建议)'pa'
匹配'Parkville'
,'Bell Park'
和'Very cool park'
,但不是 {{ 1}})您会选择在Javascript中实现此操作的数据结构?
是否有任何可以提供帮助的Javascript / Node.js库?
答案 0 :(得分:4)
我认为此类任务的最佳数据结构是trie。关于不区分大小写 - 在添加到trie之前,只需将每个单词小写,然后对较低的单词进行搜索。
当你到达特里的某个点时,有许多子节点满足字符串 - 字符串有从root到当前点的前缀。
输出建议 - 从当前点(从根到达用户类型的前缀)递归 walk 并在标记为叶子的节点上打印建议。在~10输出后停止打印,因为trie可能有许多令人满意的单词。
以下是js实现:trie-js,trie和其他许多内容。只需搜索js + trie。可能是trie + autosuggest + js也可以工作)
<强>更新强>
如果要输出O(1)
中的所有变体(当然每个建议为O(1)),而不使用递归遍历,则可以在每个节点中存储引用的arraylist。 Arraylist存储所属的所有单词的索引到节点,每个值是其他字典araylist中的索引。
类似的东西:
在dict中添加字词
签入O(word_len)
是否在trie中(已添加)。如果没有,请添加到词典并记住&#34; storage&#34;
if(!inTrie(word)){
dict.push(word);
index = dict.length-1; //zero based indices
// now adding to trie
for each node visited while addition to trie : node.references.push(index)
}
搜索:
Go to node with prefix==typed word;
for(int i=0;i<min(curNode.references.length(),10);i++)
print(dict[curNode.references[i]];
<强>更新强>
关于&#39; pa&#39; - &GT; &#39;非常酷的公园&#39;
您可以定义将短语拆分为单独的单词,以便每个单词都是&#34;可搜索的&#34;在一个特里。但!当您将短语视为一个单词时,您应该将其存储为一个单元格。
我的意思是:
String phrase = "Very cool parl";
dict.push(phrase);
index = dict.length-1;
parts[] = split(phrase);
for part in parts{
add part - for each node visited while addition of part perform node.references.push(index);
}
换句话说,短语代码与单个单词的代码相同。并且引用是相同的,因为我们将所有部分作为短语存储在一个单元格中。不同之处在于分割和添加各个部分的分段。很简单,你看。
更新
顺便说一下,参考存储不是那么昂贵&#34;在内存消耗中 - 单词中的每个字母都是trie中的某个节点,这意味着某些arraylist中的1个条目(全局存储阵列中该单词的一个整数索引)。所以,你必须只有额外的O(dictionary_length)内存,即~50000 * 20 = 1 000 000个整数~4 MB,假设每个单词最多有20个字母。因此,所需内存的上限为4 MB。<强>更新强>
关于&#39; e&#39; - &GT;东鹰。
好的,在发布任何想法之前我想警告这是非常奇怪的 autosuggest行为,通常autosuggest匹配一个前缀但不匹配所有前缀。
有一个非常简单的想法会增加某些delta的这种几个前缀并行搜索的搜索复杂度,其中delta复杂度=查找集合交集的复杂性。
<a,b> where a = index in storage, b = index in pharse.
对于简单的单词b = 0或-1或任何特殊值。a
索引放在哪里b=1
到某个集合。像往常一样查找ri
节点,迭代引用并将这些a
索引放到b=2
的其他集合中,依此类推。找到索引集的交集。按索引输出存储字,其中索引属于集合的交集。当您搜索的不是短语而是简单的单词时,您会遍历所有参考项目。
答案 1 :(得分:1)
尝试http://lunrjs.com。如果您愿意,它可以让您设置提升某些属性。简单而小巧。
如果您需要更简单的东西,您可以看到是否有任何Levenshtein距离的实施。较冷的算法是Soundex,它根据单词的语音属性进行评分。
答案 2 :(得分:1)
有时简单的方法是最好的。你说你的词典中有~5,000个条目。你没有说他们中有多少有多个单词(即“贝尔公园”或“马丁路德金大道”等)。仅仅为了论证,让我们假设每个词典条目的平均单词数是2。
我对Javascript不太满意,所以我将概括地描述这一点,但你应该能够相当容易地实现它。
在预处理步骤中,创建数据库中所有项目的数组(即全部50,000个)。所以,例如:
Carpark
Cool Park
Park
Pike's Peak
...
然后,创建一个包含每个单词条目的映射,以及包含它的第一个数组中项目的索引列表。所以你的第二个数组将包含:
Carpark, {0}
Cool, {1}
Park, {1,2}
Pike's, {3}
Peak, {3}
按字排序第二个数组。所以订单是{Carpark,Cool,Park,Peak,Pike's}
。
当用户输入“P”时,对单词数组进行二分查找,找到以“P”开头的第一个单词。您可以从该点开始对数据进行顺序扫描,直到找到不以P
开头的单词。在访问每个单词时,将索引列表中引用的单词添加到输出中。 (你必须在这里做一些重复数据删除,但这很容易。)
二进制搜索是O(log n),因此查找第一个单词的速度非常快。特别是有这么少量的数据。如果您正在为键入的每个字母执行HTTP请求,则通信时间将使处理时间相形见绌。尝试在服务器端加快速度并没有特别好的理由。
但可以减少服务器上的负载。实现当用户键入“P”并且客户端从服务器获取数据时,客户端现在具有可能在用户键入“PA”时可能返回的所有可能字符串。也就是说,“PA”的结果是“P”结果的一个子集。
因此,如果您修改了代码以使客户端仅在键入的第一个字符上向服务器发出请求,则可以在客户端上进行后续搜索。您所要做的就是让服务器返回匹配单词列表(来自第二个数据结构)以及这些单词索引的匹配短语。因此,当用户键入第二,第三,第四等字符时,客户端将经历过滤过程。服务器无需参与。
当然,这样做的好处是响应速度更快,服务器负载更少。成本是客户端上的少量增加的复杂性,并且在第一个请求上返回了少量额外数据。但是第一次请求返回的额外数据可能会少于服务器在第二次请求时返回的数据。
答案 3 :(得分:0)
实际上,trie是实现目标的正确数据结构。实施简短易行。我的解决方案如下。 Trie实施后附加用法。
function TrieNode(ch) {
this.key = ch;
this.isTail = false;
this.children = [];
}
TrieNode.prototype = {
constructor : TrieNode,
/**
* insert keyword into trie
*/
insert : function(word) {
if (word && word.length == 0)
return;
var key = word[0];
if (this.children[key] == null) {
this.children[key] = new TrieNode(key);
}
if (word.length == 1) {
this.children[key].isTail = true;
} else if (word.length > 1) {
this.children[key].insert(word.slice(1));
}
},
/**
* return whether a word are stored in trie
*/
search : function(word) {
if (word && word.length == 0 || this.children[word[0]] == null)
return false;
if (word.length == 1) {
return this.children[word[0]].isTail;
} else {
return this.children[word[0]].search(word.slice(1));
}
},
/**
* NOTICE: this function works only if length of prefix longer than minimum trigger length
*
* @param prefix
* keywords prefix
* @returns {Array} all keywords start with prefix
*/
retrieve : function(prefix) {
var MINIMUM_TRIGGER_LENGTH = 1;
if (prefix == null || prefix.length < MINIMUM_TRIGGER_LENGTH)
return [];
var curNode = this.walk(prefix);
var collection = [];
curNode && curNode.freetrieve(prefix, collection);
return collection;
},
walk : function(prefix) {
if (prefix.length == 1) {
return this.children[prefix];
}
if (this.children[prefix[0]] == null) {
return null;
}
return this.children[prefix[0]].walk(prefix.slice(1));
},
freetrieve : function(got, collection) {
for ( var k in this.children) {
var child = this.children[k];
if (child.isTail) {
collection.push(got + child.key);
}
child.freetrieve(got + child.key, collection);
}
}
}
// USAGE lists below
function initTrieEasily(keywords){
let trie= new TrieNode();
keywords.forEach(word => trie.insert(word));
return trie;
}
var words=['mavic','matrix','matrice','mavis','hello world'];
var trie=initTrieEasily(words);
trie.retrieve('ma'); // ["mavic", "mavis", "matrix", "matrice"]
trie.retrieve("mat") // ["matrix", "matrice"]
trie.search("hello"); // "false"
trie.search("hello world"); //"true"
trie.insert("hello");
trie.search("hello"); // "true"
trie.retrieve("hel"); // ["hello", "hello world"]