面试-海量题集-笔记

http://hi.baidu.com/mianshiti/blog/item/f6ac8fef7e47502862d09fbf.html

http://hi.baidu.com/clive_studio/blog/item/a641318ee04e00f0513d9294.html

http://weibo.com/julyweibo

 

2^10 = 1K;     2^20= 1M;      2^30 = 1G;      2^32=4G

K: 千;      M:百万;   G:十亿;   T:万亿

 

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

l  笨方法,就是外部排序,然后归并

l  1. Hash A文件成内存大小的1000个小块文件,每个文件约300M;

2. 然后同样的方式Hash B 文件,得到A,B对应的1000个小文件

3. 对每一对小文件,利用hash_set遍历找到共同的URL

l  如果允许错误率,Bloom Filter(广泛应用于URL过滤、查重),1G约为10亿,4G约为40亿B,也就是320亿bit。

 

1)      从海量日志数据,提取出某日访问百度次数最多的那个 IP?

IP地址最多有2^32=4G种取值可能,所以不能完全加载到内存中。 可以考虑分而治之的策略

l  按照IP地址的hash(IP)%1024值(IP地址的特点,也可以自然分段),将海量日志存储到1024个小文件中。每个小文件最多包含4M个IP地址。 

l  对于每个小文件,可以构建一个IP作为key,出现次数作为value的hash_map,并记录当前出现次数最多的1个IP地址。

l  有了1024个小文件中的出现次数最多的IP,我们就可以轻松得到总体上出现次数最多的IP。 

 

2)      在排序数组中,找出给定数字的出现次数?

1)       穷举法

2)       二分查找

 

3)      已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)

 

4)      2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。

l  将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上

l  有点像鸽巢原理,整数个数为2^32,也就是,我们可以将这2^32个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了

 

5)      100w个数中找最大的前100个数(最大的K个数)

l  维护一个100个元素大小的最小堆,复杂度O(100W * lg100)

l  如果数的范围不大,可以用一个计数数组,存放每一个的出现个数,复杂度:N

如果取值范围很大[Xmin, Xmax],可以分治,将其分成M块,则每块的范围是[Xmin, Xmin+d], [Xmin+d, Xmin+2d],……,统计每块的个数,可以得出第K大所在的块,然后在对该块处理.O(n+100W*lg100)

l  快排,每次分割之后只考虑大的一部分,如果不够的话,排序选择

l  局部淘汰,思想跟堆一样,不过,用给一个100的数组存放,记录最小值,如果比数组最小的大,就覆盖最小值,如果小就丢掉。O(100W*100)

 

 

 

6)      维持中位数的方法?

双堆:令数组L的中位数为m,用一个最大堆存储数组L中不大于m的元素,用一个最小堆存储数组L中不小于m的元素,其中这两个堆均不包含中位数m。每次往数组L插入新元素x时,若x否则插入最小堆(最小堆的最小值不小于m)。若插入新元素后导致m不再是中位数(即两个堆的元素数目相差2个或2个以上),则将当前的中位数m插入到元素数量较少的那个堆中,然后令元素数量较多的那个堆的堆顶元素为新的中位数,并将该堆顶元素从堆中删除。

 

7)      5亿个int找它们的中位数。

1)    外排,然后取第2.5亿+1个数(多路归并)

2)    分治:将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。

 

8)      现在有一个0-30000的随机数生成器。请根据这个随机数生成器,设计一个抽奖范围是0-350000彩票中奖号码列表,其中要包含20000个中奖号码。

这个题刚好和上面两个思想相反,一个0到3万的随机数生成器要生成一个0到35万的随机数。那么我们完全可以将0-35万的区间分成35/3=12个区间,然后每个区间的长度都小于等于3万,这样我们就可以用题目给的随机数生成器来生成了,然后再加上该区间的基数。那么要每个区间生成多少个随机数呢?计算公式就是:区间长度*随机数密度,在本题目中就是30000*(20000/350000)。最后要注意一点,该题目是有隐含条件的:彩票,这意味着你生成的随机数里面不能有重复,这也是我为什么用双层桶划分思想的另外一个原因。

 

9)      有10个文件,每个文件1G, 每个文件的每一行都存放的是用户的query,每个文件的query都可能重复。要你按照query的频度排序。

1.      用Hash把文件重排,让相同query一定会在同一个文件,同时进行计数,然后排序,最后归并。

2.      如果query重复的比较多,可以一次性装入内存,可以用trie树来统计每个query的频率,然后再排序

 

 

10)  有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16个字节,内存限制大小是1M。返回频数最高的100个词。

l  将其hash到2000个文件中,每个文件差不多是500k(如果大于1M,继续hash)

l  利用trie树或者hash­_map统计每一个文件的词的频度

l  维护小顶堆,遍历

 

11)  1000万个记录(这些查询串的重复度比较高,长度为1-255个字节,总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。

Top K算法

l  利用Hash将1000万记录哈希到300万的范围内,统计每个记录的频度(trie,hash_map)。然后用维护一个大小为10的小顶堆遍历这300万的统计结果。

l  也可以用trie树,在关键字域存储其串的出现次数,然后用小顶堆求

 

12)腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?。

l  快排+二分

l  用bit-map,40亿个bit = 5亿的B,因为10亿B约1G,所以约占500M内存

l  Unsigned int的范围是2^32,分成2^10个小文件,每个文件为的范围是2^22,小文件间有序。计算数属于那个文件范围,然后在文件内寻找;

l  同上一个方法类似,《编程珠玑》中提到,用一个32位的二进制代表整数,首先根据第一位(最高位)的0或1分类,然后在第二位的0或1分类,以此类推。

l  位图的方法:如果知道最大的整数,可以设置一个数组,每个数组的下标可以表征一个整数,其元素是整数是否出现过。

 

13)一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析

1.         建立Trie树,记录每颗树的出现次数,O(n*le); le:平均查找长度

2.         维护一个10的小顶堆,O(n*lg10);

3.         总复杂度: O(n*le) + O(n*lg10);

 

 

14) 怎样从10亿查询词找出出现频率最高的10个(Top K问题)??

1. 问题描述

在大规模数据处理中,常遇到的一类问题是,在海量数据中找出出现频率最高的前K个数,或者从海量数据中找出最大的前K个数,这类问题通常称为“top K”问题,如:在搜索引擎中,统计搜索最热门的10个查询词;在歌曲库中统计下载率最高的前10首歌等等。

2. 当前解决方案

针对top k类问题,通常比较好的方案是【分治+trie树/hash+小顶堆】,即先将数据集按照hash方法分解成多个小数据集,然后使用trie树或者hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出频率最高的前K个数,最后在所有top K中求出最终的top K。

实际上,最优的解决方案应该是最符合实际设计需求的方案,在实际应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。

本文针对不同的应用场景,介绍了适合相应应用场景的解决方案。

3. 解决方案

3.1 单机+单核+足够大内存

设每个查询词平均占8Byte,则10亿个查询词所需的内存大约是10^9*8=8G内存。如果你有这么大的内存,直接在内存中对查询词进行排序,顺序遍历找出10个出现频率最大的10个即可。这种方法简单快速,更加实用。当然,也可以先用HashMap求出每个词出现的频率,然后求出出现频率最大的10个词。

3.2  单机+多核+足够大内存

这时可以直接在内存中实用hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑是同3.1节类似,最后一个线程将结果归并。

该方法存在一个瓶颈会明显影响效率,即数据倾斜,每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。解决方法是,将数据划分成c*n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,直到所有数据处理完毕,最后由一个线程进行归并。

3.3  单机+单核+受限内存

这种情况下,需要将原数据文件切割成一个一个小文件,如,采用hash(x)%M,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用hash的方法对数据文件进行切割,直到每个小文件小于内存大小,这样,每个文件可放到内存中处理。采用3.1节的方法依次处理每个小文件。

3.4  多机+受限内存

这种情况下,为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用3.3节中的策略解决本地的数据。可采用hash+socket方法进行数据分发。

