今天这篇文章源于上周在工作中解决的一个实际问题,它是个比较普遍的问题,无论做什么开发,估计都有遇到过。具体是这样的,我们有一份高校的名单(2657个),需要从海量的文章标题中找到包含这些高校的标题,其实就是模糊查询(关注公众号 渡码, 回复关键词 trie 获取源码)。对应的伪代码如下

  1. selected_titles = []
  2. for 标题 in 海量标题:
  3. for 高校 in 高校名单:
  4. if 标题.contains(高校):
  5. selected_titles.add(标题)
  6. break

如果是大数据开发,对应的SQL的伪代码是这样的

  1. select title
  2. from tb
  3. where
  4. title rlike '清华大学|北京大学|...2657个高校'

上面这两种做法都能实现我们的需求,但它们的共同问题是查询效率太低。如果我们要匹配的高校不是2657个而是几十万甚至上百万,那这种方式耗费时之久是不可想象的。

优化这类问题通常需要在数据结构上做文章,这个问题中我们能优化的数据结构也只有“高校名单”这个了,上面的伪代码中我们存放“高校名单”的数据结构是数组,当我们查找某个title是否包含某个高校的时候,需要从头到尾遍历一遍“高校名单”,并且名单越长,遍历耗时就越长。

清楚了数组这种数据结构的缺点后,接下来我们重点要做的就是寻找一个数据结构可以做到在不遍历整个“高校名单”的情况下就可以完成模糊查询。这个数据结构就是我们今天要介绍的 Trie 树,冷眼一看这个单词有点陌生,又是一个树型结构,感觉会很复杂似的,实际上这个数据结构的设计思想非常简单,一学就会。

下面我们就来学习一下 Trie 树。为了方面讲解,假设“高校名单”里只有下面5个元素

  1. ABCABDBCDBCECCABCDE

对应的两种数据结构如下:

抛开这两种数据结构查找的时间复杂度,我们先从直观上看看为什么 Trie 树的查找效率要比数组高。假设我们要查找,“CDE”这个字符串,在数组结构中,我们要遍历一遍数组,比较7次才能找到结果,做了比较多的“无用功”。而在 Tire 树中只需要比较3次就可以找到,它的优势非常明显,由于树型结构我们根本不用考虑左侧A、B开头的两个分支,这就大大减少了比较的次数,从而减少“无用功”。下面用一个动画来演示一下如何创建 Trie 树,以及在 Trie树上查找字符串(如果视频播放不了可以看源码目录中的gif)

树的建立过程其实就是遍历字符串每个元素并在树上建立相应的节点。字符串查找过程其实就是按照字符串对树进行遍历。Trie 树的建立与字符串查找还是比较简单的。

不知道大家是否注意到上图 Trie 树中的节点有两种颜色——白色和绿色。绿色节点代表从根节点到当前节点的字符串是“高校名单”中的字符串,也就是我们建立 Trie 树用到的字符串。以最左侧的叶子结点“C”为例,它代表“ABC”字符串是“高校名单”中的字符串。同理,字符串“AB”就不是“高校名单”里的元素,因为“B”节点不是绿色的,因此当我们在这棵树上查找字符串“AB”时,是查不到的。这一点需要大家注意,下面编码中我们也有体现。

另外,有朋友可能会有疑问,我们最开始的需求不是模糊查询吗,在 Trie 树讲解这部分怎么都在说字符串全词(精确)匹配。这是因为全词匹配是 Tire 树支持的最基本的查找方式,在此基础上,我们做一些变通就可以很容易实现模糊匹配。

接下来,我们就来看看代码实现(Python版),首先创建两个数组

  1. colleges = utils.read_file_to_list('key_words.txt')
  2. titles = utils.read_file_to_list('titles.txt')

colleges就是我们一直在说的“高校名单”,titles便是“海量标题”,它们都是一维数组,数组每个元素都是一个字符串。

再来编写 Trie 树相关的代码,如果理解了 Trie 树的设计思想,再编写下面的代码其实很容易。首先要定义一个类代表 Trie 树节点

  1. class TrieNode:
  2. def __init__(self):
  3. self.nodes = dict()
  4. # is_end=True 代表从根节点到当前节点构造Trie树的字符串(出现在“高校名单”里)
  5. self.is_end = False

is_end=True就是我们上面说的绿色节点。

