Preface:感谢您对博文的关注!2018年秋季校招已经开始,有需要内推腾讯的可以QQ(1589276509)联系我哈,期待你的加入。
1.前言
2016.4.11日广州参加了腾讯的CC++后台技术一面,安全技术类的面试。面试官人很温和,经历了大概70分钟的问答,特将遇到的面试问题汇总如下,自己总结学习,亦供网友参考。
2.问题汇总
问题一:
不好意思,我有事,先处理一下,你先写个非递归二分查找。
答:
之前遇到过这个问题,有所了解。感觉很多面试的第一个问题都是先写段代码。因此,手写代码感觉很重要,因为这是给面试官的第一印象。除了二分查找,快排,链表反转,实现atoi()函数等等,在面试中也常被用来作为手写代码的考题。
//数组递增有序
int binarySearch(int* array,int len,int value){
int mid=0;
int low=0;
int high=len-1;
while(low<=high){
mid=(low+high)/2;
if(array[mid]==value) //找到
return mid;
if(value>array[mid]) //在右半区
low=mid+1;
else //在左半区
high=mid-1;
}
return -1; //查找失败
}
问题二:
(面试官一直在打电话)如果你写完了,你再写一个从数组中找出第二大的数。
答:
找第几大的数突然间想到了堆排序,因为自己之前练习过堆排序,所以就花了十分钟左右的时间写下了堆排序。写堆排序需要知道节点下标index的左子节点的下标为2*index+1,2*index+2。以数组构造堆的话是一个完全二叉树,数组长度为len,那么最后一个非叶子节点的下标是len/2-1。以堆排序来寻找第二大数参考代码如下:
//手写大顶堆排序求大二大数
void adjust(int A[],int index,int len){
if(index>len/2-1)//叶子节点,不同调整
return;
int biggestIndex=0;
if(2*index+2<len)
biggestIndex=A[2*index+1]>A[2*index+2]?2*index+1:2*index+2;
else
biggestIndex=2*index+1;
if(A[index]<A[biggestIndex]){
A[index]=A[index]+A[biggestIndex];
A[biggestIndex]=A[index]-A[biggestIndex];
A[index]=A[index]-A[biggestIndex];
adjust(A,biggestIndex,len);
}
}
void createMaxHeap(int A[],int len){
for(int i=len/2-1;i>=0;--i){
adjust(A,i,len);
}
}
int getSecondMax(int A[],int len){
createMaxHeap(A,len); //建堆
for(int i=0;i<2;++i){
A[0]=A[0]+A[len-1-i];
A[len-1-i]=A[0]-A[len-1-i];
A[0]=A[0]-A[len-1-i];
adjust(A,0,len-1-i);
}
return A[len-2];
}
很显然,这个求解方法的时间复杂度是nlogn,不是较优解法,不是面试官想要的答案。面试官追问有没有更好的方法,时间复杂度是O(n)。
稍微想了一下,回答冒泡排序和简单选择排序可以在O(2n)的时间复杂度找到第二大的数。他试官说还有没有更快的方法呢?不要O(2n),只要O(n)。
正确答案是:
保存最大值和第二大值,扫描一遍数组即可找到,也就是以空间换时间。冒泡排序和简单选择排序都需要扫描两遍,不太符合面试官的要求。下面给出正确的实现,参考如下代码:
int getSecondMax(int array[],int len){
if(len<=1)
return -1;
int max=0,secondMax=0;
if(array[0]>array[1]){
max=array[0];
secondMax=array[1];
}else{
max=array[1];
secondMax=array[0];
}
for(int i=2;i<len;++i){
if(array[i]<=secondMax)
continue;
if(array[i]<=max&&array[i]>secondMax) //新的第二大数
secondMax=array[i];
else{
secondMax=max; //最大数退化为第二大数
max=array[i]; //最大数
}
}
return secondMax;
}
这里要感谢网友那年我们二十五提出的问题,已经修复参考代码的bug,感谢您细心的阅读和慷慨的指教!
问题三:
C++中struct和class的区别?
答:
(1)关于继承和访问权限,struct默认继承和访问权限均为public,class均为private;
(2)关于模版,在模版中,类型参数前面可以使用class或typename,不能使用struct。
问题四:
说说C++多态的实现机制。
答:
简单来说,就是通过虚函数表来实现的,具体请参考:CVTE面试问题汇总.
问题五:
C中static关键字的作用。
答:
(1)修饰变量,一是表名变量存储空间在全局静态存储区,二是变量生命周期是真个程序的生命周期,三是变量作用域限定在当前文件。
(2)修饰函数,限制函数的作用域为当前文件。
问题五:
C中extern关键字的作用。
答:
extern修饰变量和函数起到声明的作用,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他文件中寻找其定义。
问题六:
C++中extern "C"
的作用。
答:
C++中extern "C"
修饰函数时,指明该函数以C的方式进行编译和链接。具体来说就是C++函数支持函数重载,C不支持函数重载,原因二者的函数签名不同。
如函数void foo(int,int)被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为”mangledname”)。
问题七:
说一下CC++程序的内存布局。
答:
目前我还没有找到很权威的著作对此问题有详细的论述,肯定有,只是我还不知道。看了《C++高级进阶教程》中描述如下。
如果内存地址由下到上的是从低地址到高地址,那么程序的内存布局大致如下:
问题七:
僵尸进程是如何产生的。
答:
在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程。
问题八:
僵尸进程如何避免。
答:
(1)父进程通过wait和waitpid等函数等待子进程结束,但这会导致父进程挂起。
(2)如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
(3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
(4)还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收 还要自己做。
问题九:
写一个宏,给定数组名求数组长度,数组类型未知。
答:
想到sizeof就可以很容易求出来了。思路是先用sizeof(array)求出数组占用的内存空间大小(单位字节),再通过sizeof求出数组单个元素的类型大小(单位字节),前者除以后者即可。
#define func(array) sizeof(array)/sizeof(array[0])
问题十:
Linux下awk和sed了解过吧,给定一个文本文件,编写shell脚本将文件中重复的行删除。
答:
使用sort+uniq/awk/sed可以来完成。
方法一:利用sort以不重复的方式打印出文件所有的行并排序-u,表示unique。
sort -u file
方法二:利用sort先对文件按行排好序之后再交由uniq处理。sort -k 指定列,-t指定列分隔符。
sort -k 1 -t ':' file|uniq
方法三:利用sort+awk来完成。
sort file | awk '{if ($0!=line) print;line=$0}'
if ($0!=line) print;
表示当前行是否等于上一行,不等于的话则打印,line开始是空的。line=$0
表示当前行赋给line。
方法四:利用sort+sed来完成。
sort file | sed '$!N; /^\(.*\)\n\1$/!P;D'
sort file将文件排序,排好序之后,重复的行会相邻。sed的单引号内的编辑命令中,各条命令以分号隔开。
第一条语句:$!N;
表示sed当前处理的行不是文件的最后一行时,读取下一行至当前处理的行的后面,一并存储在sed的Pattern Space(模式空间)中。$表示最后一行,!N
表示不读取下一行。
第二条语句:/^\(.*\)\n\1$/!P;
,斜杠//之间表示对行的匹配模式。匹配模式的描述是sed的对正则表达式的扩充。^\(.*\)
表示开头起任意字符,\n
表示换行符,\1
表示对前面第一个小括号内的字符重复,$
表示行末。所以/^\(.*\)\n\1$/
整个意思是匹配换行符前后相同的两行。如this\nthis
,这两行内存在sed模式空间内。!P
表示匹配成功的话,就不打印当前行,sed是默认打印当前处理后的行。
第三条语句:D
命令是删除当前模式空间开端至\n的内容(不在传至标准输出),放弃之后的命令,但是对剩余模式空间重新执行sed。这样就保证了sed的模式空间中除了最后一行时,不能读取下一行,sed的模式空间中始终有两行数据。
关于sed的用法,可以参考下面三篇文章。
(1)sed命令n,N,d,D,p,P,h,H,g,G,x解析;
(2)Sed手册;
(3)Sed与AWK入门教程之Sed篇。这篇比较基础,从总体语法结构入手,值得研读。
感觉sed不简单啊。
问题十一:
有用过Linux中的epoll吗?它的作用是什么?
答:
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
问题十二:
epoll和select的区别在哪,或者说优势在哪?
答:
(1)epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered);
(2)select的句柄数目受限,在linux/posix_types.h头文件有这样的声明:#define __FD_SETSIZE 1024
,表示select最多同时监听1024个fd。而epoll没有,epoll的最大并发的连接数的理论值无上限,但由于实际内存资源有限,实际并发的连接数受到资源的限制和最大的打开文件句柄数目的限制;
(3)epoll的最大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构类似一个数组的数据结构,而epoll 是维护一个队列,直接看队列是不是空就可以了。
(4)使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
此外,epoll创建时传入的参数是什么?
epoll对象可通过int epoll_create(int size)
来创建一个epoll的实例,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值。所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。但是自从linux2.6.8之后,size参数是被忽略的。
此外,创建epoll实例,还可以通过int epoll_create1(int flags)
。
这个函数是在linux 2.6.27中加入的,其实它和epoll_create
差不多,不同的是epoll_create1
函数的参数是flags,当flag是0时,表示和epoll_create函数完全一样,不需要size的提示了。
当flag = EPOLL_CLOEXEC
,创建的epfd会设置FD_CLOEXEC
;
当flag = EPOLL_NONBLOCK
,创建的epfd会设置为非阻塞。
一般用法都是使用EPOLL_CLOEXEC
。关于EPOLL_CLOEXEC
,网上资料说明是对epfd的一个标识说明,用来设置文件close-on-exec状态的。当close-on-exec状态为0时,调用exec时,fd不会被关闭;状态非零时则会被关闭,这样做可以防止fd泄露给执行exec后的进程。关于exec的用法,大家可以去自己查阅下,或者直接man exec。
问题十三:
给定数据表table1如下,编写SQL语句找出出现次数前三的年龄。
答:
在MS Access中测试跑通,参考语句如下:
select top 3 table2.Age,table2.num from (select Age,count(Age) as num from table1 group by Age) as table2 Order By table2.num DESC
上面的SQL语句在MS Access中验证通过。其可分为两部分。
第一部分是select Age,count(Age) as num from table1 group by Age
。这一句是利用group by函数将选择出来的以Age字段进行分组,然后统计出相同年龄出现的次数并命名为新的字段num。
第二部分是将第一部分选择出来的结果集作为新表,再次从中选择出以table2.num字段降序排序后去前三行记录。
问题十四:
网络的五层协议模型。
答:
很简单,如下图所示:
问题十五:
咱们聊一下架构的事情。如果你现在是一位架构师,如何实现QQ的大量用户并发登陆,应该考虑哪些问题?又该如何解决?
答:
(1)传输协议选择
对于用户的登陆,其口令验证过程需要保证用户相关信息如用户的口令(已经)顺利传输到服务端,这就需要保证数据传输的得到可靠性。
TCP是面向连接的协议,也就是说协议本身向对方发送数据前先确信对方准备好,对方收到后要回送确认。
UDP是无连接协议,就是说协议本身不管对方是否准备好,直接向对方发送,不能确保对方收到。
所以,在用户的登陆验证的过程中,采用TCP协议传输。
好友之间发送消息,主要采用UDP协议,内网传文件采用了P2P技术。总来的说:
(a)登陆过程,客户端client 采用TCP协议向服务器server发送信息,HTTP协议下载信息。登陆之后,会有一个TCP连接来保持在线状态;
(b)和好友发消息,客户端client采用UDP协议,但是需要通过服务器转发。腾讯为了确保传输消息的可靠,采用上层协议来保证可靠传输。如果消息发送失败,客户端会提示消息发送失败,并可重新发送;
(c)如果是在内网里面的两个客户端传文件,QQ采用的是P2P技术,最好使用TCP协议,不需要服务器中转。
(2)负载均衡
以下均是个人想法。为了应对QQ验证登陆的并发量,短时间内响应对服务器造成的压力,我们可以采用多态服务器,将同一类QQ提交到不同服务器进行验证,比如以QQ号的好两位数字,可分为100中不同类型的QQ号,这样就可实现服务器的负载均衡。
(3)线程和进程的选择
服务端程序,为了响应每一个QQ用户的登陆验证,应该采取线程进行服务。因为线程在数据共享、同步
内存占用,切换,CPU利用率等方面占有优势,而进程间通信复杂,占用资源较大。
如何优化多线程并发服务呢?
我们没有必要为每一位登陆验证的用户创建服务线程,可以采用线程池的方式来进行优化。
问题十六:
你了解过数据挖掘吗。对这方面感兴趣吗?
答:
没了解过但很感兴趣。
问题十七:
你还有什么问题要问的。
答:
请问您搞安全技术方面需要对数据库和SQL掌握很精通吗?
面试官:无需精通,但是要有良好的基础,常用的SQL要会写。
3.小结
面试官也是比较温和的人,很有耐心,整个过程节奏也很缓慢。不会的问题尽量去思考,面试官希望看到面试者的思考过程,不懂的我也向他请教,寻求提示,这也间接的产生了互动。
整个面试过程所遇到的问题,除了最后一个是比较开发的题目,前面都是比较基础的题目。之所以问这些问题,还是就个人简历中提到的求职技能和相关项目进行相关问题提问。在SQL和shell脚本方面,因为很多年没写SQL了,所以基本忘记,shell脚本,尽管平时在Linux环境编程,但是很少写,写的话也是参考网上资源,一时无任何参考手写确实有些不适,看来SQL和shell这方面要加强了。
参考文献
[1]C++中struct与class的区别0.
[2]C++中struct和class的区别1.
[3]【Linux】排序命令sort.
[4]select 和 epoll 多路复用IO.
[5]浅析epoll – epoll函数深入讲解.
[6]QQ协议的选择.