因为准备咕,所以现在就算结束缘分了(以下讲为什么要咕),就来发面经。
是一家创业公司,在实习僧上投递,先做了笔试题,然后进行了面试。现在已经收到了入职指引,入职的正常流程是入职——培训期——签合同。因为我咕了,所以就没有后续流程了。在后文也会讲一下我初次撰写此文时所了解到的这个公司的情况。
由于我没有正式加入公司,所以大多数消息源都来源于网络,如有著作权侵犯等问题请直接跟我讲。
如有对该公司描述不实不尽之处欢迎指正。
此外,我不记得做题时候有要求不允许公布题目内容等信息,如果确有相关隐私保护条例也请尽早与我协商。
以下:
目录
1. 笔试
我是在实习僧上投递的,大概在投完第二天就发了笔试链接,点进去之后看到说什么时候完成都可以。
题目好像没有限时,还是要求一天内做完来着?反正我中途还出去吃了一趟饭,一道题做了两小时ww。在这方面不卡人的。
笔试链接使用Airtable发的,要先点进链接注册账号,再返回,再点进链接,才能打开网址。如果打不开可以试试翻一下,我就是开着灯笼写的题。
笔试题目是5道算法题+很多道问答题。算法题不限制语言(我用的是Java),而且用例也很好过,不怎么卡边边角角、大用例这种情况。问答题涵盖了个人信息、Git使用、数据库、爬虫、机器学习与数据挖掘相关问题,都不难,而且忘了可以百度,不限制跳出页面、不开摄像头、不开麦克风。但是据说明,会记录你在该页面的移动痕迹,包括写算法题时是怎么debug的。
因为我在写笔试题的时候把内容都抄在了本机文件上(主要是怕写着写着网站崩了,我没有备份还要重写),所以现在能根据当时的作答结果回忆题目。可能会弄错具体题目是什么。
1.1 算法题
直接提供各语言的环境,我就是直接在网站上debug的。提交前可以跑代码,也可以跑用例,使用体验还可以。提交后可以直接看到用例运行结果,但是提交后代码里面的中文注释会变成乱码。但是提交后不能修改。
好像每个题只有十几个用例,都不是那种稀奇古怪的用例,都是正常用例。
题目全是英文。
1.1.1 第一题:Number Stream
题目大致是输入一个全是数字的字符串,检验字符串中是否含有对应数字对应数量的连续字符串,比如是否出现连续3个3、连续5个5之类的。挺简单的,一路遍历下去就完了,遇到了这种情况就输出true,没有就输出false。
以下附我的解法:
import java.util.*;
import java.io.*;
class Main {
public static String NumberStream(String str) {
// code goes here
char[] chars=str.toCharArray();
int index=0;
while(index<chars.length){
int i=chars[index]-'0';
//i是这个数字。如果该数字再连续出现i-1次,就返回true
//一个不行就跳到下一个循环
boolean returnTrue=true;
for(int k=0;k<i-1;k++){
index++;
if(index>=chars.length||chars[index]-'0'!=i){
returnTrue=false;
break;
}
}
if(returnTrue){
return "true";
}
}
return "false";
}
public static void main (String[] args) {
// keep this function call here
Scanner s = new Scanner(System.in);
System.out.print(NumberStream(s.nextLine()));
}
}
1.1.2 第二题:二叉树的前序遍历
这道题的二叉树是以数组形式给出的……它有一个坑用例不是完全二叉树……坑傻我了,我就说它怎么还有一个跟别人不一样的……
除了这个用例之外,别的都是正常用例。所以我的解法还是完全二叉树的解法……我在写这道题的时候没想到非完全二叉树的数组格式要怎么获取某一元素的父元素与子元素,后来我想了想,我也不知道要怎么找……但是!非完全二叉树数组要转化成树元素(TreeNode)形式其实不难!所以我其实可以先把数组转化为TreeNode再遍历!这样它就变成了最基础的前序遍历问题!
我这边给出的解还是完全二叉树的数组形式的前序遍历迭代解法,这个解可以过大多数测试用例,但是非完全二叉树的那个过不了:
import java.util.*;
import java.io.*;
class Main {
public static String PreorderTraversal(String[] strArr) {
StringBuffer result=new StringBuffer();
if(strArr.length==0){
return result.toString();
}
//前序遍历是根-左-右
Deque<Integer> stack=new LinkedList<Integer>(); //栈。压入栈的顺序就是输出结果的顺序
stack.push(0); //在栈中压入根节点
result.append(" ");
result.append(strArr[0]);
int i=0;
while(i*2+1<strArr.length&&!strArr[i*2+1].equals("#")){
//如果i对应的元素有左孩子,就加其左孩子,让i指到其左孩子;如是循环,直至加到根节点最后一个左孩子
i=i*2+1;
stack.push(i);
result.append(" ");
result.append(strArr[i]);
}
while(!stack.isEmpty()){ //弹出元素,压入其右孩子,再逐层压入其左孩子直至没有左孩子
int j=stack.pop();
if(j*2+2<strArr.length&&!strArr[j*2+2].equals("#")){
//如果j有右孩子
j=j*2+2;
stack.push(j);
result.append(" ");
result.append(strArr[j]);
while(j*2+1<strArr.length&&!strArr[j*2+1].equals("#")){
//加到最后一个左孩子(这个逻辑跟上上个while循环一样,为了代码的简洁其实可以优化一下的)
j=j*2+1;
stack.push(j);
result.append(" ");
result.append(strArr[j]);
}
}
}
result.append(" ");
return result.toString();
}
public static void main (String[] args) {
// keep this function call here
Scanner s = new Scanner(System.in);
System.out.print(PreorderTraversal(s.nextLine()));
}
}
1.1.3 第三题:拆数组
把一个数组拆成两个等长的数组,要求这两个数组的和相同,最后将两个数组按序输出(我看到网上有一个有点像的题目,但是数组不等长。那样的话会比较难)。直接无脑暴力遍历,可以全过测试用例的。
我的解法就是直接遍历每一种可能的数组组成方式,就从长度为n的数组里遍历每一种由n/2个元素组成的数组的可能性。所以时间复杂度是
O
(
C
n
n
/
2
)
O(C^{n/2}_{n})
O(Cnn/2)。
import java.util.*;
import java.io.*;
class Main {
public static String ParallelSums(int[] arr) {
int sum=0; //计算arr数组的和
for(int a:arr){
sum+=a;
}
if(sum%2!=0){ //如果sum非偶数,则必然无解
return "-1";
}
sum/=2; //arr数组的和/=2得到两个子数组的合
int n=arr.length;
int[] pointers=new int[n/2]; //指针数组:其中一个子数组每个元素对应arr元素的索引
for(int i=0;i<n/2;i++){ //pointers初始化为0,1,...,n/2-1
pointers[i]=i;
}
while(pointers[0]!=n/2){ //遍历每一种情况
int this_sum=0;
for(int p:pointers){
this_sum+=arr[p];
}
if(this_sum==sum){ //这种情况符合题意,输出
int[] array1=new int[n/2];
int[] array2=new int[n/2];
int p_index=0; //按pointers分配两个数组
int index1=0;
int index2=0;
for(int i=0;i<n;i++){
if(p_index<n/2&&pointers[p_index]==i){
array1[index1]=arr[i];
p_index+=1;
index1+=1;
}else{
array2[index2]=arr[i];
index2+=1;
}
}
Arrays.sort(array1);
Arrays.sort(array2);
StringBuffer result=new StringBuffer();
if(array1[0]<=array2[0]){ //先输出第一个元素比较小的数组
for(int a:array1){
result.append(a);
result.append(",");
}
for(int a=0;a<n/2-1;a++){
result.append(array2[a]);
result.append(",");
}
result.append(array2[n/2-1]);
}else{
for(int a:array2){
result.append(a);
result.append(",");
}
for(int a=0;a<n/2-1;a++){
result.append(array1[a]);
result.append(",");
}
result.append(array1[n/2-1]);
}
return result.toString();
}else{
for(int i=n/2-1;i>=0;i--){
if(pointers[i]!=i+n/2){
pointers[i]++;
int a=1;
for(int j=i+1;j<n/2;j++){
pointers[j]=pointers[i]+a;
a++;
}
break;
}
}
}
}
return "-1";
}
public static void main (String[] args) {
// keep this function call here
Scanner s = new Scanner(System.in);
System.out.print(ParallelSums(s.nextLine()));
}
}
1.1.4 第四题:City Traffic
这个题目还挺复杂的,我看了半天。
大概逻辑是有若干城市,每个城市有一个人数(这个人数不会重复),还有该城市可以连通到哪些城市(用别的城市的人数来作为标识)。这样形成一个无向图,而且题目保证图里面没有环,没有孤岛。
现在要求输出每个城市对应的最大交通量,这个交通量是它各个通路上交通量的最大值。在各个道路上的交通量,是这条路上能通向的所有城市的人数的总和。(大概逻辑就是,A城有南北两条路,南路连往B城,B城连往C和D城,C城连往E城,BCDE总人口100;北路同理,总人口70。A城的最大交通量就是max(100,70)=100)
输入大概是这样:new String[] {"1:[5]", "2:[5]", "3:[5]", "4:[5]", "5:[1,2,3,4]"}
每个元素冒号前面是城市,冒号后面是它连通的城市
输出这样:1:14,2:13,3:12,4:11,5:4
每个城市及其对应的最大交通量
要求输出按照城市人口数升序排列,有个坑就是它有的用例不是按照这个顺序输入的……我一开始没注意到这一点,就被坑了……所以还是得自己排
(这描述起来太费劲了,早知道我就该截个图的)
我还是暴力解,也能过。
import java.util.*;
import java.io.*;
class Main {
public static String CityTraffic(String[] strArr) {
int l=strArr.length;
ArrayList<ArrayList<Integer>> city2city=new ArrayList<ArrayList<Integer>>(); //将strArr转化成链表的链表
for(int i=0;i<l;i++){ //遍历strArr中每一个城市-连通城市对元素
String s=strArr[i];
String[] s_split=s.split(":|\\[|\\]|,");
//该字符串数组第一个元素是city,后面的是其连接的city们(注意split方法得到的数组有空值)
ArrayList<Integer> one_city=new ArrayList<Integer>();
for(String city:s_split){
if(!city.equals("")){
one_city.add(Integer.parseInt(city));
}
}
city2city.add(one_city);
}
//city2city中每一个元素是城市-连通城市s
//对city2city进行排序,按照第一个元素(本城市)的数字大小来排列
//这样排序后,输出时就可以直接按顺序输出了
Collections.sort(city2city, new Comparator<ArrayList<Integer>>() {
@Override
public int compare(ArrayList<Integer> list1,ArrayList<Integer> list2) {
if(list1.get(0)>list2.get(0)){
return 1;
}else{
return -1;
}
}
});
StringBuffer result=new StringBuffer();
for(ArrayList<Integer> a:city2city){ //遍历每一个城市
int this_city=a.get(0);
result.append(this_city);
result.append(":");
int max_road=0;
for(int i=1;i<a.size();i++){ //遍历该城市对应的每一条路
//加总这条路上的每一个城市的人口数
int f=a.get(i);
ArrayList<Integer> f_list=new ArrayList<Integer>(); //队列
f_list.add(f);
int road=f;
HashSet<Integer> iterated=new HashSet<Integer>();
iterated.add(this_city);
while(f_list.size()!=0){
//遍历city2city元素f,把f加进iterated,然后删掉
//找到f对应的别的城市,把这些城市中没有遍历过的加进road
f=f_list.get(0);
iterated.add(f);
f_list.remove(0);
for(ArrayList<Integer> b:city2city){
if(b.get(0)==f){
for(int c=1;c<b.size();c++){
int d=b.get(c);
if(!iterated.contains(d)){
f_list.add(d);
road+=d;
}
}
break;
}
}
}
max_road=Math.max(max_road,road);
}
result.append(max_road);
result.append(",");
}
result.deleteCharAt(result.length()-1);
return result.toString();
}
public static void main (String[] args) {
// keep this function call here
Scanner s = new Scanner(System.in);
System.out.print(CityTraffic(s.nextLine()));
}
}
1.1.5 第五题:Wildcards
一个简化版的正则表达式。+代表一个字母,$代表一个数字,*如果后面跟{N}的话代表N个相同的字符,如果不跟的话代表3个相同的字符。
这个比力扣版的简单,这个可以直接遍历解。
import java.util.*;
import java.io.*;
class Main {
public static String Wildcards(String str) {
String[] str_split=str.split(" ");
String pattern=str_split[0];
String string=str_split[1];
int p_pointer=0; //指向pattern中对应的字符
int s_pointer=0; //指向string中对应的字符
while(p_pointer<pattern.length()&&s_pointer<string.length()){
char c_p=pattern.charAt(p_pointer);
if(c_p=='+'){
p_pointer++;
char c_s=string.charAt(s_pointer);
if(c_s>='a'&&c_s<='z'){
s_pointer++;
}else{
return "false";
}
}else if(c_p=='*'){
//检查星号后面有没有{N}
int N=3;
if(p_pointer+1<pattern.length()&&pattern.charAt(p_pointer+1)=='{'){
int n_end_index=p_pointer+3;
while(n_end_index<pattern.length()&&pattern.charAt(n_end_index)!='}'){
n_end_index++;
}
N=Integer.parseInt(pattern.substring(p_pointer+2,n_end_index));
p_pointer=n_end_index+1;
}else{
p_pointer+=1;
}
//string中该元素是f
char f=string.charAt(s_pointer);
s_pointer++;
//如果f后面跟了N-1个f
for(int i=0;i<N-1;i++){
if(string.charAt(s_pointer)!=f){
return "false";
}
s_pointer++;
}
}else{
//$的情况
p_pointer++;
char c_s=string.charAt(s_pointer);
if(c_s>='0'&&c_s<='9'){
s_pointer++;
}else{
return "false";
}
}
}
if(p_pointer==pattern.length()&&s_pointer==string.length()){
return "true";
}else{
return "false";
}
}
public static void main (String[] args) {
// keep this function call here
Scanner s = new Scanner(System.in);
System.out.print(Wildcards(s.nextLine()));
}
}
1.2 问答题
中文题目。
个人信息方面:问了你叫啥、联系方式、从哪儿投递的。
Git方面:git merge和rebase的区别(哇这个问题真是常问常新),git怎么输出log,git怎么推送。(我本人没怎么用过git,不过实话说,我觉得这个工具常用的就那么几句,在需要协作的时候大家简单统一一下就行,十分钟的事)
数据库方面:范式和反范式,索引,约束,delete和truncate,还有一个SQL语句是什么意思。
数据分析方面:你有什么爬虫和机器学习相关的经验,对过拟合、大数据、数据不平衡、数据缺失、异常值等问题有没有处理经验和解决方案。对各种机器学习算法的了解程度。
2. 面试
笔试结束后过几天发邮件,用calendly约时间开腾讯会议面试。是创始人亲自面。
面试中没什么专业性问题,主要问了些经历和性格方面的问题。大约半小时左右。感觉老板很看重和员工在气质、精神上的融洽程度,尤其是员工自我解决问题的能力,也可以说是公司不怎么培养实习生主要靠自己查搜索引擎。老板希望员工有独当一面的能力,只需要简单点拨,主要内容还是靠自己。而在工作职能方面,也囿于小公司人数所限,大家都需要身兼多职,一个人扛一个项目。
在面试中聊到,数据科学家一职可以扛很多种项目。有个大项目,是在JD中写到的大数据选房项目。此外,让渡居崇尚自动化,希望各个环节都能通过自动化的方式来解决,所以数据科学家还会遇到一些自动化之类的项目。比如写个用机器学习模型筛选求职者之类的项目之类的(这类项目近几年感觉还挺火?)。
老板在面试中指出,他认为我有做题能力(指笔试题做的不错),但是没有表现出解决实际问题的能力,希望能够在日后的工作中我证明自己解决问题的能力。对此我非常感谢对我做题能力的表扬,而且对没机会入职证明解决实际问题能力表示遗憾。
工作是远程入职,不打卡(但是我在入职流程文件中看到需要写工作时长,而且根据工作时长和表现情况算工资)。
绩效计算方式,根据了解应该是OKR+工作时长+老板观感来发。说明是采用密薪制,但是实习生每天不超过100元这一点应该是统一的。对这一点如果我猜错了请指正。老板对于工资较低这一点的态度是,以前的经历证明,高工资并不能增加绩效,反而有时低工资的优秀实习生能够做出很好的成果。
因为我个人想做实习的目的就是学习,本来确实不图钱;而且我看这家公司看起来势头不错(虽然我个人对房屋中介这行和初创公司这个属性有偏见),在这里做数据岗,当有可学、可练、可得之处;而且还是远程实习,不用通勤。
但是,收到入职流程相关文件并在网上浏览了公司相关风评再经历了一番思考之后,我还是决定咕了。
3. 公司业务、入职流程、公司风评、企业文化观感及我咕的原因
让渡居是做房屋短租(民宿)中介的。我对民宿这行不熟,我看他们家在北上广深蓉有分部,估计他们家业务范围也在这些地方(这行没分部应该扩不了业务范围吧?)。
具体是什么业务逻辑我还没看懂,我看了看微信公众号和官网,感觉大概就是房东和房客分别找他们来短租,商业逻辑是他们用了大数据预测房源潜力……等等方式,省了很多中间成本,所以给房东更多钱,租给房客更便宜。
官网:https://www.rangduju.com/
他们那个房租计算器还挺复杂的,我没经验,不会看那个,懒得研究了反正我也不来了。
我看了一下,他们家感觉是主要针对房东的,网页、知乎号和微信公众号开放给房东的入口和针对房东发布的内容都更多。房客应该是用各种短租平台来租房,主要应该是用Airbnb。看逻辑应该是跟Airbnb在竞争中合作,Airbnb也会拿到一部分钱,但是让渡居更想跟租客私下交易,这样给租客更多优惠条例……类似于各大品牌都希望从淘宝把客源捞到小程序或APP上,为此会在自有平台上给出更大优惠?大概是这么个逻辑?
这家公司的企业文化还挺多的。讲几个比较落地的:
“自动化”
(图源1)
在招聘流程中,发邮件之类的也很自动化。希望自动化工作能进一步深入。我觉得现在还在细节上有一些问题,是解释不清的问题,还有入职流程文件中有些问题我需要反复查找才能找到。可能公司对自己的流程很有信心,所以建议入职者别问。当然,我觉得硬要摸索还是能摸索出来的(我指我觉得我摸索出来了),但是整体上还是感觉混乱。没有HR写个标准的SOP吗?
你画个流程图也行啊?
不过我看了一下他们家对HR的招聘要求,我觉得他们家现在没有HR,因为我根本没有看见过甜美的HR小姐姐。
(以下顺序是我猜测的)在自动化后的入职流程中,求职者通过面试后,应当先申请某一微信文档权限,并在获取权限后24h内提交入职信息(如身份证、卡号等),以上工作需要在7个工作日内完成;然后加入公司的企业微信进群,要求在入职前一天加企业微信(为了防止有的人过了面试但是要隔好久才入职,对微信群中的交流造成障碍)。
(我是准备直接拖过截止日期的。虽然现在还没过,但是我已经这么打算了,所以可以提前预设我已经咕了)
然后需要度过为期数日的培训期,培训期需要做一些工作,这些工作需要如何约定培训和反馈……以及在此期间需要注册和绑定一系列企业账号和邮箱,比如要通过企微获得企业邮箱,然后用它来注册airtable、领英等平台,但是Airbnb还是得用手机号注册……使用的工具也挺麻烦,不让用MS office全家桶;企业微信要拉一个别部门人员进本部门群还需要在后台给这人加上该部门职务再拉进群,但是这样就没法显示过去信息了,如果要让这人看过去信息可以直接在部门群拉人,但是还是得在后台做修改,要不然别的同事就不知道这人本部门是哪儿;airtable是一个很奇怪的平台,给我一种数据库+石墨文档+问卷星的感觉,国内网加载很慢,而且我打开表格往下翻一会儿它会反跳回最上面……
(我琢磨了半天都不确定我琢磨对没有这一系列逻辑)
“第一性原理”
老板推崇这玩意儿,那咱公司就应该照着这个逻辑来,把那些乱东西都砍掉:拿好奥卡姆的剃刀,一个表格里只有10个人在职的公司整出这么复杂的东西干啥?
我在网上看了一下,发现有很多人吐槽这个入职流程混乱而且没人问的。可见确有他人有此感觉。我认为凡乱局皆当斩,所以这个流程也该做简洁些。
最后,我咕的原因有四点:
- (这是主要原因) 我感觉我和公司的企业文化不符。我在应募流程中有不适、混乱、复杂感,而且我刚开始找实习确实还是想多学学规范工作、SOP,先被带入门。怎么说呢,让渡居推崇员工自己解决问题的能力,没人带,我总觉得我要是有这个能力或者想要这样一个环境,我也不会选择一家日薪不足100元在职不过20人去年融到种子轮的初创公司(当然,我这样说让渡居,只是因为我怀揣着庸俗的看公司名气、工资高低的想法,也许有人就能看出它家前途之广大而毅然选择上车呢)。而且我感觉我跟老板聊天时聊得不尽兴。这家公司推崇第一性原理,推崇艾隆马斯克,马斯克是啥人咱都知道,我考虑了考虑,我觉得我应该遵从企业文化,学马斯克,我觉得我跟老板唠得不爽,我就不来了。
- 公司风评不太好,各内容社交平台都有指责该公司的帖子。当然也有夸的。大家都是能用CSDN的人我相信你们能查得到。而且我看了一下员工表,感觉离职率确实很高,这个离职率就算是初创公司也太高了吧……初创公司都这么野的吗?而且在职的除了老板全是实习生,工号都在半数以后。感觉这个流动性过度的强了,不是说职场讲究在一个地方深扎、跳槽太快会被认为稳定性差人不踏实吗,我还是想找个能深扎(起码有深扎前例)的公司干干试试。
- 要用我自己的信息来注册很多平台的账号工作用,还要实名、用真人头像。对账号的所有权和隐私问题,我心存疑虑。害,这一点其实我也不是特别介意,我主要是觉得这也算一缺点,可以加上。
- 需要搞很多别的工具用,比如airtable(要翻。我过几个月就要去一个不敢翻的地方了)、企业微信、Airbnb……而且不让用office三大件。呃,这点我其实也不太介意,就是在本机上安装稍微有点麻烦,但是我总觉得既然这个实习让我不爽了,那我确实能找出很多让我不爽的点,它们构成了往已经被压垮的骆驼上加筹码的稻草,愈加坚定了我咕咕的心。
最后,虽然我咕了还不说(主要我没找着应该跟谁说……我确实不喜欢这样,我确实不符合企业文化),但还是祝公司前途似锦,财源广进,老板多多赚钱,员工们能收获到优质的工作体验、学到东西~~~
参考资料: