Javascript 什么';autosuggest最合适的数据结构是什么?

Javascript 什么';autosuggest最合适的数据结构是什么?,javascript,node.js,data-structures,Javascript,Node.js,Data Structures,我想实现一个autosuggest组件。对于每个用户输入,组件应该提供零个或多个建议 例如,如果用户类型“公园”,建议可以是:[“帕克维尔”、“帕克伍德”、“贝尔公园”] 要求很简单: 它应该不区分大小写(对于'park'、'park'和'park',用户应该得到相同的建议) 它应该在每个单词的开头匹配('pa'将匹配'Parkville','Bell Park',和'Very cool Park',但不匹配'Carpark') 您会选择什么数据结构在Javascript中实现这一点 有没有

我想实现一个autosuggest组件。对于每个用户输入,组件应该提供零个或多个建议

例如,如果用户类型
“公园”
,建议可以是:
[“帕克维尔”、“帕克伍德”、“贝尔公园”]

要求很简单:

  • 它应该不区分大小写(对于
    'park'
    'park'
    'park'
    ,用户应该得到相同的建议)
  • 它应该在每个单词的开头匹配(
    'pa'
    将匹配
    'Parkville'
    'Bell Park'
    ,和
    'Very cool Park'
    但不匹配
    'Carpark'
您会选择什么数据结构在Javascript中实现这一点

有没有Javascript/Node.js库可以提供帮助?

试试看。如果您愿意,它允许您设置某些属性。简单小巧


如果您需要更简单的东西,您可以查看是否有任何Levenshtein距离的实现。Soundex是一种更酷的算法,它根据单词的语音特性进行评分。

我认为这类任务的最佳数据结构是一个。关于大小写不敏感-在添加到trie之前,只需将每个单词小写,并对大小写较低的单词执行搜索

当您到达trie的某个点时,会有许多子节点满足字符串的要求—字符串的前缀为从根到当前点

输出建议-递归地从当前点(从根到用户键入的前缀)遍历,并在标记为叶的节点上打印建议。10次输出后停止打印,因为trie可能有许多令人满意的单词

以下是js实现:,以及其他许多实现。只需搜索js+trie。可能是trie+autosuggest+js也可以)

更新

如果您想在
O(1)
(当然,对于每个建议都是O(1))中输出所有变量,而不需要递归遍历,那么可以在每个节点中存储引用的arraylist。Arraylist存储属于节点的所有单词的索引,每个值都是其他字典araylist中的索引

诸如此类:

在dict中添加单词:

  • 签入
    O(word\u len)
    是否在trie中(已添加)。如果不是,则添加到字典并记住“存储”中的索引

  • 搜索:

    Go to node with prefix==typed word;
    for(int i=0;i<min(curNode.references.length(),10);i++)
    print(dict[curNode.references[i]];
    
    换言之,短语代码与单字代码相同。引用是一样的,因为我们把所有的部分作为一个短语存储在一个单元格中。不同之处在于分部分拆分和添加法尔酶。很简单,你看

    更新

    顺便说一句,存储引用在内存消耗方面并不“昂贵”——单词中的每个字母都是trie中的某个节点,这意味着某个arraylist中有一个条目(全局存储数组中该单词的一个整数索引)。因此,您只需要额外的O(dictionary_length)内存,即~50000*20=10000000个整数~4MB,假设每个单词最多有20个字母。所以,所需内存的上限是4MB

    更新

    关于东鹰

    好的,在发布任何想法之前,我想警告一下,这是非常奇怪的autosuggest行为,通常autosuggest匹配一个前缀,但不是所有前缀

    有一个非常简单的想法,它将增加这样几个前缀的搜索复杂度,并行搜索一些delta,其中delta复杂度=查找集合交集的复杂度

  • 现在,全局存储不仅包含索引,还包含成对的
    ,其中a=存储中的索引,b=pharse中的索引。
    对于简单的单词b=0或-1或任何特殊值
  • 现在,每个trie节点引用arraylist都包含对。当用户键入前缀短语时,例如“ea ri”,您应该像往常一样找到“ea”节点,迭代引用,但只考虑那些条目,其中a=any,b=1,因为键入短语中的ea索引=1。将所有此类
    a
    索引(其中
    b=1
    放在某个集合中)。像往常一样找到
    ri
    节点,迭代引用并将这些
    a
    索引放到另一个集合中,其中
    b=2
    ,依此类推。求指数集的交集。按索引输出存储字,其中索引属于集合的交集

  • 当你搜索的不是短语而是简单的单词时,你会反复搜索所有的参考项。

    有时候简单的方法是最好的。你说你的字典里有大约50000条词条。你不会说其中有多少有多个词(例如“贝尔公园”或“马丁·路德·金大道”,等等)。为了便于讨论,我们假设每个词典条目的平均字数为2

    我对Javascript不是很在行,所以我将用一般术语来描述它,但您应该能够相当容易地实现它

    在预处理步骤中,创建数据库中所有项目的数组(即所有项目)。例如:

    Carpark
    Cool Park
    Park
    Pike's Peak
    ...
    
    然后,创建一个映射,其中包含每个单词的一个条目,以及包含该单词的第一个数组中该项目的索引列表。因此,第二个数组将包含:

    Carpark, {0}
    Cool, {1}
    Park, {1,2}
    Pike's, {3}
    Peak, {3}
    
    按单词对第二个数组排序。所以顺序是{停车场,酷,公园,山顶,派克}

    当用户键入“p”时,对单词数组进行二进制搜索,以找到以“p”开头的第一个单词。您可以从该点开始顺序扫描数据,直到找到一个不以
    P
    开头的单词。访问每个单词时,将索引列表中引用的单词添加到输出中。(您必须在此处执行一些重复数据消除,但这很简单。)

    二进制搜索是O(logn),所以找到第一个单词会非常快。尤其是在数据量如此之小的情况下。如果您正在为每个键入的字母执行HTTP请求,那么通信时间将缩短处理时间。没有一个特别好的理由试图加快这一进程
    Carpark
    Cool Park
    Park
    Pike's Peak
    ...
    
    Carpark, {0}
    Cool, {1}
    Park, {1,2}
    Pike's, {3}
    Peak, {3}
    
    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"]