再来编写创建 Trie 树的代码,代码在 TrieNode 类中

  1. def insert_many(self, items: [str]):
  2. """
  3. 支持输入字符串数组,直接构造一个 Trie 树
  4. :param items: 字符串数组
  5. :return: None
  6. """
  7. for word in items:
  8. self.insert(word)

  9. def insert(self, item: str):
  10. """
  11. 向 Trie 树插入一个短语
  12. :param item: 待插入的字符串
  13. :return: None
  14. """
  15. curr = self
  16. for word in item:
  17. if word not in curr.nodes:
  18. curr.nodes[word] = TrieNode()
  19. curr = curr.nodes[word]
  20. curr.is_end = True

再来编写查找 Trie 树的代码,代码在 TrieNode 类中

  1. def suffix(self, item: str) -> bool:
  2. """
  3. 匹配前缀,也就是判断item字符串是否是以“高校名单”中某个字符串开头
  4. :param item: 待匹配字符串
  5. :return: True or False
  6. """
  7. curr = self
  8. for word in item:
  9. if word not in curr.nodes:
  10. return False
  11. curr = curr.nodes[word] # 取得子节点
  12. if curr.is_end: # 如果is_end=True说明当前字符串包含了“高校名单”的某个字符串
  13. return True
  14. return False # 未匹配上

这里并不是全词匹配,而是前缀匹配,也就是判断待查找的字符串item是否是以“高校名单”中某个字符串开头。

再来编写模糊匹配,代码在 TrieNode 中

  1. def infix(self, item: str) -> bool:
  2. for i in range(len(item)):
  3. sub_item = item[i:] # 将待查找的字符串分成不同子串
  4. # 如果子串的前缀在 Trie 树中能匹配上
  5. # 说明待查找的字符串item中包含“高校名单”中的元素,
  6. # 即实现了 tile rlike '清华大学|北京大学|...其他大学' 的功能
  7. if self.suffix(sub_item):
  8. return True
  9. return False

这里其实就是把待查找字符串item分成不同子串去做前缀匹配,如果子串匹配上,那就说整个字符串item就包含了“高校名单”里面的某个字符串。

最后,我们运行一下上面的代码,并记录查找时间,与最开始数组结构那一版做个对比。代码如下

  1. # 数组版本
  2. cnt = 0
  3. start_time = int(time.time() * 1000)
  4. for title in titles:
  5. for x in colleges:
  6. if x in title:
  7. cnt += 1
  8. break

  9. end_time = int(time.time() * 1000)
  10. print(cnt)
  11. print('spend: %.2fs' % ((end_time - start_time) / 60.0))

  12. # Trie 树版本
  13. root = TrieNode()
  14. root.insert_many(colleges)
  15. cnt = 0
  16. start_time = int(time.time() * 1000)
  17. for title in titles:
  18. if root.infix(title):
  19. cnt += 1

  20. end_time = int(time.time() * 1000)
  21. print(cnt)
  22. print('spend: %.2fs' % ((end_time - start_time) / 60.0))

输出结果如下:

  1. 5314
  2. spend: 9.13s
  3. 5314
  4. spend: 0.23s

可以看到,用数组匹配用了9s,而用 Trie 树匹配仅用0.23s!

今天介绍的这种提高海量数据模糊查询性能的方式是通过写代码的方式实现的,对于经常写 SQL 的大数据开发者来说,要把它用起来只是建个 UDF 就可以了,需要在 UDF 的初始化代码中用“高校名单”建立一颗 Trie 树。

今天的内容就分享到这里了,希望对你有帮助。公众号回复关键词 trie 获取完整源代码

欢迎公众号「渡码」,输出别地儿看不到的干货。

