image.png
实现一个站内导航搜索
image.png

引子

前缀Trie, 又叫字符Tire, trie来自单词retrieval, 一开始念作tree,后来改念try, 毕竟它与树是不一样的东西。网上许多文章都搞混了trie与树。 trie是通过”边“来储存字符的一种树状结构,所谓边就是节点与节点间的连接。trie每条边只能存放一个字符。
前缀Trie 实现类似百度搜索的提示的站内搜索 - 图3
它与hash树很相似,或者说它是哈希树的变种,哈希树是用边来存放一个整数(可以是一位数或两位数)。前树Tire与哈希树都是多叉树,换言之,父节点有N个子节点。
前缀Tire主要用于字符串的快速检索,查询效率比哈希表高
前缀Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
前缀Trie树也有它的缺点, 假定我们只对字母与数字进行处理,那么每个节点至少有52+10个子节点。为了节省内存,我们可以用链表或数组。在JS中我们直接用数组,因为JS的数组是动态的,自带优化。

基本性质

  1. 根节点不包含字符,除根节点外的每一个子节点都包含一个字符
  2. 从根节点到某一节点。路径上经过的字符连接起来,就是该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同

    程序实现

    ```javascript class TrieNode { constructor() { this.numPass = 0;//有多少个单词经过这节点 this.numEnd = 0; //统计单词的个数 this.edges = []; this.value = “”; //value为单个字符 this.isEnd = false; } }

/**

  • 前缀树

insert(word) { var cur = this.root; for (var i = 0; i < word.length; i++) { var c = word.charCodeAt(i); c -= 48; //减少“0”的charCode var node = cur.edges[c]; if (node == null) { var node = (cur.edges[c] = new TrieNode()); node.value = word.charAt(i); node.numPass = 1; //有N个字符串经过它 } else { node.numPass++; } cur = node; } cur.isEnd = true; //樯记有字符串到此节点已经结束 cur.numEnd++; //这个字符串重复次数 return true; }

/**

  • 先序遍历
  • @param {function} cb */ preTraversal(cb) { function preTraversalImpl(root, str, cb) { cb(root, str); for (let i = 0, n = root.edges.length; i < n; i++) { let node = root.edges[i]; if (node) {
    1. preTraversalImpl(node, str + node.value, cb);
    } } } preTraversalImpl(this.root, “”, cb); }

}

<a name="nLExi"></a>
## 
<a name="cUD2o"></a>
## 测试
```javascript
var trie = new Trie();
trie.insert("I");
trie.insert("Love");
trie.insert("武昭");
trie.insert("武昭");
trie.insert("武昭在哪里???");
trie.insert("武昭在干什么???");
trie.insert("武昭🙂???");
trie.insert("武昭没吃饭???");
trie.insert("China");
trie.insert("China");
trie.insert("China");
trie.insert("China");
trie.insert("xiaoliang");
trie.insert("xiaoliang");
trie.insert("man");
trie.insert("handsome");
trie.insert("love");
trie.insert("Chinaha");
trie.insert("her");
trie.insert("know");

console.log("包含武(包括本身)前缀的单词及出现次数:");
console.log("---------------start--------------------")
var map = {}
trie.preTraversal(function (node, str) {
  if (str.indexOf("武") === 0 && node.isEnd) {
    map[str] = node.numEnd
  }
})
for (var i in map) {
  console.log(i + " 出现了" + map[i] + " 次")
}

image.png

在antDesignPro中使用

image.png

代码实现

trie.js

import config from '../../config/config'

/**
  * 引入config路由生成用name做key,path做value的字典,用于站内检索
  * -----
  * @param {*} list
  */
const genDict = (list) => {
  const res = {}
  list.forEach(item => {
    const dfs = data => {
      if (typeof data.name === 'string') {
        //TODO 需要传递参数的路由排除
        res[data.name] = data.path;
      }
      data.routes && data.routes.forEach(child => dfs(child));
    }
    dfs(item);
  })
  return res
}

class TrieNode {
  constructor() {
    this.numPass = 0;//有多少个单词经过这节点
    this.numEnd = 0; //统计单词的个数
    this.edges = [];
    this.value = ""; //value为单个字符
    this.isEnd = false;
  }
}

/**
 * 搜索前缀树
 * -----
 * 参考修改
 * @see https://segmentfault.com/a/1190000013018855
 */
class Trie {
  constructor() {
    this.root = new TrieNode();
  }

  insert(word) {
    var cur = this.root;
    for (var i = 0; i < word.length; i++) {
      var c = word.charCodeAt(i);
      c -= 48; //减少“0”的charCode
      var node = cur.edges[c];
      if (node == null) {
        var node = (cur.edges[c] = new TrieNode());
        node.value = word.charAt(i);
        node.numPass = 1; //有N个字符串经过它
      } else {
        node.numPass++;
      }
      cur = node;
    }
    cur.isEnd = true; //樯记有字符串到此节点已经结束
    cur.numEnd++; //这个字符串重复次数
    return true;
  }

  /**
   * 先序遍历
   * -----
   * @param {function} cb
   */
  preTraversal(cb) {
    function preTraversalImpl(root, str, cb) {
      cb(root, str);
      for (let i = 0, n = root.edges.length; i < n; i++) {
        let node = root.edges[i];
        if (node) {
          preTraversalImpl(node, str + node.value, cb);
        }
      }
    }
    preTraversalImpl(this.root, "", cb);
  }
}


var trie = new Trie();
const nameMap = genDict(config.routes)
for (const iterator of Object.keys(nameMap)) {
  trie.insert(iterator);
}

/**
 * trie是一个 用路由的name做key,路由的path做value 生成的 搜索前缀树对象
 * -----
 * 使用示例:
 * 查找已“武”开头的字符有哪些
 * var map = {}
 * trie.preTraversal(function (node, str) {
 *   if (str.indexOf("武") === 0 && node.isEnd) {
 *     map[str] = node.numEnd
 *   }
 * })
 * for (var i in map) {
 *   console.log(i + " 出现了" + map[i] + " 次")
 * }
 *
 * 1.调用对象的先序遍历方法传入一个函数,就可以统计前缀匹配的单词
 */

export { nameMap, trie }

引入config路由生成用name做key,path做value的字典,用于站内检索

RightContent.jsx

在HeaderSearch中使用
image.png

使用方式:
在onSearch事件触发后调用这个匿名函数,匿名函数的逻辑同上边测试代码,调用trie的先序遍历传入要匹配的value,会生成匹配到的值是一个map,遍历map生成相关的数组填入AutoComplete组件的dataSource属性中,就实现了搜索展示功能。
搜索消耗还是挺大的,使用Debounce限制一下搜索的频率(这里用的lodash的)

后续完善

  1. 有一些不能直接搜索进入的路由需要在生成字典的时屏蔽掉

image.png

  1. 搜索进入选中相关的key完善,现在直接就进入了不能选中Key,因为我们为了实现三级菜单重写了这个Menu的一点逻辑。

参考摘抄

https://segmentfault.com/a/1190000013018855