近期面试题整理
1. 有25匹马,速度都不同,但每匹马的速度都是定值。现在只有5条赛道,无法计时,即每赛一场最多只能知道5匹马的相对快慢。问最少赛几场可以找出25匹马中速度最快的前3名?(人人)
网上解答:每匹马都至少要有一次参赛的机会,所以25匹马分成5组,一开始的这5场比赛是免不了的。接下来要找冠军也很容易,每一组的冠军在一起赛一场就行了 (第6场)。最后就是要找第2和第3名。我们按照第6场比赛中得到的名次依次把它们在前5场比赛中所在的组命名为A、B、C、D、E。即:A组的冠军是第 6场的第1名,B组的冠军是第6场的第2名……每一组的5匹马按照他们已经赛出的成绩从快到慢编号:
A组:1,2,3,4,5
B组:1,2,3,4,5
C组:1,2,3,4,5
D组:1,2,3,4,5
E组:1,2,3,4,5
从现在所得到的信息,我们可以知道哪些马已经被排除在3名以外。只要已经能确定有3匹或3匹以上的马比这匹马快,那么它就已经被淘汰了。可以看到, 只有上表红色的那5匹马是有可能为2、3名的。即:A组的2、3名;B组的1、2名,C组的第1名。取这5匹马进行第7场比赛,第7场比赛的前两名就是 25匹马中的2、3名。故一共最少要赛7场。
分析:上面的矩阵现在每行已经排序,第一列也已经排序,共用了6次,并且已经确定第一名是M[1][1]。由于M[1][1]> M[2][1]且M[1][1]> M[1][2],所以第二名肯定是M[2][1]和M[1][2]中的一个。依次类推,我们可以得到M[2][1]>M[3][1]且M[2][1]> M[2][2],M[1][2]> M[2][2]且M[1][2]> M[1][3],也即第三名只可能在M[2][1]和M[1][2]的较小值以及M[3][1]、M[2][2]、M[1][3]中选择。所以第七场我们将第二和三条斜对角线中的五个元素进行比较即可得到第二三名。
2. N*M的棋盘,现在在(1,1)位置,每次只能向左或右移动,求移动到(N,M)的次数。(人人)
解:该问题可以通过递归或者是动态规划解决,复杂度为O(n^2)。不过,该问题实际上就是组合数的计算,我们需要计算C(n+m-2,n)。常规的方法是利用组合数公式:
C(n,r)=n×(n-1)×(n-2)×...×2×1/[r×(r-1)×(r-2)×...×2×1]/[(n-r)×(n-r-1)×(n-r-2)×...×2×1]去求,但是这样存在求阶乘的操作,很容易溢出。事实上,我们可以直接采用公式
去计算组合数公式。只要组合数不溢出,上述公式就不会溢出,而且结果会准确。具体的做法是,将每一个除法转换成double,然后最后得到的结果加0. 5转成int即可。此时复杂度只有O(n)。
此外,还有一个类似的问题:棋盘大小为N*N,如果在走的过程中,不允许穿越对角线,则共有多少种走法?该问题的答案是卡特兰数,也即h(n)=C(2n,n)/(n+1) (n=0,1,2,...),和上面不同是组合数还除以了n+1,因为我们对可能的路径进行了限制。此外h(n)=c(2n,n)-c(2n,n+1)(n=0,1,2,...)。
3. 解决哲学家进餐问题。(人人)
解:首先明白信号量的含义,整型信号量被定义为一个用于表示资源数目的整型量S,还有两个原子操作P(wait)和V(signal)。整型信号量有个缺陷就是存在“忙等”,不符合“让权等待”原则。记录型信号量是当某个进程不能进入临界区时,便立即释放处理机,把处理机让个其他进程。
解决哲学家进餐问题有三种方法:
1) 至多允许n-1位哲学家进餐,这样最终能保证至少有一位哲学家能够进餐,并且在用毕时释放他用过的两支筷子,从而使更多的哲学家能够进餐。此时需要一个新的记录型信号量num,表示最多允许进餐的哲学家数量。代码如下:
semaphore num=n-1; semaphore chopstick[n]={1,1,1,…,1}; void philosopher(int x){ while(1){ think(); P(num); P(chopstick[x]); P(chopstick[(x+1)%n]); eat(); V(chopstick[x]); V(chopstick[(x+1)%n); V(num); } }
2) 仅当哲学家的左右两支筷子均可用时,才允许他拿起筷子进餐;
3) 规定每个哲学家都先拿起自己两旁为偶数的筷子,然后再拿为奇数的筷子,最后总会有一位哲学家能获得两支筷子而进餐。
4. 内联函数在什么阶段被替换?(腾讯)
解:宏定义是预处理器在代码优化时直接替换的,而内联函数是在编译期间插入代码.宏定义替换比内联函数执行得早.宏定义没有类型检查,所以出错率较高.内联函数是经过类型检查的. 程序在编译时,将内联函数的代码自动嵌入到主程序的代码中节省了程序执行时间。
5. 各级存储器速度对比。(腾讯)
execute typical instruction 执行基本指令 |
1/1,000,000,000 sec = 1 nanosec |
fetch from L1 cache memory 从一级缓存中读取数据 |
0.5 nanosec |
branch misprediction 分支误预测 |
5 nanosec |
fetch from L2 cache memory 从二级缓存获取数据 |
7 nanosec |
Mutex lock/unlock 互斥加锁/解锁 |
25 nanosec |
fetch from main memory 从主内存获取数据 |
100 nanosec |
send 2K bytes over 1Gbps network 通过1G bps 的网络发送2K字节 |
20,000 nanosec |
read 1MB sequentially from memory 从内存中顺序读取1MB数据 |
250,000 nanosec |
fetch from new disk location (seek) 从新的磁盘位置获取数据(随机读取) |
8,000,000 nanosec
8 ms |
read 1MB sequentially from disk 从磁盘中顺序读取1MB数据 |
20,000,000 nanosec 20 ms |
send packet US to Europe and back 从美国发送一个报文包到欧洲再返回 |
150 milliseconds = 150,000,000 nanosec |
6. 可重入函数。(腾讯)
解:主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
保证函数的可重入性的方法:在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量),对于要使用的全局变量要加以保护(如采取关中断、信号量等方法),这样构成的函数就一定是一个可重入的函数。
满足下列条件的函数多数是不可重入的:
1) 函数体内使用了静态的数据结构;
2) 函数体内调用了malloc()或者free()函数;
3) 函数体内调用了标准I/O函数。
Linux下的常用命令都是可重入函数,time和read都是。不过read会修改errno。localtime不是可重入函数。
7. 某酒店采用竞标式入住,每一个竞标是一个三元组(开始,入住时间,每天费用)。现在有N个竞标,选择使酒店效益最大的竞标。(美团)
解:该问题是一个带权的区间调度问题,请参考区间调度问题详解中带权区间调度部分。
8. 现在有100万人摇号,每月有2万人可以摇到号。摇号的人数每月增加2万,小明从第一个月开始摇号,到摇到号的耗时期望是多少?(美团)
解:小明第一个月摇到,则等待0个月,概率为:小明第二个月摇到则等待1个月,概率为:,…,小明第i个月摇到号则等待i-1个月,概率为:所以小明摇到号的期望为:
设幂级数
则
所以
所以期望E等于
9. 常用排序复杂度以及稳定性?
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生改变,则称这种排序方法是不稳定的。(注:稳定性和算法的复杂度无关)
排序法 |
平均时间 |
最差情形 |
稳定度 |
额外空间 |
备注 |
冒泡 |
O(n2) |
O(n2) |
稳定 |
O(1) |
n小时较好 |
选择 |
O(n2) |
O(n2) |
不稳定 |
O(1) |
n小时较好 |
插入 |
O(n2) |
O(n2) |
稳定 |
O(1) |
大部分已排序时较好 |
基数 |
O(logRB) |
O(logRB) |
稳定 |
O(n) |
B是真数(0-9),R是基数(个十百) |
Shell |
O(nlogn) |
O(ns) 1<s<2 |
不稳定 |
O(1) |
s是所选分组 |
快速 |
O(nlogn) |
O(n2) |
不稳定 |
O(nlogn) |
n大时较好 |
归并 |
O(nlogn) |
O(nlogn) |
稳定 |
O(1) |
n大时较好 |
堆 |
O(nlogn) |
O(nlogn) |
不稳定 |
O(1) |
n大时较好 |
10. gcd复杂度。(网易互联网)
解:假设a>b,这时gcd(a,b)gcd(b,a%b)gcd(a%b,b%(a%b))……。当b>a/2时,有a%b=a-b<a/2;当b<a/2时有a%b<b<a/2,于是经过两次递归后,第一个参数要小于原来的一半,所以其复杂度在O(log max(a,b))以内。
11. TCP计时器:
a)重传计时器:为了重传丢失的报文,TCP应用重传计时器类处理重传超时(RTO)。这里需要注意的是RTT(往返时间)和RTD(往返时间的偏差)的计算是利用了加权平均。重传超时被设置为RTT+4*RDD。如果重传,RTO指数增加。
b)持续计时器:TCP为每个连接使用一个持续计时器。当发送TCP接收到窗口值为零的确认时,就启动一个持续计时器。当持续计时器超时后,发送TCP就发送一个特殊的报文段,成为探测报文段。
c)保活计时器:利用保活计时器来防止两个TCP之间的连接长时间空闲。
d) TIME WAIT计时器:当客户端要关闭TCP连接时,首先向服务器发送FIN报文,并进入FIN-WAIT-1状态,当收到服务器的ACK报文之后进入FIN-WAIT-2状态。当收到服务器端发送的FIN报文后,向服务器发送ACK报文,并进入TIME-WAIT状态。此时,该ACK报文可能丢失,而服务器不收到ACK报文不会关闭连接。设置TIME WAIT计时器可以使客户等待足够长的时间,使得在ACK报文丢失的情况下,可以等到下一个FIN的的到来。该计时器的值为最大报文段寿命的两倍。也即客户端断开TCP是要靠TIME WAIT计时器的超时。
12. RAID(廉价磁盘冗余阵列)(网易互联网)
解: 并行交叉存取:在该系统中,有多台磁盘驱动器,系统将每一盘块的数据分为若干个子盘块数据,再分别存储到各个不同磁盘中的相同位置。
Raid 0:本级只提供并行交叉存取,能实现高速IO访问,但是可靠性不高。
Raid 1:具有磁盘镜像功能,但是磁盘利用率只有50%。
Raid 3:具有并行传输功能的磁盘阵列,n个盘块中有一个奇偶校验盘,利用率为(n-1)/n。
Raid 5:具有独立传送功能的磁盘阵列,校验信息以螺旋方式散布在每个盘块中。
Raid 6:强化的Raid 3.
Raid 7:改进的Raid 6.
13. 生产者消费者模型。(百度)
解:在生产者消费者模型中,生产者和消费者通过一个共享的缓冲区进行通信,生产者向缓冲区写入资源,消费者从缓冲区消费资源。生产者等待缓冲区有空余位置,消费者等待缓冲区有剩余资源,每一个特定时刻只能有一个生产者或消费者对缓冲区进行操作,所以总共需要三个信号量。
semaphore mutex= 1; semaphore empty=n; semaphore full=0; void put(){ P(empty); P(mutex); //put one resource V(mutex); V(full); } void get(){ P(full); P(mutex); //get one resource V(mutex); V(empty); }
注意上述操作信号量的语句不能颠倒,否则就会导致死锁。
14. 位字段的赋值。
解:当多个位字段构成一个结构体时,如果赋值超过了某个位字段的范围,只会对赋的值求模,但是不会影响其他的位字段。例如,某位字段为4位,相邻的位字段为5位,如果对第一个位字段赋值17,则值为1,但是第二个位字段还是0.
吐槽一下,上面这些公司都挂了……