职位投递
原文地址:https://keep-learning.top/2018/03/13/今日头条财经部门后台研发实习生面试/
我是2018年春节前投的这个职位,在今日头条的 招聘官网 投递的简历。简历交上去第二天头条就打来了电话,当时还蛮感慨,头条HR在春节前一天居然也这么兢兢业业。最终把面试时间定到了2月26号。
面试的地点在知春里地铁站附近的中国卫星通信大厦。头条占了大厦的好几层楼,有专门的一层楼是用来面试的,每个面试者都会在一个独立的单间进行面试。此外面试间里还有一个空气净化器,感觉头条还是蛮重视员工身体健康的。
一面
第一面的面试官是一个跟我年纪差不多的小哥。上来直截了当让我先做道算法题。
题目一:把一颗二叉树中序遍历转换成双向链表
我当时把题意理解成,创建一个新的链表,不用原来树的节点。后来想了想他的意思应该是不创建新的节点, 而是把二叉树的节点当做双向链表的节点,分别把left和right指针当做双向链表的next和prev指针。面试官要求在白板上写,我觉得题目比较简单,因此要求在电脑上用vim直接写出来。
以下是我当时面试时做出的解答。
// g++ -std=c++11 mianshi.cpp && ./a.out
#include <iostream>
using namespace std;
struct ListNode {
ListNode *prev, *next;
int val;
ListNode(ListNode* p, ListNode* n, int val): prev(p), next(n), val(val){}
};
struct TreeNode {
TreeNode *left, *right;
int val;
TreeNode(TreeNode* l, TreeNode* r, int val): left(l), right(r), val(val){}
};
pair<ListNode*, ListNode*> helper(TreeNode *root) {
if (!root) {
return make_pair(nullptr, nullptr);
}
auto pl = helper(root->left);
auto pr = helper(root->right);
auto head = pl.first;
auto tail = pr.second;
auto curNode = new ListNode(nullptr, nullptr, root->val);
if (head) {
curNode->prev = pl.second;
pl.second->next = curNode;
}
if (tail) {
curNode->next = pr.first;
pr.first->prev = curNode;
}
return make_pair(head ? head : curNode, tail ? tail : curNode);
}
ListNode* getDoubleLinkedList(TreeNode *root) {
return helper(root).first;
}
int main() {
TreeNode left2(nullptr, nullptr, 4);
TreeNode right2(nullptr, nullptr, 8);
TreeNode left3(nullptr, nullptr, 12);
TreeNode right3(nullptr, nullptr, 16);
TreeNode left(&left2, &right2, 6);
TreeNode right(&left3, &right3, 14);
TreeNode root(&left, &right, 10);
auto res = getDoubleLinkedList(&root);
for (auto h = res; h; h = h->next) {
cout << h << ' ' << h->val << endl;
}
}
如果采用第二种理解的话,应该是如下解法。
pair<TreeNode*, TreeNode*> helperInplace(TreeNode* root) {
// return the list head and tail
if (!root) {
return make_pair(nullptr, nullptr);
}
auto pl = helperInplace(root->left);
if (pl.second) pl.second->right = root;
root->left = pl.second;
auto pr = helperInplace(root->right);
root->right = pr.first;
if (pr.first) pr.first->left = root;
return make_pair(pl.first ? pl.first : root, pr.second ? pr.second : root);
}
TreeNode* getDoubleLinkedListInplace(TreeNode *root) {
auto tail = helperInplace(root);
return tail.first;
}
/* 遍历
auto head = getDoubleLinkedListInplace(&root);
for (auto h = head; h ; h = h->right) {
cout << h->val << endl;
}
*/
基础知识
因为我的简历里写到我之前做过一个小型 OS Kernel,此外也有少量计算机网络的问题。
Q:进程与线程的区。
A: 主要区别有两点,一是地址空间的区别,不同进程地址空间独立,同一进程的线程共享地址空间;二是资源管理的区别,相同进程的线程可以共享一部分进程的资源。并且资源通常以进程的单位分配。
Q: 简述x86中逻辑地址,线性地址和物理地址的区别。简述页式管理的原理。
A: 线性地址=逻辑地址+段基址,物理地址=页表中查线性地址。页式管理就是把地址分成三部分(x86二级页表),前10位确定页目录项,中间10位确定页表项,后面12位是offset,共同确定一个物理地址。
Q: 交换空间的作用,如果内存足够大,会用到交换空间吗。
A: 物理内存不够用时,可以把一部分页面换到磁盘中,这样,多个进程使用的内存总量可以超过物理内存的容量。第二个问题我不太了解,我之前在linux中做过实验,发现物理内存够用的时候似乎并不会占用交换空间。面试官告诉我说,其实交换空间即使会被利用,并且可以设置内核参数改变具体的行为。
Q: 简述调用open函数后,系统处理的过程。
A: open是libc的函数,内部会调用open syscall,并陷入内核态。内核根据路径,在目录树中查找对应的node。该过程中会用到VFS的接口。(这个问题我回答的不是很好,具体的概念还是了解的不太透彻)
Q: 简述TCP的三次握手和四次挥手。说明三次握手过程中,通信双方状态的变化。
A: 分别是SYN,SYN+ACK,ACK,最终确定链接。四次挥手是FIN,ACK,FIN,ACK。释放连接前需要等到TIME_WAIT。第二个问题当时我没太理解,后来想了想其实面试官想问的是TCP状态机。这个网上一搜就有。
结尾
结尾的时候面试官又随便问了一些问题。比如问我会不会Shell,我说懂一点,他就让我用shell命令实现一个Group By Field的功能,简单来说就是把
A 1
B 2
C 3
A 2
B 4
转换成
A 3
B 6
C 3
我说用awk或者写shell脚本都能实现,但是我得查文档,他就再没人我写。
然后他说要在面试系统里填些东西,就问我会什么shell命令,我就随便说了些比如find, grep, awk, chown, usermod之类的,提到find的时候他还问如何查找文件名以abc结尾的文件,回答曰find . -name ‘*abc’.,之后一面就结束了。
二面
一面面试官离开之后大概十来分钟,二面面试官就来了。二面的面试官似乎是部门中某个组的领导,相比上一个面试官要年长一些。
二面主要是两部分,下面分别来介绍。
第一部分,讲解项目
这一面主要询问了我所做的一个项目, KMR,这个项目是用Go写的基于Kubernetes的MapReduce框架。
面试官首先让我解释了一下什么是MapReduce。以及为什么这个编程模型实现了并行。
我的回答如下:
数据文件 =》Map =》list(k, v) with duplicated key => 框架根据k来sort这个list,并把相同k的v放到一起 => list(k, list(v)) => Reduce => list(k, v) with unique key
其中用户提供的是Map和Reduce函数,框架会在多个Node上运行用户定义的Map和Reduce函数,并实现数据的排序和存储。
Map和Reduce的对应关系如下图所示。
每个Mapper生成与Reducer数目相等的数据文件,每个Reducer从所有Mapper处读取与其相对应的数据文件。为了保证每个Mapper处生成的对应于某个Reducer的数据块中的Key范围一致,使用了Hash并对Reducer个数取模的方法进行shuffle。
至于Sort的过程,是先用快排生成比较小的FlushOut文件,然后使用归并排序把这些小的有序的文件合并,生成一个大的有序的数据文件。
底层的分布式文件系统则使用Ceph实现,任务的调度基于Kubernetes的Job实现。
关于MapReduce的详细讨论可以参考谷歌的论文。
第二部分,算法
这次问的算法仍然是比较简单的。给一颗二叉树,树的每个节点的值是一个0-9的个位整数,这样,从根节点到所有叶子节点会构成很多个十进制整数,要求算出所有这些整数的和。
这题面试官也同意我直接用vim写。
解答如下:
// g++ -std=c++11 mianshi.cpp && ./a.out
#include <iostream>
#include <vector>
using namespace std;
struct TreeNode {
TreeNode *left, *right;
int val;
TreeNode(TreeNode* l, TreeNode* r, int val): left(l), right(r), val(val){}
};
vector<char> s;
uint64_t getStackNum() {
int res = 0;
for (auto num : s) {
res *= 10;
res += num;
}
cout << res << endl;
return res;
}
void helper(TreeNode* root, uint64_t &sum) {
if (!root) {
return;
}
s.push_back(root->val);
if (!root->left && !root->right) {
sum += getStackNum();
s.pop_back();
return;
}
helper(root->left, sum);
helper(root->right, sum);
s.pop_back();
}
uint64_t getSum(TreeNode* root) {
uint64_t sum = 0;
helper(root, sum);
return sum;
}
int main() {
TreeNode left2(nullptr, nullptr, 4);
TreeNode right2(nullptr, nullptr, 8);
//TreeNode left3(nullptr, nullptr, 12);
//TreeNode right3(nullptr, nullptr, 16);
TreeNode left(&left2, &right2, 6);
//TreeNode right(&left3, &right3, 14);
TreeNode root(&left, nullptr, 2);
cout << getSum(nullptr) << endl;
}
写完之后,这一面就结束了。
三面
二面结束后已经到中午饭点了,二面面试官带我去吃了头条的食堂,真的是人山人海= =。
吃完饭后又等了二十来分钟,三面面试官就来了。从他负责的工作来看,这个面试官好像级别更高一点。
这一面其实没有严肃的技术问题了,大概就是聊了一下职业规划、对流行技术的掌握等,并且讲了一下我来头条之后主要做的事。
这一面感觉就是瞎聊,表现出真实的自我就好。其中提到的一些流行技术,比如etcd,Kubernetes,thrift等,我并没有深入了解,也只是停留在用过的层面,但是面试官关注的点应该就是在你是否对新技术有学习的意识。
聊了大概半个小时这一面就结束了。面试官走时还忘记把我送走,干等了十几分钟,HR打来电话,告诉我可以走了,我才离开。
总结
面试总体来时还是比较偏简单的,毕竟实习生嘛。面试结束后第三天HR就来了电话说可以给Offer了,待遇也是头条的实习生批发价了。
头条给实习生开的工资还是蛮高的,至于房补,虽然听着很美好,但是实际操作起来会发现其实满坑的,房补还催生了所谓的“头条房”= =,这个可以在网上查到,就不细说了。
祝大家面试成功吧。