编程之法:面试与算法心得 - 海量数据处理(2)

这里介绍海量数据处理中常用的算法

小结:
1. simhash算法
流程:分词、hash、加权、合并、降维
- 分词:输出每个单词及其权重,如CSDN(4), 博客(5)…
- hash:计算各个特征向量的hash值,如hash(CSDN) = 100101, hash(博客) = 101011
- 加权:给所有特征向量进行加权,如遇到1给hash值和权值正相乘,遇到0则负相乘,如W(CSDN) = 100101(4) = 4 -4 -4 4 -4 4, W(博客) = 101011(5) = 5 -5 5 -5 5 5
- 合并:各个特征向量累加,4 -4 -4 4 -4 4 和5 -5 5 -5 5 5得到 9 -9 1 -1 1 9
- 降维:对于n-bit签名的累加结果,如果大于0则置1,否则置0,如9 -9 1 -1 1 9得到1 0 1 0 11,从而行程simhash签名。

拓展到海量数据?
根据经验值,对64位的 SimHash值,海明距离在3以内的可认为相似度比较高。
将64位的二进制simhash签名均分成4块,每块16位。根据鸽巢原理(也称抽屉原理),如果两个签名的海明距离在 3 以内,它们必有一块完全相同。分成四个块进行比较,需4 * 2(n-16)次额外比较,其中2n为总数据量

2. 外排序
原理:排序 - 归并。划分为两个不相交的部分,排序,归并。
k趟算法可以在kn的时间开销内核n/k的空间开销内完成对最多n个小于n的无重复正整数的排序。

时间复杂度 空间复杂度
位图 O(N) 0.625M
多路归并 O(NlogN) 1M

多路归并,时间复杂度为O(kn/klogn/k ),但位图可能会超内存空间

3. mapReduce
基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约
reduce阶段则经历了三个阶段,分别Copy->Sort->reduce
4. 多层划分
本质分而治之,通过多次划分,逐步确定范围
5. bitmap

  • 可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下
  • 使用bit数组来表示某些元素是否存在,比如8位电话号码.
  • 可采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)判断是否重复

6. bloom filter
原理:当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:有任意一个0表示一定不在,如果是1,检查元素很可能在。
错误率相关:hash函数、hash函数个数、位数组大小
7. Trie树(字典树)
Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。(利用trie树,关键字域存该查询串出现的次数
8. 数据库
大量数据增删改时放入数据库
9. 倒排索引
用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,常被应用于搜索引擎和关键字查询的问题中。
倒排索引: 单词 ⇒ 文档
正向索引:文档 ⇒ 单词
10. 其他算法(有缺点):
1、分词,计算每个文件特征向量。比较不同文件特征向量的距离判断。(不适合海量数据,要计算任意两网页之间的距离/夹角余弦)
2、hash,生成文档hash,然后比较指纹。(md5轻微变化hash变化就很大)

所有算法对比:

特征 原理 场景
simhash 局部敏感哈希 主要思想是降维,将高维特征向量映射为低维,通过距离确定是否重复或高度近似 海量文章相似度
外排序 划分 - 排序 - 归并 O(nlogn) 海量数据的磁盘文件排序,内存限制1MB
MapReduce 归并 将数据交给不同的机器去处理,数据划分,结果归约 数据量大,但是数据种类小可以放入内存;如,wordcount,海量数据的Top10,海量数据的中位数
多层划分 分而治之 通过多次划分,逐步确定范围 海量数据找不重复的整数个数;找中位数
bitmap 使用bit数组来表示某些元素是否存在,比如8位电话号码.针对不重复数据(可以采用multi-bit) 在2.5亿个整数中找出不重复的整数
bloom filter 拓展bitmap 通过K个Hash函数将这个元素映射成一个位阵列中k个点,检索时,我们只要看看这些点是不是都是1 不适合零错误应用,适合能容忍低错误率场景,数据字典、数据的判重、集合求交集
Tire树 优点是最大限度地减少无谓的字符串比较 利用字符串的公共前缀来降低查询时间的开销 用于统计和排序大量的字符串,搜索引擎用于文本词频统计,如文本最频繁的10个,热门查询
数据库 增删改 海量数据的增删改
倒排索引 单词 ⇒ 文档 关键字查询


1. simhash算法

背景

如果某一天,面试官问你如何设计一个比较两篇文章相似度的算法?可能你会回答几个比较传统点的思路:

  • 一种方案是先将两篇文章分别进行分词,得到一系列特征向量,然后计算特征向量之间的距离(可以计算它们之间的欧氏距离、海明距离或者夹角余弦等等),从而通过距离的大小来判断两篇文章的相似度。
  • 另外一种方案是传统hash,我们考虑为每一个web文档通过hash的方式生成一个指纹(finger print)。

第一种方法,不适合海量数据。有着数以百万甚至亿万的网页,要求你计算这些网页的相似度。你还会去计算任意两个网页之间的距离或夹角余弦么?
第二种方法,所说的传统加密方式md5,其设计的目的是为了让整个分布尽可能地均匀,但如果输入内容一旦出现哪怕轻微的变化,hash值就会发生很大的变化。

出世

simhash作为locality sensitive hash(局部敏感哈希)的一种:

  • 其主要思想是降维,将高维的特征向量映射成低维的特征向量,通过两个向量的Hamming Distance来确定文章是否重复或者高度近似。
  • 其中,Hamming Distance,又称汉明距离,在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的不同字符的个数。
    如此,通过比较多个文档的simHash值的海明距离,可以获取它们的相似度。

流程

simhash算法分为5个步骤:分词、hash、加权、合并、降维,具体过程如下所述:

  1. 分词
    给定一段语句,进行分词,得到有效的特征向量,然后为每一个特征向量设置1-5等5个级别的权重(如果是给定一个文本,那么特征向量可以是文本中的词,其权重可以是这个词出现的次数)。例如给定一段语句:“CSDN博客结构之法算法之道的作者July”,分词后为:“CSDN 博客 结构 之 法 算法 之 道 的 作者 July”,然后为每个特征向量赋予权值:CSDN(4) 博客(5) 结构(3) 之(1) 法(2) 算法(3) 之(1) 道(2) 的(1) 作者(5) July(5),其中括号里的数字代表这个单词在整条语句中的重要程度,数字越大代表越重要。
  2. hash
    通过hash函数计算各个特征向量的hash值,hash值为二进制数01组成的n-bit签名。比如“CSDN”的hash值Hash(CSDN)为100101,“博客”的hash值Hash(博客)为“101011”。就这样,字符串就变成了一系列数字。
  3. 加权
    在hash值的基础上,给所有特征向量进行加权,即W = Hash * weight,且遇到1则hash值和权值正相乘,遇到0则hash值和权值负相乘。例如给“CSDN”的hash值“100101”加权得到:W(CSDN) = 1001014 = 4 -4 -4 4 -4 4,给“博客”的hash值“101011”加权得到:W(博客)=1010115 = 5 -5 5 -5 5 5,其余特征向量类似此般操作。
  4. 合并
    将上述各个特征向量的加权结果累加,变成只有一个序列串。拿前两个特征向量举例,例如“CSDN”的“4 -4 -4 4 -4 4”和“博客”的“5 -5 5 -5 5 5”进行累加,得到“4+5 -4±5 -4+5 4±5 -4+5 4+5”,得到“9 -9 1 -1 1”。
  5. 降维
    对于n-bit签名的累加结果,如果大于0则置1,否则置0,从而得到该语句的simhash值,最后我们便可以根据不同语句simhash的海明距离来判断它们的相似度。例如把上面计算出来的“9 -9 1 -1 1 9”降维(某位大于0记为1,小于0记为0),得到的01串为:“1 0 1 0 1 1”,从而形成它们的simhash签名。

在这里插入图片描述

应用

每篇文档得到SimHash签名值后,接着计算两个签名的海明距离即可。根据经验值,对64位的 SimHash值,海明距离在3以内的可认为相似度比较高。

如果拓展到海量数据呢?
缺陷方案1:查找待查询文本的64位simhash code的所有3位以内变化的组合,需4万多次查询
缺陷方案2:预生成库中所有样本simhash code的3位变化以内的组合,占据4万多倍的原始空间

方案3:

  1. 我们可以把 64 位的二进制simhash签名均分成4块,每块16位。根据鸽巢原理(也称抽屉原理),如果两个签名的海明距离在 3 以内,它们必有一块完全相同。
  2. 然后把分成的4 块中的每一个块分别作为前16位来进行查找,建倒排索引。

如此,如果样本库中存有234(差不多10亿)的simhash签名,则每个table返回2^(34-16)=262144个候选结果,大大减少了海明距离的计算成本。
在这里插入图片描述
假设数据是均匀分布,16位的数据,产生的像限为216个,则平均每个像限分布的文档数则为234/216 = 2(34-16)) ,四个块返回的总结果数为 4* 262144 (大概 100 万)。这样,原本需要比较10亿次,经过索引后,大概只需要处理100万次。


2. 外排序

所谓外排序,顾名思义,即是在内存外面的排序,因为当要处理的数据量很大,而不能一次装入内存时,此时只能放在读写较慢的外存储器(通常是硬盘)上。

外排序通常采用的是一种“排序-归并”的策略。

  • 在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件;
  • 尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。

问题实例

  1. 给一个107个数据的磁盘文件排序,给定一个文件,里面最多含有n个不重复的正整数,每个都小于等于n,n=10^7. 条件大约有1MB的内存可用,且要求运行时间在5分钟以下,10秒最佳。

解法一:位图方案(缺陷,但值得一看)

用一个20位长的字符串来表示一个所有元素都小于20的简单的非负整数集合,边框用如下字符串来表示集合{1,2,3,5,8,13}:
0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0

上述集合中各数对应的位置则置1,没有对应的数的位置则置0。

参考编程珠玑一书上的位图方案,针对我们的10^7个数据量的磁盘文件排序问题,我们可以这么考虑,由于每个7位十进制整数表示一个小于1000万的整数。我们可以使用一个具有1000万个位的字符串来表示这个文件,其中,当且仅当整数i在文件中存在时,第i位为1。采取这个位图的方案是因为我们面对的这个问题的特殊性:

  1. 输入数据限制在相对较小的范围内,
  2. 数据没有重复,
  3. 其中的每条记录都是单一的整数,没有任何其它与之关联的数据。

伪代码如下:

//第一步,将所有的位都初始化为0  
for i ={0,....n}      
   bit[i]=0;  
//第二步,通过读入文件中的每个整数来建立集合,将每个对应的位都置为1。  
for each i in the input file     
   bit[i]=1;  
  
//第三步,检验每一位,如果该位为1,就输出对应的整数。  
for i={0...n}      
  if bit[i]==1        
    write i on the output file  

上述的位图方案,共需要扫描输入数据两次,具体执行步骤如下:

第一次,只处理1—4999999之间的数据,这些数都是小于5000000的,对这些数进行位图排序,只需要约5000000/8=625000Byte,也就是0.625M,排序后输出。 第二次,扫描输入文件时,只处理4999999-10000000的数据项,也只需要0.625M(可以使用第一次处理申请的内存)。 因此,总共也只需要0.625M 位图的的方法有必要强调一下,就是位图的适用范围为针对不重复的数据进行排序,若数据有重复,位图方案就不适用了

不过很快,我们就将意识到,用此位图方法,严格说来还是不太行,空间消耗107/8还是大于1M(1M=1024*1024空间,小于10^7/8)。

既然如果用位图方案的话,我们需要约1.25MB(若每条记录是8位的正整数的话,则10000000/(102410248) ~= 1.2M)的空间,而现在只有1MB的可用存储空间,那么究竟该作何处理呢?

解法二:多路归并

k趟算法可以在kn的时间开销内和n/k的空间开销内完成对最多n个小于n的无重复正整数的排序。

比如可分为2块(k=2,1趟反正占用的内存只有1.25/2M),先遍历一趟,首先排序处理1-4999999之间的整数,然后再第二趟,对5000001-1000000之间的整数进行排序处理。

解法总结

  1. 关于本章中位图多路归并两种方案的时间复杂度及空间复杂度的比较,如下:
时间复杂度 空间复杂度
位图 O(N) 0.625M
多路归并 O(NlogN) 1M

(多路归并,时间复杂度为O(kn/klogn/k ),严格来说,还要加上读写磁盘的时间,而此算法绝大部分时间也是浪费在这上面)

2. bit-map
适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下

基本原理及要点:使用bit数组来表示某些元素是否存在,比如8位电话号码

扩展:bloom filter可以看做是对bit-map的扩展

例如:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。 8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。


3. 分布式处理之mapReduce

方法介绍

MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。这样做的好处是可以在任务被分解后,可以通过大量机器进行并行计算,减少整个操作的时间。但如果你要我再通俗点介绍,那么,说白了,Mapreduce的原理就是一个归并排序。

适用范围:数据量大,但是数据种类小可以放入内存。

基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。

基础架构

基本基础知识储备:

  1. MapReduce是一种模式。
  2. Hadoop是一种框架
  3. Hadoop是一个实现了MapReduce模式的开源的分布式并行编程框架。

一句话概括就是:在hadoop的框架上采取MapReduce的模式处理海量数据。

MapReduce模式

MapReduce模式的主要思想是将自动分割要执行的问题(例如程序)拆解成Map(映射)和Reduce(化简)的方式,流程图如下图1所示:
在这里插入图片描述
在数据被分割后通过Map函数的程序将数据映射成不同的区块,分配给计算机机群处理达到分布式运算的效果,在通过Reduce 函数的程序将结果汇整,从而输出开发者需要的结果。

MapReduce借鉴了函数式程序设计语言的设计思想,其软件实现是指定一个Map函数,把键值对(key/value)映射成新的键值对(key/value),形成一系列中间结果形式的key/value 对,然后把它们传给Reduce(规约)函数,把具有相同中间形式key的value合并在一起。Map和Reduce函数具有一定的关联性。函数描述如表1 所示:

函数 输入 输出 说明
Map <k1,v1> List(<k2, v2>) 1. 将小数据集进一步解析成一批<key, value>对,输入map函数中进行处理;2. 每个输入的<k1,v1>会输出一批<k2,v2>, <k2,v2>是计算的中间结果
Reduce <k2, List(v2)> <k3,v3> 输入的中间结果<k2,List(v2)>中的List(v2)表示是一批属于同一个k2的value

处理时,每个节点就近读取本地存储的数据处理(map),将处理后的数据进行合并(combine)、排序(shuffle and sort)后再分发(至reduce节点),避免了大量数据的传输,提高了处理效率。无共享式架构的另一个好处是配合复制(replication)策略,集群可以具有良好的容错性,一部分节点的down机对集群的正常工作不会造成影响。

如上图所示,其中map阶段,当map task开始运算,并产生中间数据后并非直接而简单的写入磁盘,它首先利用内存buffer来对已经产生的buffer进行缓存,并在内存buffer中进行一些预排序来优化整个map的性能。而上图右边的reduce阶段则经历了三个阶段,分别Copy->Sort->reduce。我们能明显的看出,其中的Sort是采用的归并排序,即merge sort。

问题实例

  1. The canonical example application of MapReduce is a process to count the appearances of each different word in a set of documents:
  2. 海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。
  3. 一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?

4. 多层划分

多层划分法,本质上还是分而治之的思想,因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。

问题实例

1、2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数
分析:有点像鸽巢原理,整数个数为232,也就是,我们可以将这232个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。
2. 5亿个int找它们的中位数
分析:首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。

实际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成224个区域,然后确定区域的第几大数,在将该区域分成220个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了。


5. bitmap

可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下

基本原理及要点

使用bit数组来表示某些元素是否存在,比如8位电话号码.针对不重复数据

bitmap排序代码:

//定义每个Byte中有8个Bit位  
#include <memory.h>  
#define BYTESIZE 8  
void SetBit(char *p, int posi)  
{  
    for(int i=0; i < (posi/BYTESIZE); i++)  
    {  
        p++;  
    }  
  
    *p = *p|(0x01<<(posi%BYTESIZE));//将该Bit位赋值1  
    return;  
}  
  
void BitMapSortDemo()  
{  
    //为了简单起见,我们不考虑负数  
    int num[] = {3,5,2,10,6,12,8,14,9};  
  
    //BufferLen这个值是根据待排序的数据中最大值确定的  
    //待排序中的最大值是14,因此只需要2个Bytes(16个Bit)  
    //就可以了。  
    const int BufferLen = 2;  
    char *pBuffer = new char[BufferLen];  
  
    //要将所有的Bit位置为0,否则结果不可预知。  
    memset(pBuffer,0,BufferLen);  
    for(int i=0;i<9;i++)  
    {  
        //首先将相应Bit位上置为1  
        SetBit(pBuffer,num[i]);  
    }  
  
    //输出排序结果  
    for(int i=0;i<BufferLen;i++)//每次处理一个字节(Byte)  
    {  
        for(int j=0;j<BYTESIZE;j++)//处理该字节中的每个Bit位  
        {  
            //判断该位上是否是1,进行输出,这里的判断比较笨。  
            //首先得到该第j位的掩码(0x01<<j),将内存区中的  
            //位和此掩码作与操作。最后判断掩码是否和处理后的  
            //结果相同  
            if((*pBuffer&(0x01<<j)) == (0x01<<j))  
            {  
                printf("%d ",i*BYTESIZE + j);  
            }  
        }  
        pBuffer++;  
    }  
}  
  
int _tmain(int argc, _TCHAR* argv[])  
{  
    BitMapSortDemo();  
    return 0;  
}  

问题实例

1、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数
解法一:采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。

解法二:也可采用与第1题类似的方法,进行划分小文件的方法。然后在小文件中找出不重复的整数,并排序。然后再进行归并,注意去除重复的元素。”