Trie树-提高海量数据的模糊查询性能的更多相关文章

  1. [转]ORACLE中Like与Instr模糊查询性能大比拼

    instr(title,'手册')>0  相当于  title like '%手册%' instr(title,'手册')=1  相当于  title like '手册%' instr(titl ...

  2. Like与Instr模糊查询性能

    项目中用到like模糊查询,但是总觉的太小家子气,有没有高逼格的呢? instr(title,'手册')>0 相当于 title like '%手册%' instr(title,'手册')=1 ...

  3. mysql 优化海量数据插入和查询性能

    对于一些数据量较大的系统,数据库面临的问题除了查询效率低下,还有就是数据入库时间长.特别像报表系统,每天花费在数据导入上的时间可能会长达几个小时或十几个小时之久.因此,优化数据库插入性能是很有意义的. ...

  4. Trie树入门

    Trie树入门 貌似很多人会认为\(Trie\)是字符串类型,但是这是数据结构!!!. 详情见度娘 下面开始进入正题. PS:本文章所有代码未经编译,有错误还请大家指出. 引入 先来看一个问题 ​ 给 ...

  5. 整合hibernate的lucene大数据模糊查询

      大数据模糊查询lucene 对工作单使用 like模糊查询时,实际上 数据库内部索引无法使用 ,需要逐条比较查询内容,效率比较低在数据量很多情况下, 提供模糊查询性能,我们可以使用lucene全文 ...

  6. sql查询性能调试,用SET STATISTICS IO和SET STATISTICS TIME---解释比较详细

            一个查询需要的CPU.IO资源越多,查询运行的速度就越慢,因此,描述查询性能调节任务的另一种方式是,应该以一种使用更少的CPU.IO资源的方式重写查询命令,如果能够以这样一种方式完成查 ...

  7. Trie树【字典树】浅谈

    最近随洛谷日报看了一下Trie树,来写一篇学习笔记. Trie树:支持字符串前缀查询等(目前我就学了这些qwq) 一般题型就是给定一个模式串,几个文本串,询问能够匹配前缀的文本串数量. 首先,来定义下 ...

  8. Trie树的创建、插入、查询的实现

    原文:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28977986&id=3807947 1.什么是Trie树 Tr ...

  9. Entity Framework Code First+SQL Server,改变聚集索引,提高查询性能

    .net Entity Framework(调研的是Entity Framework 4.0) code first方式生成数据库时,不能修改数据库表的索引,而SQLServer默认会把数据表的主键设 ...

随机推荐

  1. dapp 是什么?dapp 和 app 有什么区别?一文明白 dapp。

    DApp 是 decentralized application 中文分布式 APP 的缩写. 一个 DApp 有后台代码运行在分布式点对点网络中.传统的 APP 的后台代码是运行在中心化的服务器. ...

  2. 《JavaScript 模式》读书笔记(5)— 对象创建模式4

    我们学完了大部分对象创建模式相关的内容,下面还有一些小而精的部分. 七.对象常量 JavaScript中没有常量的概念,虽然许多现代的编程环境可能为您提供了用以创建常量的const语句.作为一种变通方 ...

  3. coding++:Idea设置Java类注释模板和方法注释模板

    设置类注释模板 1):选择File–>Settings–>Editor–>File and Code Templates–>Includes–>File Header. ...

  4. coding++:Spring中的@Transactional(rollbackFor = Exception.class)属性详解

    异常: 如下图所示,我们都知道 Exception 分为 运行时异常 RuntimeException 和 非运行时异常. error 是一定会回滚的. 如果不对运行时异常进行处理,那么出现运行时异常 ...

  5. RPC框架实现(一) Protobuf的rpc实现

    概述 RPC框架是云端服务基础框架之一,负责云端服务模块之间的项目调用,类似于本地的函数调用一样方便.常见的RPC框架配带的功能有: 编解码协议.比如protobuf.thrift等等. 服务发现.指 ...

  6. Ubuntu虚拟机查看文件,目录颜色详解

    查看文件 查看Home(不是home)目录下文件: [duanyongchun@localhost ~]$ ls 查看根目录下文件: [duanyongchun@localhost ~]$ cd / ...

  7. 模块 sys shell参数获取

    sys 参数获取 获取参数 sys模块是与python解释器交互的一个接口 sys.argv 命令行参数List,第一个元素是程序本身路径 sys.exit(n) 退出程序,正常退出时exit(0) ...

  8. vue+cordova实现退出app效果

    //vue钩子函数created方法中添加监听等待设备API库加载好 created(){ var that = this; document.addEventListener("devic ...

  9. NCEP CFSR数据下载

    一.简介 CFSR(Climate Forecast SystemReanalysis)再分析资料使用了 GEOS-5(Goddard EarthObserving System)大气模式与资料同化系 ...

  10. MATLAB——元胞数组

    一. 1.元胞数组的创建 >> a={;ones(,),:} a = ] [2x3 ;ones(,),:} >> b=[{};{ones(,)},{:}] b = ] [2x3 ...