70 毕业季阿里春招实习经验贴
0 引言
趁着拿到阿里offer的新鲜劲儿,写下这个帖子给想去阿里的同学们参考一下,觉得有用的话,拿走不谢嘻嘻嘻。
我是在2019年4月底投的阿里达摩院机器智能部门的计算机图形学算法岗实习,在这之前我完成了几项工作,后来证明对我通过实习面试大有裨益。
1、完成了视觉测量相关的两个项目,其中一个结题了,另外一个马上要结题了。
2、在项目基础上延申出来的毕业设计刚刚突破了关键技术难点,而且取得了一定的效果,美滋滋。
3、在offer收割机学姐的指导下,完成了简历的撰写(我不会告诉你我用了一个超级好看的模板)。
在这个基础上,我决定试水今年的互联网就业市场,之前投了几个厂但是没有消息。投阿里的当天晚上就接到约面试的电话了,这里边应该有一些运气的成分在吧,简历和岗位描述很相关。
1 一面和二面
阿里算法岗的一面和二面是类似的,放在一起讲,分成三个部分。
(1)项目描述。
项目描述是重中之重,怎么强调都不过分。一定要对自己的项目如数家珍,对自己项目的技术点了如指掌才可以。这里我给同学们一些建议。
(1.1)认真总结自己项目的主要技术路线,最好能够做到项目技术路线描述的时候绘声绘色,让面试官的脑子里有图,能够产生联想,知道你在说什么,能够根据你的描述提出针对性的问题。做到了这一步,你就把技术面试的范围限制在你的项目里了,基本可以保证面试官不会提出太超纲的问题。
(1.2)通过项目描述把面试官的兴趣引导到自己的项目这边来了之后,必须保证对自己的项目有很详细的了解,所以接下来的一步就是梳理自己项目中用到的技术点,一定要对技术的原理和实现细节有一个非常详细的了解。如果某个算法你之前用过,但只是调用了开源库的话,那我建议你回头再看看实现细节,这一块面试官可能会问得非常细,比如我一面的时候,面试官问我神经网络中卷积是怎么卷的。另外,有节制地稍微扩展一下类似的技术点,可以应付一下带有一点发散性质的问题。做到了以上两点,基本上就可以很好地描述自己的项目,并应对面试官提出的一系列与你项目有关的问题了。
(1.3)项目描述一定要有耐心,毕竟人家面试官也是很有耐心的,愿意听你讲这些其实还挺枯燥的技术细节。你要随时关注面试官的反应,询问面试官自己是否讲清楚了,礼貌一点总没有错。
(2)两道编程题。
阿里的编程测试一般安排在项目描述之后,会随机通过邮件发两道题目过来,一般都不会很难,编不出来也没有太大的关系。我感觉编程测试主要是考察应聘者的反应能力还有编程习惯,习惯非常重要。反应能力改变不了,我针对编程习惯给大家提一点建议。
(2.1)先写注释,再写代码,注释参考java的注释写法,或者我这里的写法就可以。
/* FindNumsAppearOnce
param[in]: vector<int> data, 输入数组
param[out]: 输出的第一个只出现一次的数字
param[out]:输出的第二个只出现一次的数字
algorithm: 牺牲空间,换取时间,其时间复杂度大概为 3*n;
空间复杂度为 biggestNum - smallestNum + 1;
*/
(2.2)注意函数名和变量的命名格式,我一般函数命名写明功能,然后采用大驼峰;变量用匈牙利式,下划线。
(2.3)要有输入格式判断,适当在代码行中添加几行让面试官好理解的注释,注意缩进等。 void FindNumsAppearOnce(vector<int> data,int* num1,int *num2) {
// 输入判断
if(data.size() < 2)
return;
int biggest_num = data[0];
int smallest_num = data[0];
for(int i=1; i< data.size(); ++ i){
if(biggest_num < data[i])
biggest_num = data[i];
if(smallest_num > data[i])
smallest_num = data[i];
}
// 统计数字的个数
vector<int> data_num_count;
data_num_count.resize(biggest_num - smallest_num + 1);
for(int i=0; i< data.size(); ++ i)
++ data_num_count[data[i] - smallest_num];
bool flag = 0;
// 输出目标数字
for(int i=0; i< data_num_count.size(); ++ i){
if(data_num_count[i] == 1){
if(!flag){
*num1 = i + smallest_num;
flag = 1;
}
else
*num2 = i + smallest_num;
}
}
}
(2.5)可以写一下测试用例,展示自己的好习惯。
void test(){
vector<int> data{1,2,3,4,5,6,6,5,3,1}; // 2和4出现了两次
int num1, num2;
FindNumsAppearOnce(data, &num1, &num2);
}
(2.4)万一不会,可以跟面试官卖个萌认个怂(企图萌混过关.jpg,~( ̄▽ ̄)~*),也没啥,但是一定要实事求是,技术的事含糊不得,一问就露馅。 我一面两道题只答了一道题的一个简单的解法,可以说是很差了。我跟面试官说我不是计算机专业的,编程基础一般。他反而安慰我说,我觉得你编程基础还可以呀。我想他指的应该是我的编程习惯。但是,我回去就狂补,然后二面面到这里,我两个题都很快答出来了,保质保量。所以,面试间隙也要保持努力哦。
(2.5)要有一点训练量作为基础,推荐《剑指offer》这本书以及牛客网上这本书的在线编程练习,真的非常好,对互联网笔试关通关很有帮助。之所以把这一条放在最后不是说编程不重要,只是为了避免同学们在准备算法岗面试的时候走偏了。在项目描述达到一定水平之后,适当做一点题可以提升自己的信心。给个《剑指offer》在线编程题的链接吧。
https://www.nowcoder.com/ta/coding-interviews?page=1
(3)编程语言基础。
这一块主要问了几个东西,列举如下。
(3.1)类的内存。
空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。
(1)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。
(2)类内部的成员函数:
普通函数:不占用内存。
虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的
(3.2)类的继承。
(1) 父类和子类构造函数与析构函数的调用顺序
建立对象时,会先调用父类的构造函数再调用子类的构造函数。
销毁对象时,会先调用子类的析构函数再调用父类的析构函数。
(2) C++中public、protected、private的区别
1、private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
2、protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
3、public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数
(3)继承方式:
1、public继承不改变基类成员的访问权限
2、private继承使得基类所有成员在子类中的访问权限变为private
3、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
4、基类中的private成员不受继承方式的影响,子类永远无权访问。
(3.3)C++虚函数。
(1)C++中虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码实现可变的算法.
虚函数是通过一张虚函数表来实现的,简称为V-Table(virtual table) 。 在这个表中,主要是一个类的虚函数的地址表,这张表解决了的继承、覆盖的问题。 在有虚函数的类的实例中,这个表被分配在了这个实例的内存中,所以当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
(2)获取虚函数表的地址
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
int main(){
Base b;
cout << "虚函数表地址:" << (int*)(&b) << endl;
return 0;
}
(3)函数模板实现泛型编程
template <typename T> //声明使用模板,并定义T是一个模板类型
void Swap(T& a, T& b) //紧接着使用T
{
T c = a;
a = b;
b = c;
}
void test()
{
int a=0;
int b=1;
Swap(a,b); //自动调用,编译器根据a和b的类型来推导
float c=0;
float d=1;
Swap<float>(c,d); //显示调用,告诉编译器,调用的参数是float类型
}
(3.4)其他,上面写得不全,稍微补充一下,当然补充完肯定也还是不全的,但是没关系。
(1)纯虚函数的定义,作用,以及形式。
(2)C++指针与引用的区别,引用带来的好处是什么?
(3)C++多态性的实现有哪几种形式?(虚函数与纯虚函数,函数与运算符的重载)
这一块我一面的时候几乎完全不会,然后我花了一个下午疯狂看博客,补习,晚上几乎全都答上了,嘻嘻嘻。这一块的东西我不建议花很多时间,可以一边面试一边补,一般都是一些固定的知识点,固定的问题,看看网上的帖子就行了。
2 三面和四面
三面几乎没有任何新的东西,而且只问项目,但是有一点发散性,而且问得更细了。我这一节主要讲一下这两面跟一面二面不一样的地方。
(1)面试官level比一面和二面的level要更高了,问的东西更基础也更全面。
(2)三面面试官问了一些发散性的问题,比如她给你一个应用场景,你临场提出一个可能的技术方案。当时她给我的题目是让我设计一个三维重建的技术方案,我就把我看到的google和facebook的多相机阵列以及图像拼接的技术讲了一通,并且联系到了我项目1中的双目立体视觉的技术点。
(3)四面面试官是机器智能部门的boss,boss比较忙,前两个面试电话我都没接到(原因是我手残把手机开勿扰模式了QAQ),然后我就回拨了过去,就聊了半个多小时就结束了。这一面面试官比较忙,他的时间观念很强,说半个小时到点了就没怎么问了。他问了几个针对性比较强的问题。
(3.1)还是项目描述,建议可以简洁一点,boss很忙的,而且理解能力也强。
(3.2)boss问你能介绍一下你项目/研究中做得比较深的一个点吗?我就挑了一个点给他介绍了一下,然后聊了20分钟的样子。
(3.3)最后,boss问你觉得你自己的优缺点是啥?我差不多是照实说了,就是更擅长把新技术应用到实际场景中,但是完全原创性的东西目前做得很少。
3 交叉面
交叉面是跟四面boss一个level的其他部门的boss,这一面听说主要是为了防作弊。。。
还是聊项目,发散,然后问了几个编程的题,没有上手编,只说了一个思路。
(1)项目描述,这一块没有什么不同。
(2)boss问了接下来想往哪个方向深入研究,我就把我目前的想法跟他说了一下,顺手画了个饼,说如果做出来了,会取得怎样好的效果。
(3)编程题:口述思路。
(3.1)1-10,000,把质数输出来。
比较好的解法是用筛法。我答得不好,结结巴巴,踉踉跄跄。
(3.2)乱序的10,000个数,输出中位数。
这个题我反应神速,一下子就想到了快速排序,立马口述了一个算法,感觉面试官应该比较满意。
(3.3)圆上画一个角,锐角的概率。
这个题我也反应神速,但是错了,尴尬。。。QAQ
4 HR面和offer
HR面的面试官约好晚上八点打电话,但是九点半才打过来,然后也只聊了20分钟。。。我觉得她对我完全不感兴趣是怎么回事,,,全程问我你还有没有想问我的是怎么回事,,,我绞尽脑汁跟她扯也只想出三个问题。。。
好吧,不影响我拿到offer. 接着就是商量实习时间的事情,实习是全勤没商量,但是入职时间可以稍微推迟一下。