2、给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
解法一:可以用位图/Bitmap的方法,申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。


6. bloom Filter

布隆过滤器

布隆过滤器,是一种空间效率很高的随机数据结构,Bloom filter可以看做是对bit-map的扩展,它的原理是:

  • 当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1**。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:
    • 如果这些点有任何一个0,则被检索元素一定不在;
    • 如果都是1,则被检索元素很可能在。

其可以用来实现数据字典,进行数据的判重,或者集合求交集。

但Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

1.1 集合表示和元素查询

初始状态时,Bloom Filter是一个包含m位的位数组,每位都置位0。
在这里插入图片描述
为了表达S={x1, x2,…,xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素x,第i个哈希函数映射的位置hi(x)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位,即第二个“1“处)。
在这里插入图片描述
在判断y是否属于这个集合时,我们对y应用k次哈希函数,如果所有hi(y)的位置都是1(1≤i≤k),那么我们就认为y是集合中的元素,否则就认为y不是集合中的元素。下图中y1就不是集合中的元素(因为y1有一处指向了“0”位)。y2或者属于这个集合,或者刚好是一个false positive。
在这里插入图片描述

1.2 错误率估计


f = (1 - p)k ,p为位数组中0的比例

1.3 最优的hash函数个数

既然Bloom Filter要靠多个哈希函数将集合映射到位数组中,那么应该选择几个哈希函数才能使元素查询时的错误率降到最低呢?

这里有两个互斥的理由:**如果哈希函数的个数多,那么在对一个不属于集合的元素进行查询时得到0的概率就大;但另一方面,如果哈希函数的个数少,那么位数组中的0就多。**为了得到最优的哈希函数个数,我们需要根据上一小节中的错误率公式进行计算。

换句话说,要想保持错误率低,最好让位数组有一半还空着

1.4 位数组大小

我们得出结论:在错误率不大于є的情况下,m至少要等于n log2(1/є)才能表示任意n个元素的集合。

这个结果比前面我们算得的下界n log2(1/є)大了log2e≈ 1.44倍。这说明在哈希函数的个数取到最优时,要让错误率不超过є,m至少需要取到最小值的1.44倍

问题实例

1、给你A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A,B文件共同的URL。如果是三个乃至n个文件呢?

分析:如果允许有一定的错误率,可以使用Bloom filter,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)。”


7. Trie树(字典树)

Trie树,即字典树,又称单词查找树或键树,是一种树形结构。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是最大限度地减少无谓的字符串比较,查询效率比较高。

Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

它有3个基本性质:

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

1.2 树的构建

咱们先来看一个问题:假如现在给你10万个长度不超过10的单词,对于每一个单词,我们要判断它出没出现过,如果出现了,求第一次出现在第几个位置。对于这个问题,我们该怎么解决呢?

如果我们用最傻的方法,对于每一个单词,我们都要去查找它前面的单词中是否有它。那么这个算法的复杂度就是O(n^2)。显然对于10万的范围难以接受。

即如果现在有b,abc,abd,bcd,abcd,efg,hii 这6个单词,我们可以构建一棵如下图所示的树:

在这里插入图片描述
如上图所示,对于每一个节点,从根遍历到他的过程就是一个单词,如果这个节点被标记为红色,就表示这个单词存在,否则不存在。

我们可以看到,trie树每一层的节点数是26^i级别的。所以为了节省空间,我们还可以用动态链表,或者用数组来模拟动态。而空间的花费,不会超过单词数×单词长度。

问题实例

1、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析
提示:用trie树统计每个词出现的次数,时间复杂度是O(nle)(le表示单词的平均长度),然后是找出出现最频繁的前10个词。当然,也可以用堆来实现,时间复杂度是O(nlg10)。所以总的时间复杂度,是O(nle)与O(nlg10)中较大的哪一个。

2、寻找热门查询
原题:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。

提示利用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序


8. 数据库

当遇到大数据量的增删改查时,一般把数据装进数据库中,从而利用数据的设计实现方法,对海量数据的增删改查进行处理

9. 倒排索引

倒排索引是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射,常被应用于搜索引擎和关键字查询的问题中。

倒排索引: 单词 ⇒ 文档
正向索引:文档 ⇒ 单词

正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样的查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。

问题实例

1、文档检索系统,查询那些文件包含了某单词,比如常见的学术论文的关键字搜索

提示:建倒排索引。

猜你喜欢

转载自blog.csdn.net/u010521366/article/details/89041762