做者:July。致谢:caopengcs、胡果果。
时间:二零一三年九月七日。
html
写博的近三年,整理了太多太多的笔试面试题,如微软面试100题系列,和眼下这个程序员编程艺术系列,真心以为题目年年变,但解决问题的方法永远都是那几种,用心准备后,自会发现一切有迹可循。程序员
故为更好的帮助人们找到工做,特准备在北京举办一系列面试&算法讲座。时间定为周末,每次一个上午或下午,受众对象为要找工做或换工做或对算法感兴趣的朋友,费用前期暂愿交就交,交多少全由本身决定。主讲人:我和目前zoj排名第一的caopengcs博士。9月15日为第1次讲座:http://weibo.com/1580904460/A8N6oAFZ4?mod=weibotime。面试
OK,切入正题。上面说整理过不少笔试面试题,但好的笔试面试题真心难求,包括在编程艺术系列每一章的选题,越到后面越难挑,而本文写两个跟实际挂钩的问题,它们来自此文http://blog.csdn.net/v_july_v/article/details/7974418 的第3.6题,和第87题,即算法
题目分析:本题来源于去年2012年百度的一套实习生笔试题中的系统设计题( 为尊重愿题,本章主要使用百度搜索引擎展开论述,而不是google等其它搜索引擎,但原理不会差太多。然脱离本题,平时搜的时候,鼓励用...),题目比较开放,考察的目的在于看应聘者解决问题的思路是否清晰明确,其次即是看能考虑到多少细节。
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不只限于字符串),因此常常被搜索引擎系统用于文本词频统计。它的优势是:最大限度地减小无谓的字符串比较,查询效率比哈希表高。sql
Trie的核心思想是空间换时间。利用字符串的公共前缀来下降查询时间的开销以达到提升效率的目的。
它有3个基本性质:
数据库
当每一个搜索引擎输入一个前缀时,下面它只会展现0~10个候选词,但如果碰到那种候选词不少的时候,如何取舍,哪些展现在前面,哪些展现在后面?这就是一个搜索热度的问题。编程
如本题描述所说,在去年的这个时候,当我在搜索框内搜索“北京”时,它下面会提示以“北京”为前缀的诸如“北京爱情故事”,“北京公交”,“北京医院”,且“ 北京爱情故事”展现在第一个:数据结构
为什么输入“北京”,会首先提示“北京爱情故事”呢?由于去年的这个时候,正是《北京爱情故事》这部电影上映正火的时候(其上映日期为2012年1月8日,火了至少一年),那个时候你们都一个劲的搜索这部电影的相关信息,当10我的中输入“北京”后,其中有8我的会继续敲入“爱情故事”(连起来就是“北京爱情故事”)的时候,搜索引擎对此固然不会无动于衷。nosql
也就是说,搜索引擎知道了这个时间段,你们都在疯狂查找北京爱情故事,故当用户输入以“北京”为前缀的时候,搜索引擎猜想用户有80%的机率是要查找“北京爱情故事”,故把“北京爱情故事”在下面提示出来,并放在第一个位置上。ide
但为什么今年这个时候再次搜索“北京”的时候,它展现出来的词不一样了呢?
缘由在于随着时间变化,人们对北京爱情故事这部影片的关注度逐渐降低,与此同时,又出现了新的热词,新的电影,故如今虽然一样是输入“北京”,后面提示的词也相应跟着起了变化。那解决这个问题的办法是什么呢?如开头所说:按期分析某段时间内的人们搜索的关键词,统计出搜索次数比较多的热词,继而当用户输入某个前缀时,优先展现热词。
故说白了,这个问题的第二个步骤即是统计热词,咱们把统计热词的方法称为TOP K算法,此算法的应用场景即是此文http://blog.csdn.net/v_july_v/article/details/7382693中的第2个问题,再次原文引用:
“寻找热门查询,300万个查询字符串中统计最热门的10个查询
原题:搜索引擎会经过日志文件把用户每次检索使用的全部检索串都记录下来,每一个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但若是除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
解答:由上面第1题,咱们知道,数据大则划为小的,如一亿个Ip求Top 10,可先%1000将ip分到1000个小文件中去,并保证一种ip只出如今一个文件中,再对每一个小文件中的ip进行hashmap计数统计并按数量排序,最后归并或者最小堆依次处理每一个小文件的top10以获得最后的结果。
但若是数据规模自己就比较小,能一次性装入内存呢?好比这第2题,虽然有一千万个Query,可是因为重复度比较高,所以事实上只有300万的Query,每一个Query255Byte,所以咱们能够考虑把他们都放进内存中去(300万个字符串假设没有重复,都是最大长度,那么最多占用内存3M*1K/4=0.75G。因此能够将全部字符串都存放在内存中进行处理),而如今只是须要一个合适的数据结构,在这里,HashTable绝对是咱们优先的选择。
因此咱们放弃分而治之/hash映射的步骤,直接上hash统计,而后排序。So,针对此类典型的TOP K问题,采起的对策每每是:hashmap + 堆。以下所示:
别忘了这篇文章中所述的堆排序思路:‘维护k个元素的最小堆,即用容量为k的最小堆存储最早遍历到的k个数,并假设它们便是最大的k个数,建堆费时O(k),并调整堆(费时O(logk))后,有k1>k2>...kmin(kmin设为小顶堆中最小元素)。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,若x>kmin,则更新堆(x入堆,用时logk),不然不更新堆。这样下来,总费时O(k*logk+(n-k)*logk)=O(n*logk)。此方法得益于在堆中,查找等各项操做时间复杂度均为logk。’--第三章续、Top K算法问题的实现。
固然,你也能够采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。”
相信,如此,也就不难理解开头所提出的方法了:Trie树+ TOP K「hashmap+堆,hashmap+堆 统计出如10个近似的热词,也就是说,只存与关键词近似的好比10个热词」。
并且你之后就能够告诉你身边的伙伴们,为什么输入“结构之”,会提示出来一堆以“结构之”为前缀的词拉:
方法貌似成型了,但有哪些须要注意的细节呢?如@江申_Johnson所说:“实际工做里,好比当前缀很短的时候,候选词不少的时候,查询和排序性能可能有问题,也许能够加一层索引trie(这层索引能够只索引频率高于某一个阈值的词,很短的时候查这个就能够了。数量不够的话再去查索引了所有词的trie树);并且有时候不能根据query频率来排,而要引导用户输入信息量更全面的query,或者或不只仅是前缀匹配这么简单。”
除了上文提到的trie树,三叉树或许也是一个不错的解决方案:http://igoro.com/archive/efficient-auto-complete-with-a-ternary-search-tree/。此外,StackOverflow上也有两个讨论帖子,你们能够看看:①http://stackoverflow.com/questions/2901831/algorithm-for-autocomplete,②http://stackoverflow.com/questions/1783652/what-is-the-best-autocomplete-suggest-algorithm-datastructure-c-c。
题目详情:找一个点集中与给定点距离最近的点,同时,给定的二维点集都是固定的,查询可能有不少次,时间复杂度O(n)没法接受,请设计数据结构和相应的算法。
题目分析:此题是去年微软的三面题,相似于一朋友@陈利人 出的这题:附近地点搜索,就是搜索用户附近有哪些地点。随着GPS和带有GPS功能的移动设备的普及,附近地点搜索也变得煊赫一时。在庞大的地理数据库中搜索地点,索引是很重要的。可是,咱们的需求是搜索附近地点,例如,坐标(39.91, 116.37)附近500米内有什么餐馆,那么让你来设计,该怎么作?
假定只容许你初中数学知识,那么你可能建一个X-Y坐标系,即以坐标(39.91, 116.37)为圆心,以500的长度为半径,画一个园,而后一个一个坐标点的去查找。此法看似可行,但复杂度可想而知,即使你自觉得聪明的说把整个平面划分为四个象限,一个一个象限的查找,此举虽然优化程度不够,但也说明你一步步想到点子上去了。
即不一个一个坐标点的查找,而是一个一个区域的查找,相对来讲,其平均查找速度和效率会显著提高。如此,便天然而然的想到了有没有一种一次查找定位于一个区域的数据结构呢?
若看过博客内以前介绍R树的这篇文章http://blog.csdn.net/v_JULY_v/article/details/6530142#t2 的读者立马便能意识到,R树就是解决这个区域查找继而不断缩小规模的问题。特直接引用原文:
R树是B树在高维空间的扩展,是一棵平衡树。每一个R树的叶子结点包含了多个指向不一样数据的指针,这些数据能够是存放在硬盘中的,也能够是存在内存中。根据R树的这种数据结构,当咱们须要进行一个高维空间查询时,咱们只须要遍历少数几个叶子结点所包含的指针,查看这些指针指向的数据是否知足要求便可。这种方式使咱们没必要遍历全部数据便可得到答案,效率显著提升。下图1是R树的一个简单实例:
咱们在上面说过,R树运用了空间分割的理念,这种理念是如何实现的呢?R树采用了一种称为MBR(Minimal Bounding Rectangle)的方法,在此我把它译做“最小边界矩形”。从叶子结点开始用矩形(rectangle)将空间框起来,结点越往上,框住的空间就越大,以此对空间进行分割。有点不懂?不要紧,继续往下看。在这里我还想提一下,R树中的R应该表明的是Rectangle(此处参考wikipedia上关于R树的介绍),而不是大多数国内教材中所说的Region(不少书把R树称为区域树,这是有误的)。咱们就拿二维空间来举例。下图是Guttman论文中的一幅图:
我来详细解释一下这张图。
我想你们都应该理解这个数据结构的特征了。用地图的例子来解释,就是全部的数据都是餐厅所对应的地点,先把相邻的餐厅划分到同一块区域,划分好全部餐厅以后,再把邻近的区域划分到更大的区域,划分完毕后再次进行更高层次的划分,直到划分到只剩下两个最大的区域为止。要查找的时候就方便了。
下面就能够把这些大大小小的矩形存入咱们的R树中去了。根结点存放的是两个最大的矩形,这两个最大的矩形框住了全部的剩余的矩形,固然也就框住了全部的数据。下一层的结点存放了次大的矩形,这些矩形缩小了范围。每一个叶子结点都是存放的最小的矩形,这些矩形中可能包含有n个数据。
地图查找的实例
讲完了基本的数据结构,咱们来说个实例,如何查询特定的数据。又以餐厅为例,假设我要查询广州市天河区天河城附近一千米的全部餐厅地址怎么办?
遍历全部在此区域内的结点,看是否知足咱们的要求便可。怎么样,其实R树的查找规则跟查地图很像吧?对应下图:
一棵R树知足以下的性质:
先来探究一下叶子结点的结构。叶子结点所保存的数据形式为:(I, tuple-identifier)。
其中,tuple-identifier表示的是一个存放于数据库中的tuple,也就是一条记录,它是n维的。I是一个n维空间的矩形,并能够刚好框住这个叶子结点中全部记录表明的n维空间中的点。I=(I0,I1,…,In-1)。其结构以下图所示:
下图描述的就是在二维空间中的叶子结点所要存储的信息。
在这张图中,I所表明的就是图中的矩形,其范围是a<=I0<=b,c<=I1<=d。有两个tuple-identifier,在图中即表示为那两个点。这种形式彻底能够推广到高维空间。你们简单想一想三维空间中的样子就能够了。这样,叶子结点的结构就介绍完了。
非叶子结点的结构其实与叶子结点很是相似。想象一下B树就知道了,B树的叶子结点存放的是真实存在的数据,而非叶子结点存放的是这些数据的“边界”,或者说也算是一种索引(有疑问的读者能够回顾一下上述第一节中讲解B树的部分)。
一样道理,R树的非叶子结点存放的数据结构为:(I, child-pointer)。
其中,child-pointer是指向孩子结点的指针,I是覆盖全部孩子结点对应矩形的矩形。这边有点拗口,但我想不是很难懂?给张图:
D,E,F,G为孩子结点所对应的矩形。A为可以覆盖这些矩形的更大的矩形。这个A就是这个非叶子结点所对应的矩形。这时候你应该悟到了吧?不管是叶子结点仍是非叶子结点,它们都对应着一个矩形。树形结构上层的结点所对应的矩形可以彻底覆盖它的孩子结点所对应的矩形。根结点也惟一对应一个矩形,而这个矩形是能够覆盖全部咱们拥有的数据信息在空间中表明的点的。
我我的感受这张图画的不那么精确,应该是矩形A要刚好覆盖D,E,F,G,而不该该再留出这么多没用的空间了。但为尊重原图的绘制者,特不做修改。”
但R树有些什么问题呢?如@宋枭_CD所说:“单纯用R树来做索引,搜索附近的地点,可能会遍历树的不少个分支。并且当全国的地图或者全省的地图时候,树的叶节点数目不少,树的深度也会是一个问题。通常会把地理位置上附近的节点(二维地图中点线面)预处理成page(大小为4K的倍数),在这些page上创建R树的索引。”
我在微博上跟一些朋友讨论这个附近点搜索的问题时,除了谈到R树,有几个朋友都指出GeoHash算法能够解决,故才了解了下GeoHash算法,此文http://blog.nosqlfan.com/html/1811.html 清晰阐述了MongoDB借助GeoHash算法实现地理位置索引的原理,特引用其内容加以说明,以下:
“支持地理位置索引是MongoDB的一大亮点,这也是全球最流行的LBS服务foursquare 选择MongoDB的缘由之一。咱们知道,一般的数据库索引结构是B+ Tree,如何将地理位置转化为可创建B+Tree的形式。首先假设咱们将须要索引的整个地图分红16×16的方格,以下图(左下角为坐标0,0 右上角为坐标16,16):
单纯的[x,y]的数据是没法创建索引的,因此MongoDB在创建索引的时候,会根据相应字段的坐标计算一个能够用来作索引的hash值,这个值叫作geohash,下面咱们以地图上坐标为[4,6]的点(图中红叉位置)为例。咱们第一步将整个地图分红等大小的四块,以下图:
划分红四块后咱们能够定义这四块的值,以下(左下为00,左上为01,右下为10,右上为11):
db.map.ensureIndex({point : "2d"}, {min : 0, max : 16, bits : 4})
本章完。