海量数据面试题目解析:
1、A,B两个文件各存放50亿条URL,每条URL占用64字节,内存限制4G,找出A,B文件共同的URL
答:方案一、50亿条数据,每条64字节。文件大小等于50G*64=320G,远大于内存限制的4G,不能将其完全加载到内存中,考虑分而治之的方法:
1、分而治之/hash映射:遍历文件A,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件中(a0,a1,...a999),每个文件大小约300M
2、遍历文件B,对每个url采取和A相同的方式分别存储到1000个小文件中(b0,b1,...b999),这样处理后,所有可能相同的url都在对应的小文件(a0 vs b0,a1 vs b1,...,a999 vs b999)中,不对应的小文件不可能有相同的url。然后求出1000对小文件中相同的url即可
3、hash_set统计:求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中,然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件中就可以了
方案二、
如果允许有一定的容错率,可以使用bloom filter,4G内存大概表示340亿bit。将其中一个文件中的url使用bloom filet映射到340亿bit,然后挨个读取另外一个文件中的url,检查是否与Bloom filter相同,如果是,则具有相同的url(注意会有一定的错误率)
2、10个文件,每个1G,每个文件每行存放的是用户的query,每个文件的query可能重复,要求按照query的频度排序
方案一、
1、hash映射:顺序读取10个文件,按照hash(query)%10的结果,将query写入到另外10个文件(a0,...,a9)中,这样生成的文件每个的大小大约也在1G左右(hash函数随机)
2、hash_map统计:找一台内存在2G左右的机器,依次对a0,a1,...,a9用hash_map(query,query_count)来统计每个query出现的次数。利用快速排序/堆排序/归并排序按照出现次数进行排序。将排好序的query和对应的query_count输出到文件中。这样得到了10个排好序的文件b0,b1,b2...b9
3、堆/快速/归并排序:对b0,b1,...b9这10个文件进行归并排序(内排序与外排序相结合)
方案二、
一般query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存中。这样,我们就可以采用trie树或者hash_map等直接来统计每个query出现的次数,然后按照出现次数做快速排序/堆排序/归并排序就可以了
方案三、
与方案一类似,但在做完hash,分成多个文件后,可以交给多个文件来处理,采用分布式的架构来处理(mapreduce),之后进行合并。
3、1G大小的文件,里面每行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词
a、顺序读文件,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件中,这样每个文件大概200K左右。如果其中有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。对每个小文件,统计每个文件中出现的词及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个节点的最小堆),并把100个词对应的频率存入文件,这样又取到了5000个文件。下一步就是把这5000个文件进行归并
b、这个数据具有很明显的特点,词的大小为 16 个字节,但是内存只有 1m做hash有些不够,所以可以用来排序。内存可以当输入缓冲区使用。
4、海量日志数据,提取出某日访问百度次数最多的那个IP
方案一、
取出一天中访问百度的日志中的IP,逐个写入到一个大文件中。注意IP是32位的,最多有2^32个IP。同样采用映射的方法,比如mode 1000.把整个大文件映射到1000个小文件中,再找出这些小文件中出现频率最大的IP(可以采用hash_map进行频率统计,找出频率最大的几个)及相应的频率。然后再在这1000个中最大的IP中,找出频率最大的IP,即为所求。
方案二、
算法思想:分而治之+hash
1、IP地址最多有2^32=4G种取值情况,不可能完全加载到内存中处理
2、可以考虑采用“分而治之”的思想,按照IP地址的Hash(IP)%1024值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
3、对于每一个小文件,可以构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址;
4、可以得到1024个小文件中的出现次数最多的IP,再依据常规的排序算法得到总体上出现次数最多的IP;
附:
1、Hash取模是一种等价映射,不会存在同一个元素分散到不同小文件中去的情况,即这里采用的是mod1000算法,那么相同的IP在hash后,只可能落在同一个文件中,不可能被分散的。
2、那到底什么是hash映射呢?简单来说,就是为了便于计算机在有限的内存中处理big数据,从而通过一种映射散列的方式让数据均匀分布在对应的内存位置(如大数据通过取余的方式映射成小树存放在内存中,或大文件映射成多个小文件),而这个映射散列方式便是我们通常所说的hash函数,设计的好的hash函数能让数据均匀分布而减少冲突。尽管数据映射到了另外一些不同的位置,但数据还是原来的数据,只是代替和表示这些原始数据的形式发生了变化而已。
5、2.5亿个整数中找出不重复的整数,内存空间不足以容纳这2.5亿个整数
方案一、采用2-bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11表示无意义)进行,共需内存,2^32*2=1GB内存。还可以接受。然后扫描这2.5亿个整数,查看bitmap中相对应位,如果是00变01,01变10,10保持不变。查看bitmap,吧对应位是01的整数输出即可
将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map,都是一样的道理。
方案二、进行小文件的划分,然后在小文件中找出不重复的整数,并做排序,然后进行归并,注意去重
有点像鸽巢原理,整数个数为 2^32,也就是,我们可以将这 2^32 个数,划分为 2^8 个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。
6、海量数据分布在100台电脑上,想办法高效统计出这批数据的TOP10
1、在每台电脑上求出TOP10,然后采用包含10个元素的堆完成(TOP10小,用最大堆;TOP10大用最小堆)。求TOP10大,首先取前10个元素调整成最小堆,如果发现,然后扫描后面的数据,并与堆顶元素标胶,如果比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆,最后堆中的元素就是TOP10大
2、求出每台电脑上的TOP10后,然后把这100台电脑上的TOP10 组合起来工1000个数据,再利用上面类似的方法求出TOP10就可以了
注意点:TOP10是指最大的10个数,而不是指出现频率最多的10个数。
附:
如果同一个元素重复出现在不同的电脑中,解决方法如下:
法一、遍历一遍所有数据,重新hash取摸,如此使得同一个元素只出现在单独的一台电脑中,然后采用上面所说的方法,统计每台电脑中各个元素的出现次数找出TOP10,继而组合100台电脑上的TOP10,找出最终的TOP10。
法二、或者,暴力求解:直接统计统计每台电脑中各个元素的出现次数,然后把同一个元素在不同机器中的出现次数相加,最终从所有数据中找出TOP10。
7、海量数据中找出重复次数最多的那一个
先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。然后找出上一步求出的数据中重复次数最多的一个就是所求(参考前面)
8、海量数据中找出重复次数最多的前N个数据
上千万或上亿的数据,现在的机器的内存应该能存下。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了,可以用第6题提到的堆机制完成。
(统计可以用hash 二叉树 trie树 对统计结果用堆求出现的前n大数据,增加点限制可以提高效率。比如:出现次数>数据总数/N的一定在前N个之内)
9、1KW字符串,其中有重复的,需要去重,保留没有重复的字符串,该如何设计和实现?
方案一、
这题用trie树比较合适,hash_map也应该能行。
方案二、
1000w的数据规模插入操作完全不现实,以前试过在stl下100w元素插入set中已经慢得不能忍受,觉得基于hash的实现不会比红黑树好太多,使用vector+sort+unique都要可行许多,建议还是先hash成小文件分开处理再综合。
附:
1、hash_set在千万级数据下,insert操作优于set
2、那map和hash_map的性能
3、查询操作
小数据量时用map,构造快,大数据量时用hash_map
rbtree PK hashtable
当数据量基本上int型key时,hash table是rbtree的3-4倍,但hash table一般会浪费大概一半内存。
因为hash table所做的运算就是个%,而rbtree要比较很多,比如rbtree要看value的数据 ,每个节点要多出3个指针(或者偏移量) 如果需要其他功能,比如,统计某个范围内的key的数量,就需要加一个计数成员。
且1s rbtree能进行大概50w+次插入,hash table大概是差不多200w次。不过很多的时候,其速度可以忍了,例如倒排索引差不多也是这个速度,而且单线程,且倒排表的拉链长度不会太大。正因为基于树的实现其实不比hashtable慢到哪里去,所以数据库的索引一般都是用的B/B+树,而且B+树还对磁盘友好(B树能有效降低它的高度,所以减少磁盘交互次数)。比如现在非常流行的NoSQL数据库,像MongoDB也是采用的B树索引。
10、一个文本文件,大约有1W行,每行一个词,要求统计频繁出现的前10个词,给出思想及时间复杂度分析
该题目考察的是时间效率,用Trie树来统计每个词出现的次数,时间复杂度为O(n*le)(le表示单词的平准长度)。
然后是找出出现最频繁的前10个词,可以用堆来实现,前面的题中已经讲到了,时间复杂度是O(n*lg10)。所以总的时间复杂度,是O(n*le)与O(n*lg10)中较大的哪一个。
11、一个文本文件,找出前10个经常出现的词,文件较长 ,有上亿行,无法一次全部读入内存,求最优解
首先根据用hash并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词
12、100W个数中找最大的前100个数
方案1
采用局部淘汰法,选取前100个元素,并作排序,记做序列L。然后依次扫描剩余的元素X,与排好序的100个元素中最小的元素比,如果比这个最小的要打,那么把这个最小的元素删除,并把X利用插入排序的思想,插入到序列L中。依次循环,直到扫描了所有的元素,复杂度为O(100w*100)。
方案2
采用快速排序的思想,每次分割之后只考虑比轴大的一部分,知道比轴大的一部分在比100多的时候,采用传统排序算法排序,取前100个。复杂度为O(100w*100)。
方案3
在前面的题中,我们已经提到了,用一个含100个元素的最小堆完成。复杂度为O(100w*lg100)。
13、搜索引擎通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1~255字节,假设目前有1000W个记录
这些查询串的重复度较高,虽然总数是1KW,除去重复后,不超过300W。一个串出现的重复度越高,查询它的用户越多,也就越热门
统计最热门的10个查询串,要求使用的内存不超过1G
a、描述解决问题的思路
b、给出主要处理流程,算法,及算法复杂度
答:
方案一、
采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。
方案二、
1、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计
2、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。
即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)
附:
虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去(300万个字符串假设没有重复,都是最大长度,那么最多占用内存3M*1K/4=0.75G。所以可以将所有字符串都存放在内存中进行处理),而现在只是需要一个合适的数据结构,在这里,HashTable绝对是我们优先的选择。
所以我们放弃分而治之/hash映射的步骤,直接上hash统计,然后排序。So,针对此类典型的TOP K问题,采取的对策往往是:hashmap + 堆。如下所示:
1、hash_map统计:先对这批海量数据预处理。具体方法是:维护一个Key为Query字串,Value为该Query出现次数的HashTable,即hash_map(Query,Value),每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内用Hash表完成了统计;
2、堆排序:第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。即借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。所以,我们最终的时间复杂度是:O(N) + N' * O(logK),(N为1000万,N’为300万)。
“维护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。”
14、一共N个机器,每个机器上有N个数,每个机器最多存储O(N)个数并对其进行操作,如何找到N^2个数的中数(median)
方案1
先大体估计一下这些数的范围,比如这里假设这些数都是32位无符号整数(共有2^32个)。我们把0到2^32-1的整数划分为N个范围段,每个段包含(2^32)/N个整数。比如,第一个段位0到2^32/N-1,第二段为(2^32)/N到(2^32)/N-1,…,第N个段为(2^32)(N-1)/N到2^32-1。然后,扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。注意这个过程每个机器上存储的数应该是O(N)的。下面我们依次统计每个机器上数的个数,一次累加,直到找到第k个机器,在该机器上累加的数大于或等于(N^2)/2,而在第k-1个机器上的累加数小于(N^2)/2,并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第(N^2)/2-x位。然后我们对第k个机器的数排序,并找出第(N^2)/2-x个数,即为所求的中位数的复杂度是O(N^2)的。
方案2
先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第(N^2)/2个便是所求。复杂度是O(N^2*lgN^2)的。
15、最大间隙问题
给定n个实数,x1,x2,x3...,xn求这n个实数在实轴上向量两个数之间最大差值,要求线性的时间算法
对这n个数据进行排序,然后一遍扫描即可确定相邻的最大间隙。该方法不能满足线性时间的要求。采用如下方法:
1、找到n个数据中最大和最小数据max和min
2、用n-2个点等分区间[min,max],即将[min.max]等分为n-1个区间(前闭后开区间),将这些区间看做桶,编号为1,2,3...n-2,n-1,且桶i的上界和桶i+1的下界相同,每个桶的大小相同。每个桶的大小为d=(max-min)/n-1.实际上这些桶的边界构成了一个等差数组(min首项,max尾项,步长为d),min放在第一个桶,max放在n-1个桶
3、将n个数放入n-1个桶中,将每个元素x[i]分配到某个桶中(编号为index),index=[(x[i]-min)/d +]+1,并求出每个桶的最大最小数据
4、最大间隙:除最大最小数据max和min以外的n-2个数据放入n-1个桶中,由抽屉原理可知至少有一个桶是空的,又因为每个桶的大小相同,所以最大间隙不会在同一桶中出现,一定是某个桶的上界和气候某个桶的下界之间隙,且该量筒之间的桶(即便好在该连个便好之间的桶)一定是空桶。也就是说,最大间隙在桶i的上界和桶j的下界之间产生j>=i+1。一遍扫描即可完成。
16、将多个集合合并成没有交集的集合
给定一个字符串的集合,格式如:{aaa,bbb,ccc},{bbb,ddd},{eee,fff},{ggg},{ddd,hhh},要求将其中交集不为空的集合合并,要求合并完成的集合之间无交集,例如上例应输出{aaa,bbb,ccc,ddd,hhh},{eee,fff},{ggg}
a、解决问题的思路
b、主要的处理流程,算法及算法的复杂度
c、描述可能的改进
采用并查集。首先所有的字符串都在单独的并查集中。然后依次扫描每个集合,将两个相邻元素顺序合并。
例如:对于{aaa,bbb,ccc},首先查看aaa和bbb是否在同一个并查集中,如果不在,那么也把它们所在的并查集合并。接下来再扫描其他的集合,当所有的集合都扫描完了,并查集代表的集合便是所求。
复杂度应该是O(NlgN)的。
改进的话,首先可以记录每个节点的根结点,改进查询。合并的时候,可以把大的和小的进行合,这样也减少复杂度。
17、最大子序列与最大子矩阵问题
数组的最大子序列问题:给定一个数组,其中元素有正,也有负,找出其中一个连续子序列,使和最大
方案1、
这个问题可以动态规划的思想解决。设b[i]表示以第i个元素a[i]结尾的最大子序列,那么显然
b[i+1]=b[i]>0?b[i]+a[i+1]:a[i+1] 基于这一点可以很快用代码实现。
最大子矩阵问题:给定一个矩阵(二维数组),其中数据有大有小,请找一个子矩阵,使得子矩阵的和最大,并输出这个和。
方案2、
可以采用与最大子序列类似的思想来解决。如果我们确定了选择第i列和第j列之间的元素,那么在这个范围内,其实就是一个最大子序列问题。如何确定第i列和第j列可以词用暴搜的方法进行。
18、给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
方案一、申请512M的内存,一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
方案二、因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;这里我们把40亿个数中的每一个用32位的二进制来表示,假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类:
1.最高位为0
2.最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找
再然后把这个文件为又分成两类:
1.次最高位为0
2.次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了); 与要查找的数的次最高位比较并接着进入相应的文件再查找。
以此类推,就可以找到了,而且时间复杂度为O(logn)。
附:位图法
使用位图法判断整形数组是否存在重复 判断集合中存在重复是常见编程任务之一,当集合中数据量比较大时我们通常希望少进行几次扫描,这时双重循环法就不可取了。
位图法比较适合于这种情况,它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。这种给新数组初始化时置零其后置一的做法类似于位图的处理方法故称位图法。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。
19、非常大的文件,装不进内存。每行一个int类型数据,现在要你随机取100个数。
答:
操作系统中的方法,先生成4G的地址表,在把这个表划分为小的4M的小文件做个索引,二级索引。30位前十位表示第几个4M文件,后20位表示在这个4M文件的第几个,等等,基于key value来设计存储,用key来建索引。
附:
操作系统内存分页系统设计(就是映射+建索引)。
Windows 2000使用基于分页机制的虚拟内存。每个进程有4GB的虚拟地址空间。基于分页机制,这4GB地址空间的一些部分被映射了物理内存,一些部分映射硬盘上的交换文 件,一些部分什么也没有映射。程序中使用的都是4GB地址空间中的虚拟地址。而访问物理内存,需要使用物理地址。
物理地址 (physical address): 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个 地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
虚拟地址 (virtual address): 4G虚拟地址空间中的地址,程序中使用的都是虚拟地址。 使用了分页机制之后,4G的地址空间被分成了固定大小的页,每一页或者被映射到物理内存,或者被映射到硬盘上的交换文件中,或者没有映射任何东西。对于一 般程序来说,4G的地址空间,只有一小部分映射了物理内存,大片大片的部分是没有映射任何东西。物理内存也被分页,来映射地址空间。对于32bit的 Win2k,页的大小是4K字节。CPU用来把虚拟地址转换成物理地址的信息存放在叫做页目录和页表的结构里。
物理内存分页,一个物理页的大小为4K字节,第0个物理页从物理地址 0x00000000 处开始。由于页的大小为4KB,就是0x1000字节,所以第1页从物理地址 0x00001000 处开始。第2页从物理地址 0x00002000 处开始。可以看到由于页的大小是4KB,所以只需要32bit的地址中高20bit来寻址物理页。
海量数据:数据量太大,所以导致要么是无法在较短时间内迅速解决,要么是数据太大,导致无法一次性装入内存。
针对时间,我们可以采用巧妙的算法搭配合适的数据结构,如Bloom filter/Hash/bit-map/堆/数据库或倒排索引/trie树,
针对空间,无非就一个办法:大而化小,分而治之(hash映射)
所谓的单机及集群问题,通俗点来讲,
单机就是处理装载数据的机器有限(只要考虑cpu,内存,硬盘的数据交互),
而集群,机器有多辆,适合分布式处理,并行计算(更多考虑节点和节点间的数据交互)。
处理海量数据问题的方法:
1、分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
2、双层桶划分
3、Bloom filter/Bitmap;
4、Trie树/数据库/倒排索引;
5、外排序;
6、分布式处理之Hadoop/Mapreduce。
part 1、从set/map谈到hashtable/hash_map/hash_set
一般来说,STL容器分两种,
1、序列式容器(vector/list/deque/stack/queue/heap),
2、关联式容器。关联式容器又分为set(集合)和map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和multimap(多键映射表),这些容器均以RB-tree完成。此外,还有第3类关联式容器,如hashtable(散列表),以及以hashtable为底层机制完成的hash_set(散列集合)/hash_map(散列映射表)/hash_multiset(散列多键集合)/hash_multimap(散列多键映射表)。也就是说,set/map/multiset/multimap都内含一个RB-tree,而hash_set/hash_map/hash_multiset/hash_multimap都内含一个hashtable。
所谓关联式容器,类似关联式数据库,每笔数据或每个元素都有一个键值(key)和一个实值(value),即所谓的Key-Value(键-值对)。当元素被插入到关联式容器中时,容器内部结构(RB-tree/hashtable)便依照其键值大小,以某种特定规则将这个元素放置于适当位置。
包括在非关联式数据库中,比如,在MongoDB内,文档(document)是最基本的数据组织形式,每个文档也是以Key-Value(键-值对)的方式组织起来。一个文档可以有多个Key-Value组合,每个Value可以是不同的类型,比如String、Integer、List等等。
{ "name" : "July",
"sex" : "male",
"age" : 23 }
set/map/multiset/multimap
set,同map一样,所有元素都会根据元素的键值自动被排序,因为set/map两者的所有各种操作,都只是转而调用RB-tree的操作行为,不过,值得注意的是,两者都不允许两个元素有相同的键值。
不同的是:set的元素不像map那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值,而map的所有元素都是pair,同时拥有实值(value)和键值(key),pair的第一个元素被视为键值,第二个元素被视为实值。
至于multiset/multimap,他们的特性及用法和set/map完全相同,唯一的差别就在于它们允许键值重复,即所有的插入操作基于RB-tree的insert_equal()而非insert_unique()。
hash_set/hash_map/hash_multiset/hash_multimap
hash_set/hash_map,两者的一切操作都是基于hashtable之上。不同的是,hash_set同set一样,同时拥有实值和键值,且实质就是键值,键值就是实值,而hash_map同map一样,每一个元素同时拥有一个实值(value)和一个键值(key),所以其使用方式,和上面的map基本相同。但由于hash_set/hash_map都是基于hashtable之上,所以不具备自动排序功能。为什么?因为hashtable没有自动排序功能。
至于hash_multiset/hash_multimap的特性与上面的multiset/multimap完全相同,唯一的差别就是它们hash_multiset/hash_multimap的底层实现机制是hashtable(而multiset/multimap,上面说了,底层实现机制是RB-tree),所以它们的元素都不会被自动排序,不过也都允许键值重复。
总结:
因为set/map/multiset/multimap都是基于RB-tree之上,所以有自动排序功能,而hash_set/hash_map/hash_multiset/hash_multimap都是基于hashtable之上,所以不含有自动排序功能,至于加个前缀multi_无非就是允许键值重复而已。
海量数据处理方法总结:
1、Bloom filter
实现数据字典,数据的判重或者几个求交集
基本原理及要点:
位组数+K个独立hash函数,将hash函数对应的值的位数组置1,查找时如果发现hash函数对应位都是1说明存在。
这个过程并不保证查找的结果是100%正确的,同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动其他的关键字。
改进的counting Bloom filter,用一个counter数组代替位数组,可以支持删除
根据输入元素个数n,确定位数组m的大小及hash函数的个数。当hash函数个数K = (ln2)*(m/n)时错误率最小。在错误率不大于E的情况下,m至少
要等于n*lg(1/E)才能表示任意n个元素的集合,但m应该更大些,因为还要保证bit数组里至少一半为0,则m>=nlg(1/E)*lge,大概就是nlg(1/E) 1.44倍(lg表示以2为底的对数)
举个例子我们假设错误率为 0.01,则此时m应大概是n的 13 倍。这样k大概是8 个。
注意这里m与n的单位不同,m是bit为单位,而n则是以元素个数为单位(准确的说是不同元素的个数)。通常单个元素的长度都是有很多bit的。所以使用bloom filter内存上通常都是节省的。
扩展:
Bloom filter将集合中的元素映射到位数组中,用k(k为哈希函数个数)个映射位是否全 1 表示元素在不在这个集合中。Counting bloom filter(CBF)将位
数组中的每一位扩展为一个counter,从而支持了元素的删除操作。Spectral Bloom Filter(SBF)将其与集合元素的出现次数关联。SBF采用counter中的最小值来近似表示元素的出现频率。
对应于上题1
2、trie树
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。
适用范围:数据量大,重复多,但是数据种类小可以放入内存
基本原理及要点:实现方式,节点孩子的表示方式
扩展:压缩实现。
对应上题2 9 13
3、bit-map
所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。
例子:
1、假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0
2、然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0×01<<(i%8)) 当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending),因为是从零开始的,所以要把第五位置为一
3、然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1
4、然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。
可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下
基本原理及要点
使用bit数组来表示某些元素是否存在,比如8位电话号码
扩展
Bloom filter可以看做是对bit-map的扩展
问题实例:
1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)
对应上题5
4、hashing
适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存
基本原理及要点:
hash函数选择,针对字符串,整数,排列,具体相应的hash方法。碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。
扩展:
d-left hashing中的d是多个的意思,我们先简化这个问题,看一看 2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做 T1 和T2,给T1 和T2 分别配备一个哈希函数,h1 和h2。在存储一个新的key
时,同时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1 中的h1[key]位置和T2 中的h2[key]位置,哪一个位置已经存储的
(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1 子表
中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。
对应于上题4
5、堆
适用范围:海量数据前n大,并且n比较小,堆可以放入内存
基本原理及要点:最大堆求前n小,最小堆求前n大。方法,比如求前n小,我们比较当前元素与最大堆里的最大元素,如果它小于最大元素,则应该替换那个最大元素。这样最后得到的n个元素就是最小的n个。适合大数据量,求前n小,n的大小比较小的情况,这样可以扫描一遍即可得到所有的前n元素,效率很高。
扩展:双堆,一个最大堆与一个最小堆结合,可以用来维护中位数。
对应上题12
6、双层桶划分
适用范围:第k大,中位数,不重复或重复的数字
基本原理及要点:因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。可以通过多次缩小,双层只是一个例子。
例子:5 亿个int找它们的中位数。
方案一、
首先我们将int划分为 2^16 个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
实际上,如果不是int是int64,我们可以经过 3 次这样的划分即可降低到可以接受的程度。即可以先将int64 分成 2^24 个区域,然后确定区域的第几大数,在将该区域分成 2^20 个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有 2^20,就可以直接利用direct addr table进行统计了。
方案二、
同样需要做两遍统计,如果数据存在硬盘上,就需要读取2次。
方法同基数排序有些像,开一个大小为65536的Int数组,第一遍读取,统计Int32的高16位的情况,也就是0-65535,都算作0,65536 - 131071都算作1。就相当于用该数除以65536。Int32 除以 65536的结果不会超过65536种情况,因此开一个长度为65536的数组计数就可以。每读取一个数,数组中对应的计数+1,考虑有负数的情况,需要将结果加32768后,记录在相应的数组内。
第一遍统计之后,遍历数组,逐个累加统计,看中位数处于哪个区间,比如处于区间k,那么0- k-1的区间里数字的数量sum应该<n/2(2.5亿)。而k+1 - 65535的计数和也<n/2,第二遍统计同上面的方法类似,但这次只统计处于区间k的情况,也就是说(x / 65536) + 32768 = k。统计只统计低16位的情况。并且利用刚才统计的sum,比如sum = 2.49亿,那么现在就是要在低16位里面找100万个数(2.5亿-2.49亿)。这次计数之后,再统计一下,看中位数所处的区间,最后将高位和低位组合一下就是结果了。
对应上题5
7、数据库索引
适用范围:大数据量的增删改查
基本原理及要点:利用数据库的设计实现方法,对海量数据的增删改查进行处理。
8、倒排索引(inverted index)
适用范围:搜索引擎,关键字查询
基本原理及要点:为何叫倒排索引?一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
例子:
以英文为例,下面是要被索引的文本:
T0 = "it is what it is"
T1 = "what is it"
T2 = "it is a banana"
我们就能得到下面的反向文件索引:
"a": {2}
"banana": {2}
"is": {0, 1, 2}
"it": {0, 1, 2}
"what": {0, 1}
检索的条件"what", "is" 和 "it" 将对应集合的交集。
正向索引开发出来用来存储每个文档的单词的列表。正向索引的查询往往满足每个文档有序频繁的全文查询和每个单词在校验文档中的验证这样的查询。在正向索引中,文档占据了中心的位置,每个文档指向了一个它所包含的索引项的序列。也就是说文档指向了它包含的那些单词,而反向索引则是单词指向了包含它的文档,很容易看到这个反向的关系。
扩展:
问题实例:文档检索系统,查询那些文件包含了某单词,比如常见的学术论文的关键字搜索。
9、外排序
适用范围:大数据的排序,去重
基本原理及要点:外排序的归并方法,置换选择 败者树原理,最优归并树
对应上题3
10、分布式处理mapreduce
适用范围:数据量大,但是数据种类小可以放入内存
基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。
问题实例:
1).The canonical example application of MapReduce is a process to
count the appearances of
each different word in a set of documents:
void map(String name, String document):
// name: document name
// document: document contents
for each word w in document:
EmitIntermediate(w, 1);
void reduce(String word, Iterator partialCounts):
// key: a word
// values: a list of aggregated partial counts
int result = 0;
for each v in partialCounts:
result += ParseInt(v);
Emit(result);
Here, each document is split in words, and each word is counted
initially with a "1" value by
the Map function, using the word as the result key. The framework
puts together all the pairs
with the same key and feeds them to the same call to Reduce, thus
this function just needs to
sum all of its input values to find the total appearances of that word.
对应上题6
example1:
上千万or亿数据(有重复),统计其中出现次数最多的前N个数据,分两种情况:
1、可一次读入内存
2、不可一次读入。
可用思路:trie树+堆,数据库索引,划分子集分别统计,hash,分布式计算,近似统计,外排序
答:
1、可一次读入内存
所谓的是否能一次读入内存,实际上应该指去除重复后的数据量。如果去重后数据可以放入内存,我们可以为数据建立字典,比如通过 map,hashmap,trie,然后直接进行统计即可。当然在更新每条数据的出现次数的时候,我们可以利用一个堆来维护出现次数最多的前N个数据,当然这样导致维护次数增加,不如完全统计后在求前N大效率高。
2、不可一次读入
如果数据无法放入内存。一方面我们可以考虑上面的字典方法能否被改进以适应这种情形,可以做的改变就是将字典存放到硬盘上,而不是内存,这可以参考数据库的存储方法。
当然还有更好的方法,就是可以采用分布式计算,基本上就是map-reduce过程,首先可以根据数据值或者把数据hash(md5)后的值,将数据按照范围划分到不同的机子,最好可以让数据划分后可以一次读入内存,这样不同的机子负责处理
各种的数值范围,实际上就是map。得到结果后,各个机子只需拿出各自的出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是reduce过程。
实际上可能想直接将数据均分到不同的机子上进行处理,这样是无法得到正确的解的。因为一个数据可能被均分到不同的机子上,而另一个则可能完全聚集到一个机子上,同时还可能存在具有相同数目的数据。比如我们要找出现次数最多的
前 100 个,我们将 1000 万的数据分布到 10 台机器上,找到每台出现次数最多的前 100 个,归并之后这样不能保证找到真正的第 100 个,因为比如出现次数最多的第 100 个可能有 1 万个,但是它被分到了 10 台机子,这样在每台上
只有 1 千个,假设这些机子排名在 1000 个之前的那些都是单独分布在一台机子上的,比如有 1001 个,这样本来具有 1 万个的这个就会被淘汰,即使我们让每台机子选出出现次数最多的 1000 个再归并,仍然会出错,因为可能存在大
量个数为 1001 个的发生聚集。因此不能将数据随便均分到不同机子上,而是要根据hash 后的值将它们映射到不同的机子上处理,让不同的机器处理一个数值范围。
而外排序的方法会消耗大量的IO,效率不会很高。而上面的分布式方法,也可以用于单机版本,也就是将总的数据根据值的范围,划分成多个不同的子文件,
然后逐个处理。处理完毕之后再对这些单词的及其出现频率进行一个归并。实际上就可以利用一个外排序的归并过程。
另外还可以考虑近似计算,也就是我们可以通过结合自然语言属性,只将那些真正实际中出现最多的那些词作为一个字典,使得这个规模可以放入内存。
对example1做部分总结:
实际来说并不需要数据可以一次放入内存。
基本上就是 map-reduce 过程,首先可以根据数据值或者把数据 hash(md5)后的值,将数据按照范围划分到不同的机子,最好可以让数据划分后可以一次读入内存,这样不同的机子负责处理各种的数值范围,实际上就是 map。得到结果后,各个机子只需拿出各自的出现次数最多的前 N 个数据,然后汇总,选出所有的数据中出现次数最多的前 N 个数据,这实际上就是 reduce 过程。
此段描述实际上是 map reduce 模型的一个小的实现,在当利用实际的MapReduce 实现进行计算时,实际上只是一个 wordcount。划分归并排序都是由系统处理掉的。
概念性总结
1、完美hash函数
所谓完美哈希函数,就是指没有冲突的哈希函数,设定义域为X,值域为Y, n=|X|,m=|Y|,那么肯定有m>=n,如果对于不同的key1,key2 属于X,有h(key1)!=h(key2),那么称h为完美哈希函数,当m=n时,h称为最小完美哈希函数(这个时候就是一一映射了)。
相信大家在处理大规模字符串数据的时候,经常遇到这样的需求:首先需要把数据中出现的每个不同字符串分配一个唯一的整数ID,以后就用这个整数来代替这个字符串了。这个时候只要找到一个字符串的完美哈希函数,就可以解决了。
算法:假设有 2 个随机的哈希函数h1 和h2,都将字符串映射到 0..m-1 域内,假设现在有一个g函数,使得(g(h1(str_i))+g(h2(str_i))) % n = i,那么这个哈希函数就可以作为最小完美哈希函数了。由于h1 和h2 已知,现在的目标就是
要找到g函数,建立一个m个顶点的图,然后添加n条边,第i条边为(h1(str_i),h2(str_i)),边权为i,可以证明:只要这个图是一个无环图,就一定存在满足条件的g函数:每次找一个没有分配g值的顶点v,令g[v]=0,然后从这个顶点开始深度优先遍历,给其它每个点分配相应的g值。
问题:算法最关键的问题是m值的选取,这个涉及到 2 方面的取舍:
1.m值不能太大,否则g函数定义域太大,内存存不下
2.m值不能太小,否则生成的图有环的概率会非常大。
解决方法:设置更多的随机函数,比如h1,h2,h3,这个时候哈希函数就是(g(h1(str_i))+g(h2(str_i)+g(h3(str_i))) % n = i,可以证明,此时m值不需要很大,就能使生成的图无环的概率很大。
2、排序二叉树
排序二叉树是一个动态的数据结构,一般说的排序二叉树的用途就是动态的快速查找某一个数。但是如果我们在二叉树的结点上增加更多的信息,就能发挥更nb的作用了。
实例:有一个在线论坛,发帖量和回复量都非常大,帖子按照新发表或者新回复的时间来排序,要你设计一个算法,来快速的选出第n页的所有帖子(假设每页显示 20 个帖子)。
解决方法:这个就是要动态的查询一堆数里面第x大到第y大之间的所有数了,可以增加,删除,修改那些数。如果在普通的二叉排序树的结点上增加一个域,表示它的左子树中的结点数,那么就可以很好的解决这个问题了。
code:
void search(tree t, int x, int y) {
if(x > y) return;
if(y <= t.left_num) {
search(t.left_child, x, y);
return;
}
if(x > t.left_num + 1) {
search(t.right_child, x - t.left_num - 1, y - t.left_num - 1);
return;
}
search(t.left_child, x, t.left_num);
选取t;
search(t.right_child, 1, y - t.left_num - 1);
}
注意:上面说的二叉树在面对大规模数据时,是指的平衡二叉树。
3、树状数组
假设有n个元素的数组a,每次可以在某个元素上执行加上一个数或者减去一个数的操作,然后需要能够快速的求出a[0]+a[1]+a[2]+...a[i]。这个可以用树状数组解决,每次更新或者询问的时间复杂度都是O(logn).
应用实例:qq拼音输入法引入了等级制度,用户会不定期的发送一个积分w到服务器,然后服务器把这个w累加到用户的总积分,并快速的返回这个用户的总积分在全球的排名。
解决方法:
1. 由于这个问题实质还是动态的求某个数的排名(也就是求集合中比这个数小的数有多少个),可以利用上面平衡二叉树或者SBT树来解决,但是由于用户众多,树太大,只能保存在磁盘上。
2.注意到这个问题有个显著的特点:用户量很大,但是用户的积分值不可能很大。假设用户的积分值最大为 10^6,那么开一个 10^6 的数组a,a[i]表示积分为i的用户有多少个,那么当需要给某个用户增加积分时,假设这个用户原始积分为o,那么首先使a[o]=a[o]-1,然后a[o+w]=a[o+w]+1,询问积分为i的用户的排名,实质就是求a[0]+a[1]+a[2]+...a[w-1],这个用上面说到的树状数组就可以了。时间空间效率都非常好。
4、索引
数据库里面的聚族索引和非聚族索引
这方面的问题挺重要的,但是了解的人不是很多。一般来说,聚族索引就是数据的存放顺序和聚族索引的顺序是一致的(有时数据会直接存放到聚族索引那个磁盘页中),而非聚族索引则不然,在非聚族索引中,需要存放一个磁盘地址指
向真实的数据块,而且连续的非聚族索引会对应着不连续的数据块。由于数据只可能有一种存放顺序,所以一个数据表中只能有一个聚族索引,但是可以有多个非聚族索引。
查询效率区别:由于上面说到的区别,这 2 个索引在应对不同类别的查询时,效率是不同的。一般来说,聚族索引可以很高效的应对各种查询,但是非聚族索引基本只能高效的应对结果集是少量的查询,比如select * from A where id=1。对于范围类查询,比如select * from A where id>1 and id<1000,
如果id字段是非聚族索引,那么效率远远没有聚族索引高,因为数据库每找到一个索引页后,还需要单独的一次io去取数据块,而且由于非聚族索引的特点,这些数据块是不连续的,导致磁头会不停的寻道,浪费很多io时间。
链表:
1、节点的定义如下:
typedef struct list {
int key;
struct list *next;
}list;
(1)已知链表的头结点head,写一个函数把这个链表逆序 ( Intel)
list * reverse(list * head){
list * h = head;
list * new_head = NULL,*temp;
if(h==NULL) return h;//如果是空链表
do{
temp = h;
h = h -> next;
temp -> next = new_head;
new_head = temp;
}while(h != NULL && h != head)//检测是循环链表
return new_head;
}
其实还要注意一点,链表内部是否包含小环。
2、已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序。(保留所有结点,即便大小相同)
list * merge (list *list1_head, list *list2_head){
}
其实需要问一下,head1 head2 是否都是从大到小,这点一定要明确,不能默认两个是相同的规格排序。
3、已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。 (Autodesk)
list * merge (list *list1_head, list *list2_head){
list * res;
if(list1_head == NULL) return list2_head;
if(list2_head == NULL) return list1_head;
if(list1_head->key > list2_head->key){
res = list1_head;
res->next = merge(list1_head->next,list2_head);
}else{
res = list2_head;
res->next = merge(list1_head,list2_head->next);
}
return res;
}
4、写一个程序,计算链表的长度
unsigned int list_len(list *head){
unsigned int len = 0;
list * h = head;
if(h == NULL)return 0;
d0{
len++;
h = h->next;
}while(h != NULL && h != head)
return len;
}
5、有一个链表L,其每个节点有 2 个指针,一个指针next指向链表的下个节点,另一个random随机指向链表中的任一个节点,可能是自己或者为空,写一个程序,要求复制这个链表的结构并分析其复杂性。
这个题目的思路很巧妙。当然简单的方法,可以利用一个map或者hash,将链表的 random指针的指向,保存起来。这样有O(n)存储空间的浪费,时间基本可以接近O(n).
实际上可以这样来做:我们将新的链表节点,插入到原来链表节点当中,并且修改原来的链表的random指针,使得该指针由我们现在的新节点所有。实际上形成下面这样一种结构,同时将原来o的 random指针的值,复制给它后面的现在的@的random指针,该结构如下:
o->@->o->@->o->@->NULL
现在可以利用@拥有的random指针方便的找到它真正的random指针。因为原来 的 @ 的 random 指 针 指 向 o 的 random 指 针 , 只 要 把 让 @->random=@->random->next,random就是真正的那个指针了,然后我们再把@从这个链表中删除。
6、小1:给你一个单向链表的头指针,可能最后不是NULL终止,而是循环链表。题目问你怎么找出这个链表循环部分的第一个节点。
小2:如何检查一个单向链表上是否有环?
方法1:这个原来的判断链表环的扩展,需要求出环的开始点。只要稍微对原来的方法进行扩展,也就可以解决这个问题。使用两个指针一个步长为 1,另一个步长为 2,如果第二个指针可以追上第一个指针,则说明有环。这样我们首先可以求出环的
长度L,然后在设两个步长为 1 的指针,让他们间距为L,这样第一个相等的地方,就是环的起点。
方法2:还有一个更好的方法,就是在步长为 1,步长为 2 指针相遇后,再从头开始一个步长为 1 的指针,然后开始步长为 1 的指针继续行进,当这两个指针相遇的地方就是环的起点。
7、找出单向链表中中间结点
这个可以借鉴上面的方法,利用步长为 1,步长为 2 指针,当步长为 2 的指针到达末尾时,指针 1 就刚好达到中间。
8、给定链表的头指针和一个结点指针,在 O(1)时间删除该结点。链表
结点的定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
函数的声明如下:
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
实际上我们可以将该节点的下一个节点的值copy到这个节点,然后删除下一个节点。这样实际上就将一个不能处理的情况,通过一种技巧转化成了我们可以处理的正常情况。
但是还需要注意下面这个边界情况:如果要删除的是尾指针?这就意味着当前节点没有下一个节点。
9、 查找链表中倒数第k个结点
这个可以参考(7),我们可以设置步长相同,间距为k的指针,其特点在当内存无法存放所有元素,需要从硬盘读取时,性能卓越,k在可以接受的情况下可以一次读取到内存,大大提高了效率。
但是仍需要注意考虑边界情况: (细节,如,假如链表的长度不足m,它就不存在m个元素,因此必须在两个指针的出发阶段检查最先出发的当前指针是否已经到了链表尾。)
10、两个单向链表,开头结点不一样, 在中间某处开始, 结点一样了,找出它们的第一个公共结点。
这个问题,可以通过判断最后一个节点是否相同,判断是否相交。如果相交,可以统计两个链表的长度,设一个为a,一个为b,然后这样再搞两个指针,一个先走|a-b|步,另一个再走,当它们相等时就是第一个公共结点。