从实际应用的角度考虑,3.1~3.4节的方案并不可行,因为在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。算法应该具有良好的扩展性,以便数据量进一步加大(随着业务的发展,数据量加大是必然的)时,在不修改算法框架的前提下,可达到近似的线性比;算法应该具有容错性,即当前某个文件处理失败后,能自动将其交给另外一个线程继续处理,而不是从头开始处理。

Top k问题很适合采用MapReduce框架解决,用户只需编写一个map函数和两个reduce 函数,然后提交到Hadoop(采用mapchain和reducechain)上即可解决该问题。对于map函数,采用hash算法,将hash值相同的数据交给同一个reduce task;对于第一个reduce函数,采用HashMap统计出每个词出现的频率,对于第二个reduce 函数,统计所有reduce task输出数据中的top k即可。

4. 总结

Top K问题是一个非常常见的问题,公司一般不会自己写个程序进行计算,而是提交到自己核心的数据处理平台上计算,该平台的计算效率可能不如直接写程序高,但它具有良好的扩展性和容错性,而这才是企业最看重的。

5. 参考资料

《十道海量数据处理面试题与十个方法大总结》:http://blog.csdn.net/v_JULY_v/archive/2011/03/26/6279498.aspx

原创文章,转载请注明: 转载自董的博客

本文链接地址: http://dongxicheng.org/big-data/select-ten-from-billions/

 

15) 一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件:  100=

 

方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。

 

实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。

 

16)  已知一个已经从小到大排序的数组,这个数组中的一个平台(Plateau)就是连续的一串值相同的元素 ,并且这一串元素不能再延伸。例如,在 1,2,2,3,3,3,4,5,5,6中[1]、[2,2]、[3,3,3]、[4]、[5,5]、[6]都是平台。是编写一个程序,接受一个数组,把这个数组中最长的平台找出 来。在上面的例子中3,3,3就是该数组中最长的平台。

【说明】
这个程序十分简单,但是要编写好却不容易,因此在编写程序时应该考虑下面 几点:
(1) 使用的变量越少越好;
(2) 把数组的元素每一个都只查一次就得到结果;
(3) 程序语句也要越少越好。
这个问题曾经困扰过David Gries 这位知名的计算机科学家。本题与解答取自David Gries 编写的有关程序设计的专著。

#include 

int longest_plateau()

{

     int  x[] = { 3, 4, 4, 7, 8, 9, 9, 9, 9, 10};

     int  n   = sizeof(x)/sizeof(int);

     int  length = 1;         /* plateau length >= 1.     */

     int  i;

 

     for (i = 1; i < n; i++)

          if (x[i] == x[i-length])

               length++;

     return length;

}

这是一个时间复杂度为O(n) 的经典算法,其代码十分简练。

改进:

http://hxraid.iteye.com/blog/655389

 

17)  元素选择问题   : 给定线性序集中n个元素和一个整数k(1<=k<=n),要求找出这n个元素中第k小的元素(第n-k大)。 这一问题可以演化成找最大最小值、找中位数等。

最简单思想:如果是直接找最大最小值,则可以通过N次比较来完成,其时间复杂度为O(N),空间复杂度为O(1)。除此之外,对于一般的k值,可以考虑对序列N先进行排序,然后直接定位第k个位置上的数即可。时间复杂度最好为O(N*logN)。

 

改进思想:

(1) 在某些特殊情况下,是很容易设计出O(N)的算法。比如最大最小值的时候。

      如果k<=n/logn 找第k小的元素。我们可以通过堆排序的方法。 首先建立小顶堆,其时间复杂度为O(N)。然后每次输出堆顶元素(当前堆的最小值)后调整堆顶,其时间复杂度为O(logN)。循环k次,当第k次输出堆顶时结束。这样的时间复杂度为O(N+k*logN),  而k<=n/logn,即k接近于常数,则时间复杂度近似为O(N)。

      如果k>=n-n/logn 找第k小的元素。同理可以建立一个(n-k)次输出的大顶堆即可。

      当k的大小靠近n的两侧时,比如n=10,k=2或8。我们可以同归堆排序来达到近似O(N)的时间复杂度。

 

(2) 但一般的k值,特别是中位数的选择问题似乎就比上一种情况要难了。但事实上,我们仍然可以在O(n)的时间内解决。

      可以考虑快排的分治算法,对N序列进行一次Partition。与快排不同的是,每次只对划分后的一个子数组进行处理。

 

18) 有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。

问题:

1、 如何判断一个链表是不是这类链表?

2、 如果链表为存在环,如果找到环的入口点?

一、判断链表是否存在环,办法为:

设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定 相遇。(当然,fast先行头到尾部为NULL,则为无环链表)程序如下:

bool IsExitsLoop(slist *head)

{

    slist *slow = head, *fast = head;

    while ( fast && fast->next )

    {

        slow = slow->next;

        fast = fast->next->next;

        if ( slow == fast ) break;

    }

    return !(fast == NULL || fast->next == NULL);

}

二、找到环的入口点

当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则 fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:

2s = s + nr
s= nr

设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a。
a + x = nr
a + x = (n – 1)r +r = (n-1)r + L - a
a = (n-1)r + (L – a – x)

(L – a – x)为相遇点到环入口点的距离,由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是我们从链表头、与相遇点分别设一个指针,每 次各走一步,两个指针必定相遇,且相遇第一点为环入口点。程序描述如下:

slist* FindLoopPort(slist *head)

{

    slist *slow = head, *fast = head;

    while ( fast && fast->next )

    {

        slow = slow->next;

        fast = fast->next->next;

        if ( slow == fast ) break;

    }

    if (fast == NULL || fast->next == NULL)

        return NULL;

    slow = head;

    while (slow != fast)

    {

         slow = slow->next;

         fast = fast->next;

    }

    return slow;

}

扩展问题:

判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。

比较好的方法有两个:

一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。

二、如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同 样的结尾点,则两个链表相交。

这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。

 

问题描述:

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

解答转自:http://blog.redfox66.com/post/2010/09/23/top-k-algoriyhm-analysis.aspx

问题解析:

【分析】:要统计最热门查询,首先就是要统计每个Query出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法。下面分别给出这两步的算法:


第一步:Query统计

算法一:直接排序法

首先我们能想到的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。但是 题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是225Byte,很显然要占据2.55G内存,这个条件就不满足要求了。

让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候,我们可以采用外排序的方法来进行排序,这里笔者采用归并排序,是因为归并排序有一个比较好的时间复杂度O(NlgN)。

排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。

综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度是O(N),因此该算法的总体时间复杂度就是O(NlgN)。

算法二:Hash Table法

在上个方法中,我们采用了排序的办法来统计每个Query出现的次数,时间复杂度是NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢?
题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑 把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。
那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串 不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度 内完成了对该海量数据的处理。
本方法相比算法一:在时间复杂度上提高了一个数量级,但不仅仅是时间复杂度上的优化,该方法只需要IO数据文件一次,而算法一的IO次数较多的,因此该算法比算法一在工程上有更好的可操作性。


第二步:找出Top 10

算法一:排序

我想对于排序算法大家都已经不陌生了,这里不在赘述,我们要注意的是排序算法的时间复杂度是NlgN,在本题目中,三百万条记录,用1G内存是可以存下的。

算法二:部分排序

题目要求是求出Top 10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10Query,按照每个Query的统计次数由 大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数 据淘汰,加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。
不难分析出,这样的算法的时间复杂度是N*K, 其中K是指top多少。

算法三:堆

在算法二中,我们已经将时间复杂度由NlogN优化到NK,不得不说这是一个比较大的改进了,可是有没有更好的办法呢?
分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组 是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多 了。不过,这个算法还是比算法二有了改进。
基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。
借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此到这里,我们的算法可以改进为这样,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。。。
那么这样,这个算法发时间复杂度就降到了NlogK,和算法而相比,又有了比较大的改进。


结语:

至此,我们的算法就完全结束了,经过步骤一和步骤二的最优结合,我们最终的时间复杂度是O(N) + O(N’)logK。如果各位有什么好的算法,欢迎跟帖讨论。

 

5. 动态查找树比较

介绍的动态查找树主要有: 二叉查找树(BST),平衡二叉查找树(AVL),红黑树(RBT),B~/B+树(B-tree)。这四种树都具备下面几个优势:

1.     都是动态结构。在删除,插入操作的时候,都不需要彻底重建原始的索引树。最多就是执行一定量的旋转,变色操作来有限的改变树的形态。而这些操作所付出的代价都远远小于重建一棵树。这一优势在《查找结构专题(1):静态查找结构概论 》中讲到过。

2.     查找的时间复杂度大体维持在O(log(N))数量级上。可能有些结构在最差的情况下效率将会下降很快,比如BST。这个会在下面的对比中详细阐述。

下面我们开始概括性描述这四种树,并相互比较一下优劣。

二叉查找树  (Binary Search Tree)  

详细见《查找结构专题(2):二叉查找树[BST] 》

      很显然,二叉查找树的发现完全是因为静态查找结构在动态插入,删除结点所表现出来的无能为力(需要付出极大的代价)。

 

   BST 的操作代价分析:

1)     查找代价: 任何一个数据的查找过程都需要从根结点出发,沿某一个路径朝叶子结点前进。因此查找中数据比较次数与树的形态密切相关。

当树中每个结点左右子树高度大致相同时,树高为logN。则平均查找长度与logN成正比,查找的平均时间复杂度在O(logN)数量级上。

当先后插入的关键字有序时,BST退化成单支树结构。此时树高n。平均查找长度为(n+1)/2,查找的平均时间复杂度在O(N)数量级上。

2)     插入代价: 新结点插入到树的叶子上,完全不需要改变树中原有结点的组织结构。插入一个结点的代价与查找一个不存在的数据的代价完全相同。

3)     删除代价: 当删除一个结点P,首先需要定位到这个结点P,这个过程需要一个查找的代价。然后稍微改变一下树的形态。如果被删除结点的左、右子树只有一个存在,则改变形态的代价仅为O(1)。如果被删除结点的左、右子树均存在,只需要将当P的左孩子的右孩子的右孩子的...的右叶子结点与P互换,在改变一些左右子树即可。因此删除操作的时间复杂度最大不会超过O(logN)。

 

    BST效率总结 :  查找最好时间复杂度O(logN),最坏时间复杂度O(N)。

                            插入删除操作算法简单,时间复杂度与查找差不多

 

平衡二叉查找树 ( Balanced Binary Search Tree ) 

详细见《查找结构专题(3):平衡二叉查找树[AVL] 》

      二叉查找树在最差情况下竟然和顺序查找效率相当,这是无法仍受的。事实也证明,当存储数据足够大的时候,树的结构对某些关键字的查找效率影响很大。当然,造成这种情况的主要原因就是BST不够平衡(左右子树高度差太大)。

      既然如此,那么我们就需要通过一定的算法,将不平衡树改变成平衡树。因此,AVL树就诞生了。

 

    AVL 的操作代价分析:

    (1) 查找代价: AVL是严格平衡的BST(平衡因子不超过1)。那么查找过程与BST一样,只是AVL不会出现最差情况的BST(单支树)。因此查找效率最好,最坏情况都是O(logN)数量级的。

 

    (2) 插入代价: AVL必须要保证严格平衡(|bf|<=1),那么每一次插入数据使得AVL中某些结点的平衡因子超过1就必须进行旋转操作。事实上,AVL的每一次插入结点操作最多只需要旋转1次(单旋转或双旋转)。因此,总体上插入操作的代价仍然在O(logN)级别上(插入结点需要首先查找插入的位置)。

 

    (3) 删除代价:AVL删除结点的算法可以参见BST的删除结点,但是删除之后必须检查从删除结点开始到根结点路径上的所有结点的平衡因子。因此删除的代价稍微要大一些。每一次删除操作最多需要O(logN)次旋转。因此,删除操作的时间复杂度为O(logN)+O(logN)=O(2logN)

 

    AVL 效率总结 :  查找的时间复杂度维持在O(logN),不会出现最差情况

                           AVL树在执行每个插入操作时最多需要1次旋转,其时间复杂度在O(logN)左右。

                           AVL树在执行删除时代价稍大,执行每个删除操作的时间复杂度需要O(2logN)。

 

 

 

 

