[设计题] 17道经典设计题串讲

17道经典设计题串讲

最近正在面试uber,uber以频繁考察系统设计而著名,系统设计是一个没有标准答案的open-end问题,因此关键在于对于特定问题的设计选择,俗称trade-off,这个思惟平时无处锻炼,故开启此文,记录学习和理解的过程。javascript

本文所有题目和分析来自《Elements Of Programming Interviews》的 Design Problems 章节,我只是在他的基础上wrap了本身的理解,尽可能通俗地表达。html

设计题的三个要点:
1.组件分解
2.并行计算
3.利用缓存java

1. Spell Checker

场景:

给一个word,假设这是一个拼错的英文单词,咱们应返回一个list of word,list里的words都是word可能的正确拼写。web

分析:

有两个概念要clarify:
1.对于“拼错"这个概念,什么叫"拼错",什么叫"拼对",都是相对的,咱们能够假设一本词典,这里面全部的单词都是拼对的,其余的都是拼错的。还有一个延伸就是,若是这个词”拼对“了,程序还check不check呢?
2.给一个word,咱们先生成一堆和他很像的词,这些词可能拼对也可能拼错,咱们把拼对的返回,问题在于,这里的”像“怎么界定?这里咱们假设 - “像”,表示全部和word的edit distance(Levenshtein distance) exactly为2的单词们。面试

数据结构:

把词典预处理成一个map,key是全部的字典单词,value无所谓。咱们只想快速的知道这个word是否是词典(拼对)的,对这个单词的其余性质并不关心。算法

核心算法:

给一个word,生成全部与这个word的edit distance(只substitute,不add也不remove)为2的全部string来,挨个去查hashmap。查出来的这些按照某种rank algorithm排序选出最高的10条。
至于这个排序算法,请看本题的后续部分。编程

复杂度:

对于一个request_spell_check(String word), 假设字母有m(英语的话就是26)个,单词长n(通常来说1~15)
时间O(m^2 * n^2),由于设定edit distance为2,改装一个word的可能性有n(n-1)(m-1)^2/2种,假设改写一种是一条java语句(一次循环),那么改写一个interesting(长度为11)大约须要循环次数为:34375,假设1s内一台电脑能够运行10^8条java语句,那么每一个request的耗时大约是0.3ms,1ms这个级别,是能够接受的。
空间O(1),经过上面的查找,查找的结果可能会很是大,咱们只要返回一个ranked list of suggestions就行了,好比rank最高的十条。数组

后续:

关于排序算法,输入是一个list of word,这些word都在词典里,是拼对的词,如今要从里面找出10条最多是答案的词,明显是一个没有标准答案的问题,因此要根据状况分析trade-off:缓存

  1. Typing errors model,咱们的排序就要根据键盘的layout来判断了;数据结构

  2. Phonetic modeling,情景:我想打一个词,只会读,不会写,好比kat打出了cat。解决方法是,把输入的单词按phonemes划分,每段按发音进行替换。

  3. History of refinements,搜集用户打错的数据 - 用户常常会打一个词,而后删了,打了另外一个词,而后request请求,这两个词第一个词就是潜在的错误拼写,第二个是正确拼写。

  4. Stemming,经过把["computers", "computer", "compute", "computing"]提取为"compute",当咱们拿到一个word时,对他stemming,而后把stemming结果的列表返回。

2. Stemming Problem

场景:

Stemming是搜索引擎normalize环节的一个技术,中文叫作词干提取,顾名思义,即把["computers", "computer", "compute", "computing"]提取为"compute"。Stemming既用在query string上,也用在document的存储中。前者能够延伸语义,好比查compute的时候极可能对computer感兴趣或者你想查的根本就是computer。后者能够大大下降搜索引擎索引的存储大小,也能够提升搜索效率。

分析:

