程序员面试修炼 | 腾讯研发类面试题总结
Posted 魔据教育
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序员面试修炼 | 腾讯研发类面试题总结相关的知识,希望对你有一定的参考价值。
名词解释
1、BBIT:模块间接口测试,验证模块之间的接口能不能配合,有时和联调混在一起,其实目的并不相同。BBIT的目的,是根据系统设计对系统的分解,从已通过验证的模块开始,逐层向上集成,得到一个可运行的系统。而联调一般涉及软件、硬件或者不同产品间的配合测试。MST和BBIT可以归到“模块级” 的测试,一个验证模块,一个验证模块间的接口。
2、SDV:属于测试人员开展的系统测试,但是有点偏灰盒测试,关注内部实现,验证多个模块集成以后是否满足设计需求。
3、SIT:把系统当作一个黑盒来测试,不关心内部具体的实现,验证设计需求是否得以满足。
4、SVT:验收测试,其测试对象是产品包需求OR。产品包需求给出了产品的范围,从产品可能的应用环境的角度刻画系统,SVT的目的就是确认(或验收)产品包需求给出的各种应用场景产品均能满足。
面试真题
★★★ 腾讯研发类常见面试题总结 ★★★
一、KMP字符串匹配
KMP算法完成的任务是:给定两个字符串O和f,长度分别为n和m,判断f是否在O中出现,如果出现则返回出现的位置。常规方法是遍历a的每一个位置,然后从该位置开始和b进行匹配,但是这种方法的复杂度是O(nm)。KMP算法通过一个O(m)的预处理,使匹配的复杂度降为O(n+m)。
1. KMP算法思想
我们首先用一个图来描述KMP算法的思想。在字符串 O 中寻找 f,当匹配到位置 i 时两个字符串不相等,这时我们需要将字符串f向前移动。常规方法是每次向前移动一位,但是它没有考虑前 i-1 位已经比较过这个事实,所以效率不高。事实上,如果我们提前计算某些信息,就有可能一次前移多位。假设我们根据已经获得的信息知道可以前移 k 位,我们分析移位前后的 f 有什么特点。我们可以得到如下的结论:
(1)A段字符串是 f 的一个前缀。
(2)B段字符串是 f 的一个后缀。
(3)A段字符串和B段字符串相等。
所以前移 k 位之后,可以继续比较位置 i 的前提是 f 的前 i-1 个位置满足:长度为 i-k-1 的前缀A和后缀B相同。只有这样,我们才可以前移 k 位后从新的位置继续比较。
所以KMP算法的核心即是计算字符串 f 每一个位置之前的字符串的前缀和后缀公共部分的最大长度(不包括字符串本身,否则最大长度始终是字符串本身)。获得 f 每一个位置的最大公共长度之后,就可以利用该最大公共长度快速和字符串O比较。当每次比较到两个字符串的字符不同时,我们就可以根据最大公共长度将字符串f向前移动(已匹配长度-最大公共长度)位,接着继续比较下一个位置。事实上,字符串 f 的前移只是概念上的前移,只要我们在比较的时候从最大公共长度之后比较f和O即可达到字符串f前移的目的。
2. next数组计算
理解了KMP算法的基本原理,下一步就是要获得字符串 f 每一个位置的最大公共长度。这个最大公共长度在算法导论里面被记为next数组。在这里要注意一点,next数组表示的是长度,下标从1开始;但是在遍历原字符串时,下标还是从0开始。假设我们现在已经求得next[1]、next[2]、……next[i],分别表示长度为1到 i 的字符串的前缀和后缀最大公共长度,现在要求next[i+1]。由上图我们可以看到,如果位置i和位置next[i]处的两个字符相同(下标从零开始),则next[i+1]等于next[i]加1。如果两个位置的字符不相同,我们可以将长度为next[i]的字符串继续分割,获得其最大公共长度next[next[i]],然后再和位置 i 的字符比较。这是因为长度为next[i]前缀和后缀都可以分割成上部的构造,如果位置next[next[i]]和位置 i 的字符相同,则next[i+1]就等于next[next[i]]加1。如果不相等,就可以继续分割长度为next[next[i]]的字符串,直到字符串长度为0为止。由此我们可以写出求next数组的代码(JAVA):
public
int[] getNext(String b)
{
int
len=b.length();
int
j=
0;
intnext
[]=new
int[len+
1];
//next表示长度为
i
的字符串前缀和后缀的最长公共部分,从
1开始
next
[
0]=
next[
1]=
0;
for
(
inti=
1;i<len;i++)//i
表示字符串的下标,从
0开始
{
//j在每次循环开始都表示
next[i]
的值,同时也表示需要比较的下一个位置
while
(j>
0&&b.charAt(i)!=b.charAt(j))j=
next[j];
if
(b.charAt(i)==b.charAt(j))j++;
next
[i+
1]=j;
}
returnnext
;
}
上述代码需要注意的问题是,我们求取的next数组表示长度为1到m的字符串 f 前缀的最大公共长度,所以需要多分配一个空间。而在遍历字符串 f 的时候,还是从下标0开始(位置0和1的next值为0,所以放在循环外面),到m-1为止。代码的结构和上面的讲解一致,都是利用前面的next值去求下一个next值。
3. 字符串匹配
计算完成next数组之后,我们就可以利用next数组在字符串O中寻找字符串f的出现位置。匹配的代码和求next数组的代码非常相似,因为匹配的过程和求next数组的过程其实是一样的。假设现在字符串f的前 i 个位置都和从某个位置开始的字符串O匹配,现在比较第 i+1 个位置。如果第 i+1 个位置相同,接着比较第 i+2 个位置;如果第 i+1 个位置不同,则出现不匹配,我们依旧要将长度为 i 的字符串分割,获得其最大公共长度next[i],然后从next[i]继续比较两个字符串。这个过程和求next数组一致,所以可以匹配代码如下(JAVA):
public void search(Stringoriginal, String find, int next[])
{
int
j=
0;
for
(
inti =
0; i< original.length(); i++) {
while
(j >
0&& original.charAt(i) != find.charAt(j))
j = next[j];
if
(original.charAt(i) == find.charAt(j))
j++;
if
(j== find.length()) {
System.
out.println(
"findat position "+ (i - j));
System.
out.println(original.subSequence(i- j +
1, i +
1));
j = next[j];
}
}
}
上述代码需要注意的一点是,每次我们得到一个匹配之后都要对 j 重新赋值。
二、进程与线程
1. 进程
我们电脑的应用程序,都是进程,假设我们用的电脑是单核的,cpu同时只能执行一个进程。当程序处于I/O阻塞的时候,CPU如果和程序一起等待,那就太浪费了,cpu会去执行其他的程序,此时就涉及到切换,切换前要保存上一个程序运行的状态,才能恢复,所以就需要有个东西来记录这个东西,就可以引出进程的概念了。
进程就是一个程序在一个数据集上的一次动态执行过程。进程由程序,数据集,进程控制块三部分组成。程序用来描述进程哪些功能以及如何完成;数据集是程序执行过程中所使用的资源;进程控制块用来保存程序运行的状态
2. 线程
一个进程中可以开多个线程,为什么要有进程,而不做成线程呢?因为一个程序中,线程共享一套数据,如果都做成进程,每个进程独占一块内存,那这套数据就要复制好几份给每个程序,不合理,所以有了线程。
线程又叫轻量级进程,是一个基本的cpu执行单元,也是程序执行过程中的最小单元。一个进程最少也会有一个主线程,在主线程中通过threading模块,再开子线程。
3.进程线程的关系
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
(2)资源分配给进程,进程是程序的主体,同一进程的所有线程共享该进程的所有资源;
(3)cpu分配给线程,即真正在cpu上运行的是线程;
(4)线程是最小的执行单元,进程是最小的资源管理单元。
4. 进程间通信方式和线程间通信方式
(1)进程间通信方式:
a. 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系;
b. 信号量( semophore) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;
c. 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;
d. 共享内存( sharedmemory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信;
e. 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
(2)线程间通信方式:
a. 全局变量;
b. Messages消息机制;
c. CEvent对象(MFC中的一种线程通信对象,通过其触发状态的改变实现同步与通信)。
三、虚函数的实现
1. 概述
简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。例:
其中:
B的虚函数表中存放着B::foo和B::bar两个函数指针。
D的虚函数表中存放的既有继承自B的虚函数B::foo,又有重写(override)了基类虚函数B::bar的D::bar,还有新增的虚函数D::quz。
2. 虚函数表构造过程
从编译器的角度来说,B的虚函数表很好构造,D的虚函数表构造过程相对复杂。下面给出了构造D的虚函数表的一种方式(仅供参考):
3. 虚函数调用过程
以下面的图示为例:
编译器只知道pb是B*类型的指针,并不知道它指向的具体对象类型:pb可能指向的是B的对象,也可能指向的是D的对象。
但对于“pb->bar()”,编译时能够确定的是:此处operator->的另一个参数是B::bar(因为pb是B*类型的,编译器认为bar是B::bar),而B::bar和D::bar在各自虚函数表中的偏移位置是相等的。
无论pb指向哪种类型的对象,只要能够确定被调函数在虚函数中的偏移值,待运行时,能够确定具体类型,并能找到相应vptr了,就能找出真正应该调用的函数。
B::bar是一个虚函数指针, 它的ptr部分内容为9,它在B的虚函数表中的偏移值为8(8+1=9)。
当程序执行到“pb->bar()”时,已经能够判断pb指向的具体类型了:
(1)如果pb指向B的对象,可以获取到B对象的vptr,加上偏移值8((char*)vptr + 8),可以找到B::bar。
(2)如果pb指向D的对象,可以获取到D对象的vptr,加上偏移值8((char*)vptr + 8) ,可以找到D::bar。
(3)如果pb指向其它类型对象,同理。
来源:19应届生
大数据招生火热进行中
有想报名进行免费试听的同学可以
点击原文连接或加小骨头QQ(2187963075)进行了解哦!
大有作为
数倍薪资
据为己有
以上是关于程序员面试修炼 | 腾讯研发类面试题总结的主要内容,如果未能解决你的问题,请参考以下文章