腾讯游戏客户端开发面试经历记录
Reeker丶
简历是去年在腾讯招聘官网投的,都快忘记这事了,前一周突然来了面试邀请。一共面了两轮,都是电面,现在在等结果。这算是我人生第一次面试工作,还是蛮有意义的,趁着还有印象+录了一部分音,赶紧过来记录一下。
初面
- 【~1:50】开始是惯例的自我介绍,说了一下专业、成绩、个人特点,(面试官表示GPA有点东西,却不知我就这点东西… my vegetable explodes)
- 【~7:40】面试官提出让我选一个有代表性的项目介绍一下。针对选的项目,展开问了挺多——遇到了什么问题,怎么处理的,配置方面,分工方面等等。因为做过的项目都是些中规中矩的项目,没有什么很炫酷的技术,所以也没什么波澜
- 【~9:00】针对简历上提到做过的游戏,进行了一些简单的提问,使用的开发引擎,使用的开发语言,有没有用过Lua等脚本语言,对Python的了解程度如何。我除了C++比较系统的学习过,其他的都说只是有一定的了解,能用的程度……(流下了没技术的泪水.jpg)
- 【~16:00】面试官听我对C++还比较自信,比较细致的考察了一下各方面基础内容,下面用问答形式给出大致过程。
- C++11新特性有过了解吗,说一下都有哪些吧
- 一个是比较好用的关键字auto,¥@#&*%……
- 经常跟它一起出现的关键字是什么?
- 应该是decltype吧?
- 对,那智能指针了解吗?
- 就是shared_ptr吗?
- 对,为什么要用到智能指针呢?
- 避免在忘记delete和发生异常时造成资源释放的问题,比普通指针安全性高一些……&¥#@%……
- new/delete和malloc/free有什么区别
- 前面这一对是C++提供的关键字,malloc和free是C提供的库函数,前面这个在用来创建类对象的时候会自动调用构造函数,后面的要自己手动计算需要分配的空间大小!@#¥%&*……
- (小声地)嗯…不完全正确……那其他的一些新特性呢
- 还有lambda表达式比较常用
- 你一般在什么时候用lambda表达式呢
- 主要是在写比较函数的时候,一些比较简单的比较函数如果只用一次,就不太适合专门写一个具名函数,容易污染命名空间,而且可读性也不会更高
- 那lambda表达式有什么优缺点呢
- 优点比较明显,写起来方便,不会污染命名空间等等,缺点就是如果函数比较复杂的时候可读性会比较低(用的比较少,一时也想不出很典型的优缺点…)
- 用过强枚举类型吗
- (我也是之后查了才知道这方面的内容,当时完全不知道这个,瞎猜可能是变体数据类型,菜哭)是variant吗?
- (些许的尴尬…)那应该我们说的不是一个东西
- const关键字你一般在什么情况下会用
- 修饰函数参数或者函数本身,保证传入对象或者调用对象本身不会被修改,很多时候也用来代替某些宏定义,因为宏不提供编译检查,不够安全……
- 对C++编译连接过程了解吗?(这里我重复了一下问题内容,可能语气显得有点不确定)对,就是gcc连接器这一类的,比较概念性的东西。我就是简单问一下看看你了解的程度。(我估计这应该就是看看个人对一项技术研究的深度吧)
- (我简单答了一下CSAPP讲的那一套流程,没有太深入,不复述了,面试官的意思是了解到这个程度就差不多了,然而我心里明白要是说的很深入肯定能加分就是了…)
- 构造函数可以是虚函数吗
- 不可以,(虚函数表原理那一套)。不过如果一个类有虚函数,析构函数最好是虚函数
- 初始化列表有哪些应用情景
- (简单说了一下,初始化列表和构造函数体里的不同,其实不是很确定)
- 那什么时候必须用初始化列表
- 调用父类构造函数和const的成员变量的时候吧…(不确定…)
- 【~40:00】下一步转战代码,加了面试官QQ,发来一个腾讯文档。(如果有不了解这个的,其实就是云同步的word文档,没有任何提示功能,你的每个按键操作对面都能同步看到)写代码过程中,面试官没有在听电话,只是在看你写代码(这种方式真特容易紧张导致大脑空白…)
- 面试官口述了题目,输入一个日期和一个天数,返回这个天数之后的日期。
- 当时第一感觉根本不像是个算法题,考察的就是各种临界情况,超麻烦…做的过程中发现自己连闰年怎么判断都忘了,还问的面试官,然后闰年的英语也不会写,用拼音写的,惭愧…
- 代码就不贴了,最后的代码临界条件应该处理的还可以,但是当时实现的整体思路是按从年往下判断,导致处理完还要往回判断一趟,只能实现的算一般情况吧…
- ∆第一次面试有点紧张,其实申完题目应该先说一下自己的理解和思路,看面试官给的反馈再做调整,既能侧面体现沟通理解能力,也可以借机避免一些错误的思路。但我是写了一会才想起这事,然后示意面试官说了一下思路,略尴尬…
- 【~42:00】写完代码又回过头来询问关于数据结构方面的内容
- 对数据结构方面了解吗
- 基本的数据结构比较熟悉,像红黑树这种就只了解概念和使用场景了……(我真坦率…)
- 对STL了解吗,STL是什么
- 就是C++提供的一组比较高效的分别适用于各类场景的容器和容器适配器外加一组算法等等吧
- 有那些容器
- list vector deque map set multixx,在C11之后还有unordered map和set那几个
- vector是怎样扩容的
- vector维护一个capacity,如果当前size到了这个capacity的时候,就在内存中重新开辟一块是原来两倍大的连续空间,然后把之前的内容易到这边来……
- 【~45:00】算法数据结构等等基础的内容就到这里,之后聊了一下关于游戏引擎等等岗位相关的内容
- 你对unity游戏引擎了解到什么程度
- (卑微而羞愧地)我现在觉得,就是用过的水平……(再次菜哭)
- 嗯…那MonoBehaviour的生命周期是怎样的
- (试探性瞎说)%@#¥@&*@……比较陌生了,印象中有这些
- 碰撞器和触发器什么区别
- (继续瞎说)
- unity的垃圾回收你知道吗
- (直接放弃)
- (面试官听出了我这块知道的很少,开始转移话题)那矩阵运算你了解吗
- 线代学过一些,还有点印象
- 正交矩阵的逆矩阵怎么求
- 我说我只记得一般矩阵通用的那种求逆过程了…
- 面试官思索了一下,可能是发现我这几个方面都太菜了,没啥好问的了,表示今天就到这里吧……
- 【~48:00】最后给了提问环节,我问了一个关于腾讯在市场导向方面的问题,其实也是因为国内和国外游戏市场的价值观差距太大了,一直也挺想了解的
- P.S. 面完感觉已经凉了…除了C++好像没有答得上来的,显得学习领域很没有广度,但是面完过了一会一查居然状态是复试了……就当是人品了
二面
- 二面除了开始简单的交流一下基本情况和意向之外,就是硬核的两道算法题,不贴代码了
- 第一个:给一个长len的数组,让你删除里面最大的N个数,重复的数字算一个。len很大,N不大
- 我的实现思路就是用一个最大堆,遍历一遍数组,都扔到堆里。然后从堆顶拿数,每拿出一个,都把后面跟着的重复的删掉,直到拿出N个数,然后遍历一遍数组把符合的删掉,把后面的往前挪
- 面完之后自己反思了一下思路,又跟大佬交流了一下,有这么几个问题,最基本的至少应该倒着扫描,这样不会重复挪很多次后面的部分,其次堆应该大小够用就行,毕竟len很长,等于多了一份拷贝,空间效率不够高
- 第二个:n个桶n个球,序号都是1~n,每个桶只能放一个球,放的球的序号跟桶的序号不能一样,一共有多少种放的方式
- 稍一思考就能发现是个数学问题,找到通项就能很简单写出递归,至于能不能再改成循环来提高效率再另说
- 但是尴尬的是我脑回路跟正常人有点不一样,所以我的递归思路有点偏门,还挺绕。刚开始代码写了一段时间之后,面试官打字提示我我的递归参数有个没有意义。其实那个参数在我的思路里是有意义的,但是面试官以为我是正常人的递归思路,所以好心给了一个提示。却不知道这个提示让我非常慌,思路整个都有点乱了……好在之后花了几分钟冷静下来,还是按照自己的思路写通了。最后给面试官讲了一下,他可能也没听过这种思路,所以还挺感兴趣的,而且至少能证明我是独立思考出来的……就结果来说也算是中规中矩了
- 第一个:给一个长len的数组,让你删除里面最大的N个数,重复的数字算一个。len很大,N不大
- 二面完之后还给我留了一道题,可能是觉得我这两道题做的还可以,想进一步考察一下。
- 题目是考官自己出的概念,叫格式化树,反正我之后查是没有查到相关概念了。具体的定义我也记不太清了,大概有这么几点:格式化树是用来输出显示的,是对普通的树进行一系列规整得到的,同一节点的子节点之间的距离要相同,父节点要在所有子节点的中间,相同深度的节点所在的高度要相同,要求整个树所占面积尽量小。
- 题目的需求不是特别明确,要求也很open,面试官的意思是尽量写,当天晚上之前把代码发过去,什么程度不做要求。
- 我根据自己的理解限制了一下需求,尽可能简单,但还是只能做到局部最优解,而且还采用了暴力枚举等等效率比较低的做法……只能说这类题目上下限确实很高,跟OJ那些有明确解和思路的算法题不一样,还是需要以后重点关注一下的
- 贴一下代码吧,大概写了4个小时(可见水平很菜了……),可能参考意义不大,我甚至都没有测试过……
typedef struct { int id; //other data... vector<Node*> children; //the order of the sub nodes is exactly the shown order in picture } Node; class Tree { private: Node* root; void format(); //other functions... }; /* * this implementation can only find local optimal solutions, assuming each step has no aftereffect (actually not) * format the tree from the lowest level * for each node, mapping to a deque, which stores a pair of displacements relative to the node for the most left and right sub nodes for each sub level * after formating all sub nodes of a node, use brutal force to compute the possible minimal gap with the deques of the sub nodes * during the process, maintain the optimal permutation, after enumerate all the cases, compute the deque of the current node * suppose : * 1. nodes with the same depth must have the same y * 2. the sub nodes of different nodes cannot be interlaced * 3. the distance of every two sub nodes of a single node must be the same */ void Tree::format() { if (root == nullptr) return; //post order traversal of the tree stack< pair<Node*, int> > stackOfNodes; //int element is used to record the times of encountering the node unordered_map<Node*, deque<pair<int, int>>> subLevelDis; //the pair of two ints records the left and the right displacement * 2 of the level relative to its parent stackOfNodes.push(make_pair(root, 1)); while (!stackOfNodes.empty()) { Node* curNode = stackOfNodes.top().first; int cnt = stackOfNodes.top().second; switch (cnt) { case 1: //the sub nodes of the current node havent been formatted yet stackOfNodes.top().second = 2; //push the sub nodes to stack in the reverse node such that the first child is on the top for (auto it = curNode->children.rbegin(); it != curNode->children.rend(); ++it) { stackOfNodes.push(make_pair(*it, 1)); } break; //end case1 case 2: //the sub nodes of the current node have been formatted stackOfNodes.pop(); //format the order of sub nodes of the current node vector<Node*>& childrens = curNode->children; if (!childrens.empty()) { int cntOfChildren = childrens.size(); vector<int> permutation(cntOfChildren); //the permutation of the index of the sub nodes for (int i = 0; i < cntOfChildren; ++i) permutation[i] = i; int minMaxGap = INT_MAX; vector<int> minMaxPermutation(cntOfChildren); //use brutal force to check each permutation //for each permutation, find the max gap do { int maxGap = 0; //compute the gap between each two adjacent node for (int i = 0; i < cntOfChildren - 1; ++i) { int tempMaxGap = 0; auto& deque1 = subLevelDis[childrens[permutation[i]]]; auto& deque2 = subLevelDis[childrens[permutation[i+1]]]; int minLen = min(deque1.size(), deque2.size()); auto it1 = deque1.begin(), it2 = deque2.begin(); for (int i = 0; i < minLen; ++i, ++it1, ++it2) { tempMaxGap = it1->second - it2->first; maxGap = max(maxGap, tempMaxGap); } } if (maxGap < minMaxGap) { minMaxGap = maxGap; minMaxPermutation.assign(permutation.begin(), permutation.end()); } } while (next_permutation(permutation.begin(), permutation.end())); //reorder the subnodes according to the indice in minMaxPermutation vector<Node*> tempChildrens(childrens.begin(), childrens.end()); for (int i = 0; i < cntOfChildren; ++i) { childrens[i] = tempChildrens[minMaxPermutation[i]]; } //compute the level displacements of the current node and bind to the node vector<int> maxLens(cntOfChildren); int maxLen = 0; for (int i = 0; i < cntOfChildren; ++i) { maxLens[i] = subLevelDis[childrens[i]].size(); maxLen = max(maxLen, maxLens[i]); } //for each level, find the index of the most left and right node, then compute the displacement deque<pair<int, int>> dis; //if all the sub nodes of current node if (dis.empty()) dis.push_back(make_pair(-cntOfChildren * minMaxGap, cntOfChildren * minMaxGap)); //otherwise for (int i = 0; i < maxLen; ++i) { int left = 0, right = cntOfChildren; while (maxLens[left] < i) ++left; while (maxLens[right] < i) --right; int leftDis = subLevelDis[childrens[left]][i].first * 2 + (left * 2 - cntOfChildren) * minMaxGap; int rightDis = subLevelDis[childrens[right]][i].second * 2 + (right * 2 - cntOfChildren) * minMaxGap; dis.push_back(make_pair(leftDis, rightDis)); } subLevelDis[curNode] = dis; //free the memory space for level displacements of the sub nodes for (int i = 0; i < cntOfChildren; ++i) subLevelDis.erase(childrens[i]); } break; //end case2 } //end switch } }
后记
- 尽管到现在还没有出结果,但是面试过程中暴露的问题已经足够有意义了。下一步,大方向上还是按照自己原本规划的推进,具体执行过程就需要根据这些问题加以侧重了。
- 这学期到现在为止都没有投过简历了,大四之前可能也不再投了,先看看这次面试的结果吧。
- 愿大家都能拿到喜欢的学校或公司的Offer!
2019.3.28
Reeker