3. 红黑树(Red-Black Tree ) 详细见《查找结构专题(4):红黑树[RBT] 》

 

    二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢?

 

    能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。

 

    RBT 的操作代价分析:

     (1) 查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像AVL一样是严格平衡的,但平衡性能还是要比BST要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比AVL要略逊色一点。

 

    (2) 插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和AVL的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。

 

    (3) 删除代价:RBT的删除操作代价要比AVL要好的多,删除一个结点最多只需要3次旋转操作。

 

    RBT 效率总结 : 查找 效率最好情况下时间复杂度为O(logN),但在最坏情况下比AVL要差一些,但也远远好于BST。

                          插入和删除操作改变树的平衡性的概率要远远小于AVL(RBT不是高度平衡的)。因此需要的旋转操作的可能性要小,而且一旦需要旋转,插入一个结点最多只需要旋转2次,删除最多只需要旋转3次(小于AVL的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在O(logN),但是实际上,这种操作由于简单所需要的代价很小。

 

 

 

 

4. B~树/B+树(B-Tree ) 详细见《查找结构专题(5):B~树/B+树 》

 

     对于在内存中的查找结构而言,红黑树的效率已经非常好了(实际上很多实际应用还对RBT进行了优化)。但是如果是数据量非常大的查找呢?将这些数据全部放入内存组织成RBT结构显然是不实际的。实际上,像OS中的文件目录存储,数据库中的文件索引结构的存储.... 都不可能在内存中建立查找结构。必须在磁盘中建立好这个结构。那么在这个背景下,RBT还是一种好的选择吗?

 

     在磁盘中组织查找结构,从任何一个结点指向其他结点都有可能读取一次磁盘数据,再将数据写入内存进行比较。大家都知道,频繁的磁盘IO操作,效率是很低下的(机械运动比电子运动要慢不知道多少)。显而易见,所有的二叉树的查找结构在磁盘中都是低效的。因此,B树很好的解决了这一个问题。

 

    B-Tree的操作代价分析:

    (1) 查找代价: B-Tree作为一个平衡多路查找树(m-叉)。B树的查找分成两种:一种是从一个结点查找另一结点的地址的时候,需要定位磁盘地址(查找地址),查找代价极高。另一种是将结点中的有序关键字序列放入内存,进行优化查找(可以用折半),相比查找代价极低。而B树的高度很小,因此在这一背景下,B树比任何二叉结构查找树的效率都要高很多。而且B+树作为B树的变种,其查找效率更高。

 

    (2)插入代价: B-Tree的插入会发生结点的分裂操作。当插入操作引起了s个节点的分裂时,磁盘访问的次数为h(读取搜索路径上的节点)+2s(回写两个分裂出的新节点)+1(回写新的根节点或插入后没有导致分裂的节点)。因此,所需要的磁盘访问次数是h+2s+1,最多可达到3h+1。因此插入的代价是很大的。

 

    (3)删除代价:B-Tree的删除会发生结点合并操作。最坏情况下磁盘访问次数是3h=(找到包含被删除元素需要h次
读访问)+(获取第2至h层的最相邻兄弟需要h-1次读访问)+(在第3至h层的合并需要h-2次写
访问)+(对修改过的根节点和第2层的两个节点进行3次写访问)

 

   B-Tree效率总结: 由于考虑磁盘储存结构,B树的查找、删除、插入的代价都远远要小于任何二叉结构树(读写磁盘次数的降低)。

 

 

 

 

动态查找树结构的对比:

 

(1) 平衡二叉树和红黑树  [AVL  PK  RBT]

 

      AVL 和RBT 都是二叉查找树的优化。其性能要远远好于二叉查找树。他们之间都有自己的优势,其应用上也有不同。

 

      结构对比: AVL的结构高度平衡,RBT的结构基本平衡。平衡度AVL > RBT.

 

      查找对比: AVL 查找时间复杂度最好,最坏情况都是O(logN)。

                     RBT 查找时间复杂度最好为O(logN),最坏情况下比AVL略差。

 

      插入删除对比:  1. AVL的插入和删除结点很容易造成树结构的不平衡,而RBT的平衡度要求较低。因此在大量数据插入的情况下,RBT需要通过旋转变色操作来重新达到平衡的频度要小于AVL。

                            2. 如果需要平衡处理时,RBT比AVL多一种变色操作,而且变色的时间复杂度在O(logN)数量级上。但是由于操作简单,所以在实践中这种变色仍然是非常快速的。

                            3. 当插入一个结点都引起了树的不平衡,AVL和RBT都最多需要2次旋转操作。但删除一个结点引起不平衡后,AVL最多需要logN 次旋转操作,而RBT最多只需要3次。因此两者插入一个结点的代价差不多,但删除一个结点的代价RBT要低一些。

                            4. AVL和RBT的插入删除代价主要还是消耗在查找待操作的结点上。因此时间复杂度基本上都是与O(logN) 成正比的。

 

        总体评价:大量数据实践证明,RBT的总体统计性能要好于平衡二叉树。

 

 

(2) B~树和B+树    [ B~Tree   PK  B+Tree]

 

      B+树是B~树的一种变体,在磁盘查找结构中,B+树更适合文件系统的磁盘存储结构。

 

      结构对比: B~树是平衡多路查找树,所有结点中都包含了待查关键字的有效信息(比如文件磁盘指针)。每个结点若有n个关键字,则有n+1个指向其他结点的指针。

                    B+树严格意义上说已经不是树,它的叶子结点之间也有指针链接。B+树的非终结点中并不含有关键字的信息,需要查找的关键字的全部信息都包含在叶子结点上。非终结点中只作为叶子结点关键字的索引而存在。

 

      查找对比:1. 在相同数量的待查数据下,B+树查找过程中需要调用的磁盘IO操作要少于普通B~树。由于B树所在的磁盘存储背景下,因此B+树的查找性能要好于B~树。

                    2. B+树的查找效率更加稳定,因为所有叶子结点都处于同一层中,而且查找所有关键字都必须走完从根结点到叶子结点的全部历程。因此同一颗B+树中,任何关键字的查找比较次数都是一样的。而B树就不一定了,可能查找到某一个非终结点就结束了。

 

      插入删除对比:  B+树与B~树在插入删除操作中的效率是差不多的。

 

      总体评价:在应用背景下,特别是文件结构存储中。B+树的应用要更多,其效率也要比B~树好。

 

字符串查找结构

 

这次专题所讲的BST、AVL、BRT、B~Tree等可以胜任对任何关键字数据进行查找。但对字符串的查找(字符串匹配)结构,有专门的结构和算法。详见:《KMP算法 》,《TrieTree 》

 

1. 概述

排序算法是计算机技术中最基本的算法,许多复杂算法都会用到排序。尽管各种排序算法都已被封装成库函数供程序员使用,但了解排序算法的思想和原理,对于编写高质量的软件,显得非常重要。

本文介绍了常见的排序算法,从算法思想,复杂度和使用场景等方面做了总结。

2. 几个概念

(1)排序稳定:如果两个数相同,对他们进行的排序结果为他们的相对顺序不变。例如A={1,2,1,2,1}这里排序之后是A = {1,1,1,2,2} 稳定就是排序后第一个1就是排序前的第一个1,第二个1就是排序前第二个1,第三个1就是排序前的第三个1。同理2也是一样。不稳定就是他们的顺序与开始顺序不一致。

(2)原地排序:指不申请多余的空间进行的排序,就是在原来的排序数据中比较和交换的排序。例如快速排序,堆排序等都是原地排序,合并排序,计数排序等不是原地排序。

总体上说,排序算法有两种设计思路,一种是基于比较,另一种不是基于比较。《算法导论》一书给出了这样一个证明:“基于比较的算法的最优时间复杂度是O(N lg N)”。对于基于比较的算法,有三种设计思路,分别为:插入排序,交换排序和选择排序。非基于比较的排序算法时间复杂度为O(lg N),之所以复杂度如此低,是因为它们一般对排序数据有特殊要求。如计数排序要求数据范围不会太大,基数排序要求数据可以分解成多个属性等。

3. 基于比较的排序算法

正如前一节介绍的,基于比较的排序算法有三种设计思路,分别为插入,交换和选择。对于插入排序,主要有直接插入排序,希尔排序;对于交换排序,主要有冒泡排序,快速排序;对于选择排序,主要有简单选择排序,堆排序;其它排序:归并排序。

3.1  插入排序

(1) 直接插入排序

特点:稳定排序,原地排序,时间复杂度O(N*N)

思想:将所有待排序数据分成两个序列,一个是有序序列S,另一个是待排序序列U,初始时,S为空,U为所有数据组成的数列,然后依次将U中的数据插到有序序列S中,直到U变为空。

适用场景:当数据已经基本有序时,采用插入排序可以明显减少数据交换和数据移动次数,进而提升排序效率。

(2)希尔排序

特点:非稳定排序,原地排序,时间复杂度O(n^lamda)(1 < lamda < 2), lamda和每次步长选择有关。

思想:增量缩小排序。先将序列按增量划分为元素个数近似的若干组,使用直接插入排序法对每组进行排序,然后不断缩小增量直至为1,最后使用直接插入排序完成排序。

适用场景:因为增量初始值不容易选择,所以该算法不常用。

3.2  交换排序

(1)冒泡排序

特点:稳定排序,原地排序,时间复杂度O(N*N)

思想:将整个序列分为无序和有序两个子序列,不断通过交换较大元素至无序子序列首完成排序。

适用场景:同直接插入排序类似

(2)快速排序

特点:不稳定排序,原地排序,时间复杂度O(N*lg N)

思想:不断寻找一个序列的枢轴点,然后分别把小于和大于枢轴点的数据移到枢轴点两边,然后在两边数列中继续这样的操作,直至全部序列排序完成。

适用场景:应用很广泛,差不多各种语言均提供了快排API

3.3  选择排序

(1)简单选择排序

特点:不稳定排序(比如对3 3 2三个数进行排序,第一个3会与2交换),原地排序,时间复杂度O(N*N)

思想:将序列划分为无序和有序两个子序列,寻找无序序列中的最小(大)值和无序序列的首元素交换,有序区扩大一个,循环下去,最终完成全部排序。

适用场景:交换少

(2) 堆排序

特点:非稳定排序,原地排序,时间复杂度O(N*lg N)

思想:小顶堆或者大顶堆

适用场景:不如快排广泛

3.4  其它排序

(1) 归并排序

特点:稳定排序,非原地排序,时间复杂度O(N*N)

思想:首先,将整个序列(共N个元素)看成N个有序子序列,然后依次合并相邻的两个子序列,这样一直下去,直至变成一个整体有序的序列。

适用场景:外部排序

4. 非基于比较的排序算法

非基于比较的排序算法主要有三种,分别为:基数排序,桶排序和计数排序。这些算法均是针对特殊数据的,不如要求数据分布均匀,数据偏差不会太大。采用的思想均是内存换时间,因而全是非原地排序。

4.1 基数排序

特点:稳定排序,非原地排序,时间复杂度O(N)

思想:把每个数据看成d个属性组成,依次按照d个属性对数据排序(每轮排序可采用计数排序),复杂度为O(d*N)

适用场景:数据明显有几个关键字或者几个属性组成

4.2  桶排序

特点:稳定排序,非原地排序,时间复杂度O(N)

思想:将数据按大小分到若干个桶(比如链表)里面,每个桶内部采用简单排序算法进行排序。

适用场景:0

4.3  计数排序

特点:稳定排序,非原地排序,时间复杂度O(N)

思想:对每个数据出现次数进行技术(用hash方法计数,最简单的hash是数组!),然后从大到小或者从小到大输出每个数据。

使用场景:比基数排序和桶排序广泛得多。

5.  总结

对于基于比较的排序算法,大部分简单排序(直接插入排序,选择排序和冒泡排序)都是稳定排序,选择排序除外;大部分高级排序(除简单排序以外的)都是不稳定排序,归并排序除外,但归并排序需要额外的存储空间。对于非基于比较的排序算法,它们都对数据规律有特殊要求 ,且采用了内存换时间的思想。排序算法如此之多,往往需要根据实际应用选择最适合的排序算法。

19)   

你可能感兴趣的:(算法)