Stemming明显是一个没有标准答案的问题,基本上是在trade-off,根据情景解决问题。并且这个题目太大,这里只是简述。
本质上Stemming就是根据某种规则(rule)重写(rewrite)。

  • 重写,基本都是focus在后缀(suffix)上,改后缀(wolves -> wolf),删后缀(cars -> car)。

  • 规则,这些规则干的事儿就是,先找到哪儿到哪儿是后缀(应该有个后缀表什么的),而后把这个后缀transform一下(应该也有个转换表什么的)。

一个有效的实现方法是根据这些规则创建一个Finite State Machine - 有限状态机。

后续:

越sophisticated的system会有更多的exceptions,即考虑更多地特殊状况,好比Chinese -> China这种。
Porter stemmer,做者Martin Porter,目前是公认最好的英语stemming algorithm。
其余的方法有:stochastic methods, n-gram based approaches.

3. Plagiarism Detector

场景:

在一堆文章里找类似的文章对们。
这里文章咱们看作String,两篇文章若是有长度为k的公共substring,就说他们是类似的。

分析:

对某个String(文章),假设这个String长度为len,那么对这个String计算len-k+1个hashcode。
举例好比:"abcdefg"这篇文章,取k=3,咱们对"abc","bcd","cde","def","efg"这5个substring分别作5次hash,存到一个大大的HashTable里,key为hash的值,value为文章id以及offset。
这样当遇到collision(这里不是指发生了多对一映射,假设hashfunction很好能够一对一映射)的时候,咱们就知道那两篇文章是类似的。
这里就须要一个能够incrementally更新的hash算法,这种hash方法叫rolling hash,可参见Rabin-Karp算法,Leetcode这道题 - 187. Repeated DNA Sequences

这里说一下为何hash算法取模的size都是质数:

好的HASH函数须要把原始数据均匀地分布到HASH数组里

原始数据不大会是真正的随机的,可能有某些规律,

好比大部分是偶数,这时候若是HASH数组容量是偶数,容易使原始数据HASH后不会均匀分布。 好比 2 4 6 8 10 12这6个数,若是对
6 取余 获得 2 4 0 2 4 0 只会获得3种HASH值,冲突会不少 若是对 7 取余 获得 2 4 6 1 3 5
获得6种HASH值,没有冲突

一样地,若是数据都是3的倍数,而HASH数组容量是3的倍数,HASH后也容易有冲突 用一个质数则会减小冲突的几率

这个HashTable的size要很是大,这样不一样substring映射到同一个hashcode的概率会小。
若是想要省空间,且对结果的精确度要求不高,能够不存全部的substring的hashcode,而只是存特定的hashcode,好比最后5个bit都是0的hashcode,这样咱们只存全部substring的1/(2^5)便可。
上面的方法存在不少false positive,好比假设这里文章指的是html页面,全部的文章几乎在开头都会调用一样的javascript段,咱们的方法就会认为这些html都是类似的。这里咱们就须要在判断前,对这些文章进行预处理,把headers去掉。
上述的方法在每篇文章(String)都很长,且这些文章分布在不少servers的时候,尤为好用。mapreduce就很适合干这个事。

4. Pair Users By Attributes

场景:

在social network里,每一个用户(user)有一些属性(attributes),如今咱们要把属性相同的用户配对。

分析:

用户怎么表示:用户id - 32位的int
属性怎么表示:strings
怎么叫配对:每次读入一个user(有他的id和属性们),从以前未成功配对的集合里找有相同属性们的用户。

数据结构:

map。

算法:

