一面(1h30min)
一开始看了我的简历里有关于hadoop的项目经历,所以一开是问了hadoop相关的问题
1 问:先介绍以下hadoop的简单原理
答:map从hdfs获取数据,处理成键值对,然后可以经过shuffle进行初步的reduce处理,然后经过一定的算法发送给reduce,进行整合处理,最后输出到hdfs上。
2 你们在做这个项目的时候处理了多大的数据,用了多长时间,有没有遇到过什么会让程序卡住的问题?
答:因为是学校的专选课课设,老师给了三台服务器,一个master和两个slaver,都是8g内存,一次处理大概20g的数据,用了20min左右。因为一开始对hadoop的配置不是很熟,一开始设置的读取数据的块数很小,因此很慢,后来加大之后就好了。
3 你觉得限制hadoop处理数据速度的主要原因有哪些?
答:两个方面,一是负载不均衡,比如hdfs上每个文件的大小不同,导致给每个服务器的任务量不相同,导致产生了负载不均衡的现象(此处balabala一堆),二是map到reduce的映射关系没做好,导致网络IO阻塞(这里忘记提从hdfs获取文件时,可能也会有这个问题)
4 既然hadoop存在这些问题,那你有没有其他的方式来处理大量的数据?有没有听过spark?
答:spark听是听过,但是没有了解过。至于类似的方式,我提了一下消息队列,多个consumer根据topic将数据发送到消息队列中,然后消息队列将数据进行类似的处理,然后producer根据topic从消息队列中获取数据,也可以解决类似的大量数据的问题。
5 服务器代理有没有听过,如果你有很多服务器,和很多的用户请求,如何使这些服务器负载均衡?
答:代理分为正向代理,反向代理和隧道,一般情况下互联网公司采用的都是反向代理。具体的话可以采用根据IP重定向,对于用户发来的请求,根据其IP地址利用hash函数映射到不同的服务器上,就可以负载均衡,而且可以解决用户缓存的问题。
6 如果给你一个用户请求,你如何决定将他发送到你的哪一台服务器上?
答:根据物理地址的远近和IP重定向原则,先发送到物理地址最近的服务器上,然后再根据IP重定向转发。
7 给你一个请求,你怎么样让他最快的受到处理?
答:两个方面 1由物理地址最近(或者路由器跳数最小的)的服务器进行处理 2 在由代理服务器转发的时候,选择负载最小的服务器进行处理,这样可以快一点
8 那用户请求服务器就是根据IP地址的么?
答:不是,是利用DNS将域名转换为IP进行请求
9 比如说有底层的DNS服务器,高级一点的DNS服务器和顶级的DNS服务器,用户是如何请求到它需要的IP地址的?
答:先看本地的DNS缓存中有没有,如果有的话,就直接用本地缓存的。如果没有,那么就存在两种方式,递归和非递归方式,吃出balabala,说了下递归和非递归是怎么回事。
10 好,那我们来问一些C++相关的问题,STL用过么,说一下list和set?
答:说到list,STL和它最相近的vector,list在底层是由双向循环链表实现的,它的成员变量只有一个指向了这个双向循环链表的最后一个节点,相对于vector来说有个优点是,vector是根据数组实现的,它的内部有3个成员变量,first,last和capacity,最后一个是vector的容量,如果vector内的元素个数大于capacity后,要进行扩容,就会另取一片内存进行复制,这样会导致之前的迭代器失效,而且会有很大的开销,而list就不会有这个问题,只是单纯的删除节点而已。至于set,它的底层是由红黑数实现的,只有键,没有值,而且其中不可以存放相同的值,类似的还有map。multi_set和multi_map可以存放相同的值。
11 听说过cast么?
答:主要知道的是static_cast和dynamic_cast(说一下二者的区别)。static_cast在进行类型转换的时候是编译时检查(大概是这样,不知道有没有记错),所以可能会把父类转换为子类,这样就会导致错误。而dynamic_cast则是在运行是检查,如果将父类转换为子类,则不允许转换。
12 听说过constexptr没有?
答:这个真的没了解过,然后面试官还很善良的帮我科普了一下,自己了解的还是太少了
13 说一下智能指针把?底层是怎么实现的?
答:shared_ptr,主要是解决内存泄漏的问题,就是将平常的指针用类封装了起来并加上了引用计数,同时采用了RAII的模式,当脱离了作用域,会调用析构函数,将申请的内存释放掉(这里少说了引用计数的判断,但是面试官也没说),但是使用shared_ptr会产生问题,就是循环引用时,会导致本该被释放的指针没有被释放掉,因此产生了weak_ptr,它的引用计数始终为1,这样就不会产生之前的问题。还有unique_ptr,它指向的变量只能被一个指针指向,shared_ptr则不是。
14 说了这么多,你可以实现一下shared_ptr的类么?
这里没有回答,就是在共享屏幕上写了一下简单的shared_ptr的类,主要是构造函数,析构函数和=运算符的重载,主要注意的是关于引用计数的加和减的时机以及当引用计数为0时需要进行的释放内存操作,最后在=重载的时候多了一步操作,面试官也提醒了一下,然后就过了。
15 学过网络相关的么?
答:(吐槽一下,之前问的题难道不都是网络相关的么)看过一点TCP/IP详解和UNIX网络编程,学校里有计算机网络这门课。
16 那就好,说一下TCP的三次握手和四次挥手
答:(最老套的问题,答案网上都有,我就加了一些可能会被问到的知识点,比如在三次握手的时候,第一次发送过去的SYN序列号是随机的,不然会被别人很轻易的利用产生危险,在四次挥手的时候,FIN_WAIT1,FIN_WAIT2和TIME_WAIT状态,又说了下为什么要有TIME_WAIT状态,结果面试官只是说了下"嗯,TIME_WAIT"。。。。。)
17 说一下一个请求是如何从客户端发送到服务器的,主要是协议的分层
答:这里说了一下从应用层,传输层,网络层和物理层层层包装,然后经由各个路由器转发之后到达服务器端,层层解析,然后根据网络层包装的IP地址和端口号,相同的方式发送回去即可。
18-?应该还有几个问题,但是记不太清楚了
final 我们来写一道算法题吧,如何不用乘,除和求余运算来实现除法运算,就是100/3 = 33.
答:这里我说了两个思路,一个是利用被除数,直接进行加法运算,例如3+3=6 6+6 =12 ,将得到的结果与被除数进行比较,如果小的话就继续加,大的话就减去这个数然后比较上一个结果(面试官说这个复杂度有点高,有没有其他的方法)。第二个我说用移位运算来处理(其实这里的移位运算就是相当于乘法和除法,可能题目限制的就是*和/运算符把。。。。)然后说了下思路,面试官说没有问题(结果!结果!怎么写运算的结果都不对。。。。当时极度慌张,写了半个小时应该是到饭点了)面试官问我写完了没有,我说出了点问题,运算的结果不太对,然后面试官看了下我的代码,说我的思路和代码风格都没有问题,判断的边界出了点问题(当时觉得怕是要凉了。。。然后面试官说你回去再考虑一下,下午四点准备接着面,当时内心一度欢喜,又极度难受,二面也太快了,因为之前前一天下午还有数据库考试。。。一边复习一边准备面试),我说没问题,然后就是下午的二面了。
第一面总体来说难度不是很大,面试官都是根据你的回答进行相应的拓展,主要还是解决问题的能力和基础知识。
二面 1h
下午的面试出了点小问题,约的是4点然后4点07分还没有面试官来,我就打了个电话给hr,过了一会面试官来了,看样子很年轻很和善,说是刚刚在开会,晚了几分钟,开始的时候让我做了个自我介绍,然后问我大几了,我说大三,问我能不能保研,我说还不一定,9月份学校还有一个考试,要加上那次考试的成绩,然后随便聊了几句,就开始了。
1 说一下你简历里印象最深的一个项目吧
答:(当时想回答组原课设来着,自己设计的CPU,但是想了一下好像和这个岗位没什么关系,就提了下在新国大的结课项目,一个互联网家居的模型,大概说了下原理和自己做的内容,然后面试官又细问了一些项目的具体内容,后来想想我负责的主要就是前端的编写和一个简单的andriod APP,也和这个没啥关系。。。)
2 我们来点C++最基础的吧,说一下这几个的大小,int,int*,bool,bool*, char x[]
答:先问了下是32位系统还是64位系统(当时我的内心是崩溃的,我对类型的大小一向是记不住的,然后就说了一通,回去试了一下,好像只错了一个,万幸万幸)又追问了一下哪些类型是8个字节的,我说又double和long long,其他的记不清了。
3 知道char x[10]这种,一般是以什么结尾的么?
答:\0 (知道它的ascii码是多少么)应该是0 (知道sizeof和strlen的区别么),一个是求字符串大小,一个是长度(什么时候这两个值相等呢)当时大脑当机了,不知道问这个问题是干啥的,面试官解释了好几遍,我还是懵逼状态,然后就跳过了这个问题,(后来想了一下,明明是很简单的问题。。。。)
4 说一下new/malloc free/delete 和delete p/delete []p的区别
答:new和malloc都是分配内存的,但是new会调用构造函数,而malloc不会调用,同样的,delete会调用析构函数,而free不会。delete []的话是释放对象数组的,如果不用delete []p的话,只会调用数组第一个对象的析构函数,后面的都不会调用
5 如果是new一个int型的数组呢,这两个有区别么?
答:没有区别。
6 来聊一点进程和线程相关的问题(这里问题真的让人头大。。。太多太具体了,很多我也不知道答案是什么,就列举几个印象比较深的)
-进程间通信的方式又哪些?答:共享内存,信号,消息队列(这里最基本的管道忘了回答,面试官也指出来了)
-socket呢?答:一般socket是进行不同机器间的进程通信的。
-那能不能进行同一机器上的进程通信呢?答:可以。
-平时在哪个平台上编程的比较多?windows还是linux?知道windows下的临界区么?答:我以为是进程间的临界区。。结果是windows下的一种线程同步方式。。。
-进程间进行同步的方式有哪些?答:锁和信号量。
-如果用锁的话会不会出现什么问题呢?答:可能会产生死锁。
-怎么能避免死锁的产生呢?答:学过银行家算法。(感觉这个答案不是想要的,可能是顺序封锁法?)
-知道原子变量么?答:知道atomic和atomic_flag,atomic_flag还能用来实现读写锁和自旋锁。
-为什么atomic是原子的呢?答:(这里不太了解,就回答了下应该是基于底层的读-改-写不可分割的操作)。
-如果进程间用锁的话,是不是效率有点低呢?有没有其他的方法呢?答:用基于互斥量的锁的话,是采用了阻塞的方式,如果申请的互斥量已经上锁的话,会使得进程在这里一直请求,会使得效率低下,改进的话C++11里有try_lock这种非阻塞的方式,可以在请求未通过的时候做其他的事情,可以提高效率。(好像也不是理想的答案,面试官让我回去看一下底层是怎么实现的,后面还问了很多细节的东西,区分windows和linux,很多都不知道就跳过了)
7 说一下C++对象的内存分布吧
答:对于一般的对象来说,内部只有成员变量,如果有虚函数的话,还会有虚函数表指针,放在对象内存的头部。如果是单继承的话,子类继承父类的虚函数表,如果重写了父类的虚函数,在原来的虚函数表中进行修改,如果添加了新的虚函数,就会在原来的虚函数表末尾添加新的虚函数地址。如果是多继承的话,就在继承的第一个父类进行上述操作。
8 那菱形继承呢
答:如果是菱形继承的话,一般将公共父类标记为虚基类,这样在继承的时候在子类的内存空间里只会继承一次。
9 如果其中一个父类中只有虚函数的话,那么两个虚函数表会相邻么?
答:应该会,可能还会考虑内存对齐的问题等等。。。。。。
10 关于static变量你知道多少?
答:对于类中的static变量,是所有类的实例变量共享的,放在静态变量存储区内(然后开始了死亡拷问。。。)
11 如果两个文件中定义了相同的static变量,会产生冲突么?
答:应该会吧(这里真的不太清楚)
12 如果有两个线程,调用了同样的函数,这两个线程中的函数地址是相同的么?
答:应该不同的,因为调用的时候,放在内存里,每次都要进行一次从虚拟地址到物理地址的转换,这个映射的结果应该是不同的。
13 如果是虚拟地址呢?
答:应该是相同的(毕竟是同一个地址空间和代码区。。但是没有考证过)
14 vs用过么?用的哪个版本的?会多线程的调试么?知道怎么查看线程的堆栈情况么?
答:这里是真的不知道(因为已经转mac了。。。很久没用vs写过代码了)
15 调试时如何查看变量呢?
答:每次进入一个作用域时,都会显示这个作用域内的变量,也可以自己添加要查看的变量。
16 知道怎么在调试的时候修改变量的值么?就是条件调试(大概是这个说法)
答:这个不太清楚。。。。(面试官问我平常是不是调试的时候就加个断点然后调试,我说是的,因为平常写的多线程都很简单。。。)
17 知道main函数执行前还做了哪些事情么?
答:我只记得读取全局变量,其他的不太清楚(面试官让我回去可以尝试一下看看还做了哪些事情)
18 在windows或linux下,如果我要显示某个目录的所有子目录的话,一般都采用广度优先遍历的方式,为什么不用递归呢?
答 :因为目录里会有..和.这两个目录,如果读取到的话可能会产生循环访问。
19 怎么避免这个问题呢?
答:判断文件的文件名,如果是这个文件名的话,就跳过不访问即可。
20 解决了这个问题,如果目录有好几万层,如果采用递归方式会怎么样呢?
答:可能会爆栈。
21 那你能用伪代码来实现一下以上的代码么?
答:就是用队列来实现广度优先遍历即可,注意区分目录中的文件和目录区分操作即可。
面试完之后面试官问我有没有什么想要问的,我说如果实习期间能不能请一段时间的假期,因为学校这9月份要保研还有很多事。面试官说可以,然后又问可以实习几个月,什么时候可以开始实习(学校要求至少7个月。。。无力吐槽,至于开始时间当然现在就可以)然后就说之后会让hr联系我,然后到周日晚上接到hr的电话说是二面过了,约到周四下午4点半三面。
总体来说,感觉第二次面试比第一次面试难度更大。。。可能是我基础知识不是很牢固,很多基本的问题回答不上来,还是太菜了。。。。
三面(1h10min)
面试官很年轻。。。上来问了下以后的打算,是读研还是工作之类的,然后就是聊了聊简历上的项目,哪个项目印象最深,其中最大的挑战是什么等等,然后说了一段时间,开始进入正题。
问:听你刚才说你了解html,给你一段html代码,只有标签的那种,你给我生成一个DOM树,给出的示例代码如下:
<body>
<div>
<span>hello</span>
</div>
<div></div>
</body>
输入的话当成是一个整的字符串,然后返回DOM的根节点。
说实话,我一开始看到这题的时候没特别好的思路,后来想到做编译器的时候,有过类似的建树经历,就照着类似的写了一份代码,没有跑过,(跑了肯定一堆红。。。)面试官只是看思想。
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#define Max 100
typedef T{
string name;
int level;
struct T child[Max];
}Node;
typedef T2{
string name;
int level;
}Node2;
int level = 0;
queue<string> parse(string str){
if(str.size() == 0) return NULL;
else{
queue<string> que;
int flag = 0;
Node2 node;
for(int i = 0; i < str.size(); i++){
if(flag != 1){
if(str[i] == "<" && str[i+1] != "/") {
flag = 1;
level++;
}
else if(str[i] == "<" && str[i+1] == "/"){
flag = 1;
level--;
}else{
node.name += str[i];
if(str[i+1] == "<")
node.level = level;
que.push(node);
node.name = "";
}
}
}
else{
if(str[i]==">") {
flag = 0;
node.level = level;
que.push(node);
node.name ="";
}
else{
node2.name += str[i]
}
}
}
return que;
}
}
void parse2(queue<string> &q, Node* t){
if(node.level == (t.level+1)){
Node* node = (Node*)malloc(sizeof(Node*));
Node2 node = q.front();
q.pop();
int i = sizeof(t.child)/sizeof(Node);
t.child[i] = node;
t = node;
parse2(q,t);
}
}
using namespace std;
int main() {
string html;
html = "";
Node* root = (Node*)malloc(sizeof(Node*));
root.level = 0;
queue<string> res1 = parse(html);
parse2(res1,root);
return 0;
}
后来面试官问我为什么要有parse2,直接在parse里进行构造就可以了,后来想了一下的确可以,直接用stack,当遇到闭合时,全部出栈,但是构造树的时候应该要修改一下树的结构,要有孩子指向父亲的节点,而且因为每个节点的子节点个数不确定,因此可以采用父亲兄弟型的树结构。
后面问了两个简单的C++问题,如下:
class A{
public:
void f1(){};
virtual void f2(){};
};
........
A *a = nullptr;
a->f1();
a->f2();
...........
问我两个函数的执行结果是怎么样的,答案肯定是f1可以执行,f2无法执行。
然后追问我原因:首先f1的话,因为他是普通函数成员,它在重定向的时候直接就已经指向了代码段对应的地址,即使没有实例化对象也是可以调用的。对于f2,因为它是虚函数,类调用虚函数一般是经过虚函数表来调用的,而每个类的内部有指向虚函数表的指针,这个指针必须要经过实例化才能产生,后来也验证了一下。
class A{
public:
void f1(){cout<<"hello1"<<endl;};
virtual void f2(){cout<<"hello2"<<endl;};
};
int main() {
A *a = nullptr;
a->f1();
a->f2();
return 0;
}
结果如下:
后来又问了我关于this指针的问题,如果上述代码变为
A *a = new A();
a->f1();
问我这个this是怎么处理的?
我一开始没理解面试官的意思,后来问我这行a->f1()这段代码的汇编指令是怎么样的,我才明白什么意思。。。。
this是当作参数,直接压栈进去的。