第一个层面:brute force。
假设连续读入n个用户,对于每一个读入的用户,与unpaired set里每一个用户进行attributes的比对,时间复杂度为O(N^2).
第二个层面:hashtable,key存attributes组合,value存用户。
问题是怎么把一堆attributes的组合变成一个key,即这个hashfunction怎么搞?
若是属性不多,只有几个,好比只有"蓝色","绿色","红色","吉他","钢琴","提亲","爵士鼓",那彻底能够用一个bit array一波带走,而后把这个bit array随便hash一下。
时间复杂度O(nm),n为用户数,m是全部distinct attribute数,对于上面的例子就是7。时间消耗就是把每一个用户的attributes转化成bit array而后把bit array哈希的时间。
第三个层面:若是attribute有不少,有1亿个,且大部分的用户只有不多的几条attributes,这样上面的方法就很差用了,对于每个用户都要看这一亿个attribute里有没有,是浪费时间的。
在这么一个sparse subsets里,更好的方法是,直接把这些attribute们concatenate成一个string,固然concatenation以前先排个序,而后对这个string使用hash function。
时间复杂度是O(NM),n是用户数,m是平均每一个用户的attributes concatenation以后的长度。

5. Detect Video Copyright Infringement

场景:

故事是这样的:有一天华谊发现youku.com上有人上传了”让子弹飞高清蓝光“整部电影,这侵犯了华谊的版权,假设你是华谊的工程师,如何给定华谊的视频库,判断youku.com哪些视频是侵权的。

分析:

和第3题要干的事儿同样,可是把document换成了video。同理也能够换成audio,这里讨论video。
判断对象是video的话,就不能像document那么干了(rolling hash),缘由在于即便内容相同(好比都是“让子弹飞”),但不一样的视频能够压缩成不一样的格式(flv,mp4,avi),有不一样的分辨率(1366x768, 1024×768),不一样的压缩程度。

算法:

把全部视频都转换成相同的格式(format),分辨率(resolution)和压缩度(level of compression)。注意,这么作了以后,相同内容的视频仍然不是相同的文件。可是,通过了这样的normalization以后,咱们就能够开始对每一个视频生成signature了。

  • 最简单的signature生成方法:对每一帧(frame)用一个bit根据一个亮度(brightness)阈值赋0或1,这个阈值是average的brightness。

  • 更好的signature生成方法:对每帧用三个bit表示R,G,B,每一个bit的取值根据R,G,B的intensity阈值赋0或1。

  • 再精确一点的signature生成方法:把每一帧分红几个region,每一个region求出一个R,G,B的3个bit,再把这些region contatenate起来。

后续:

以上算法是algorithmic的,还有其余的手法:
让用户来标明是否侵权(有报酬的);和之前确认了的侵权视频进行比对,看是否彻底同样;看视频header的meta-information,等等。

6. Design TeX

场景:

TeX是一个排版系统,写论文,出版社全用它。好处是不依赖平台,输出是设备无关的。像JVM同样。妈妈不再用担忧Windows下编辑的书稿在Mac里凌乱了。
一旦TeX处理了你的文件,你所获得的 DVI 文件就能够被送到任何输出设备如打印机,屏幕等而且总会获得相同的结果,而这与这些输出设备的限制没有任何关系。这说明 DVI 文件中全部的元素,从页面设置到文本中字符的位置都被固定,不能更改。TeX是一个很是多才多艺的程序。它不但能够编辑论文,书籍,幻灯片,学术杂志,我的简历,还能够 编辑曲谱,化学分子图,电路图,国际象棋,中国象棋,甚至围棋棋谱,……事实上只有少许文档不适合用TeX编辑。
问,怎么设计TeX?

分析:

两方面:

  1. building blocks,即基本元素,包括字体(fonts)和符号(symbols)怎么存,怎么表示?

    • 每一个基本元素(symbol和font)最简单的表示方法是用2D array,俗称bit-mapped representation.这样作的问题是,对于resolution要求比较高的设备,这样的空间消耗极大,且不说对一个font来讲有不一样大小,斜体,粗体之分,这都要另存一份。

    • 更好的方法是,咱们能够用数学方程来定义symbols。这样咱们反而能够更精细地编程生成symbol和font,甚至能够控制字体长宽比(aspect ratio),笔画粗细(stroke width),字体的倾斜度(font slant),描边粒度(serif size)。

  2. hierarchical layout,布局,即怎么把基本元素漂亮的摆放在一块儿

    • 咱们能够把元素(component)抽象成一个矩形的box,全部的都是矩形box,矩形box组装起来成为新的矩形Box.
      举例好比:每一个symbol是个box(好比@,3,A,d,-,#),line(好比:---)和paragraph都是由box组成的,section title, tables, table entries 以及 images也都是由矩形Box组成的。至于把这些box漂亮地组装起来就用到不少的算法。

后续:

其余的问题还有:enabling cross-referencing, automatically creating indices, supporting colors, and outputting PDF.

7. Design a Search Engine

场景:

给100万个documents,每一个平均10KB。输入set of words,返回set of documents,其中每一个document都包含这个set of words。假设全部documents能够存放在一台电脑的RAM中。

分析:

很少说,inverted indices走起。
对于每一个索引(inverted index),至关于map的一个entry,key是word,value是sorted list of ['document_ID',offset],这里list是先按document_ID,后按offset排序的。
在搜索的时候,好比搜索"beatles nirvana eminem",会分别对三个words找他们的sorted list(为了方便,下面只显示document_ID):
找到了三个词对应的索引条目:
beatles: [1]->[3]->[120]->[789]->[1022]->[7881]->[9912]
nirvana: [1]->[2]->[3]->[456]
grammy : [2]->[3]->[120]
答案就是这三个list的交集,即[3]这个document,求交集的时间复杂度和三个list的长度总和成比例(proportional to the aggregate len of the sequences)。
固然咱们能够作一些优化:

  1. Compression:既省空间又提升cache命中率。由于list都是排序了的,能够用增量编码法(delta compression),每一个documentId能够省几个bit。
    增量编码又叫差分编码,简单地说:不存「2, 4, 6, 9, 7」,而是存「2, 2, 2, 3, -2」。单独使用用处不大,可是在序列式数值常出现时能够帮助压缩大小。

  2. Cacheing:查询语句(query)通常都是有热点的,所以能够把频率高的query答案cache起来。

  3. Frequency-based optimization:并非List的每个document都返回,只返回top ten就行了。因此咱们能够维护两套inverted indices,一套是top ten的,常驻RAM;另外一套是剩下的,常驻硬盘。只要大部分的query都访问top ten,那整个系统的throughput 和 latency都是能够接受的。

  4. Intersection order:合并的顺序对速度有很大影响,要先合并小集合。上面的例子,应先合并nirvana和grammy,而后拿合并的结果与beatles去合并,这样速度最快。

咱们也能够用多级索引(multilevel index)增长准确率。对于一个高优先级的web page,能够先将其分解成paragraphs和sentences,而后把这些paragraphs和sentences拿去建索引。
这样的好处是,咱们有可能在一个paragraph或者一个sentence里找到query里全部的词,这是分级以前作不到的,以前咱们的粒度只能返回文档(web page),如今能够返回paragraph和sentence,精度提升。

算法:

这里涉及两个算法题:

  1. 找两个sorted list的共同元素(intersection):两个指针从头比,一大一小不要小(advance小的pointer),两个相等就添加到result,O(N+M)。

  2. find the smallest subarray covering all values(搜索的结果里会给一段话,里面highlight出这两个关键字):两个指针维护窗口,O(N)

8. Implement PageRank

场景:

设计一个能够快速计算100亿网页pagerank的系统。

分析:

PageRank的介绍:Introduction to PageRank
简单地说就是一个N(网页数)维矩阵A,其中i行j列的值表示用户从页面j转到页面i的几率。
每一个网页做为一个vertex,有超连接的两个网页有一条边,整张图是一个稀疏图,最好用邻接表(adjacency list)来表示。
建表的过程通常是:下载网页(pages),提取网页的超连接。URL因为长度变化很大,因此通常将其hash。
整个算法最耗时的地方在于矩阵乘法的迭代。
邻接表用一台电脑的RAM是放不下的,咱们通常有两种作法:

  1. Disk-based sorting:

相关文章
相关标签/搜索