腾讯天美工作室后台开发实习面试

大本营暴露了🐒

一面

面试

形式:腾讯会议。
面试官是个留长发的老哥。

提前聊天

怀疑他能看到前面其他面试官对我的评价,强调了他的岗位重视基础知识扎实。
Q:是否熟悉Linux?
A:不太用,但是我用苹果的很多命令很像。
Q:基础知识学得怎么样?
A:(那我能说啥,我只能说)不错啊。有些太细节的忘记了,但是大体的能记住。
Q:学校的课学得怎么样?
A:平均83分吧。
Q:哪个课学得最好?
A:有好几个学期以前的记不住了,都差不多吧。

无自我介绍

因为被面试官发现了这个博客网站。🐵
直接挑明了看到了我Github上的项目和博客,并说不会问我其他面试问过的。🙈
我只是在简历上放了两个项目的链接,没想到他点开我的Github帐号看到了这个博客。🙉
幸好昨天网不好博客没更新上阿里的二面部分🙊

操作系统

Q:进程的调度?
A:先来先服务、短作业优先、时间片轮转。
Q:具体解释一下?先来先服务会造成什么样的问题?
A:前面的一个进程很长时间运行不完,就占着地方后面的也运行不了。
Q:给进程分配内存?
A:有固定分区的有根据大小分的,不够用时候有交换内存(说的不够详细)。
Q:JVM?
A:是个Java的虚拟机,有管分配和垃圾回收。

网络

Q:用UDP实现可靠传输?
A:在应用层做。每一个数据帧有编号要确认。
Q:说到应用层,那说说一共有哪些层?
A:OSI的是7层,TCP/IP的5层。物理层、数据链路层、网络层、传输层、(会话层、表示层、)应用层。
Q:网络层有哪些协议?
A:IP、ICMP、ARP。
Q:在博客里看到其他面试官问到RPC给解释一下呗?
A:我依然不会。

数据库

Q:关系数据库的组成?
A:???就一个个table组成的,底层是B+树。
Q:B+树的优劣(好像是问的这个吧?)
A:不适合单点查询,没有Hash那么快,但是适合范围查询。

C++

Q:问了一系列邪门知识:

  • 定义一个变量,在操作系统和内存中进行了哪些操作?
  • 你写的这个cin它为什么是输入,怎么做到的?
  • 对象的结构是什么?
  • templete是如何实现的?
  • ……

A:我这C++主要是学了怎样用,它底层怎么实现的这我真就没了解过。

游戏

Q:平时玩游戏吗?
A:偶尔玩呀。(TiMi问这我倒也不惊讶)
Q:喜欢玩游戏吗?
A:喜欢当然是喜欢呀!(会有人不喜欢玩游戏吗啊喂!)
Q:很惊奇,以为你是那种学霸女生的形象。
A:(题外话:我也很惊奇为什么好多人都觉得我不玩游戏,看到我空间里的吃鸡第6的截图就感叹我很厉害,听到我说起steam一脸惊讶“你居然知道steam?”,不然我吃鸡哪里下载的呢?再说我用着一原神的头像你觉得是哪里来的?)
Q:喜欢什么样的游戏?
A:单机的,音游,能挑战自我的这种。(我总不能说我下载过王者荣耀等等很多流行游戏玩了2天觉得不是我喜欢的类型就弃了吧)节奏大师入的坑,之后也玩了类似的几个下落式比如Deemo和Arcaea,现在在玩出现式的CytusII。
Q:(看反应是听说过节奏大师与Deemo)感叹了一下节奏大师确实很老了和Deemo确实好看。
Q:你觉得设计不好的地方有哪些?
A:有的歌不好找拍(比如Deemo,还有歌比较难听的比如节奏大师),有的谱面设计不够合理把note放在不好打不符合人体工学的位置上。

其他

Q:为啥喜欢后台开发?你说的做程序、管理数据之类听起来很无聊呀。
A:(那你又是为啥搞后台开发呢?)有用到一些算法,自己去创造一个程序出来比较有成就感吧。
Q:看什么书?
A:刘汝佳的《算法竞赛入门经典》还有另一个感觉它排版好好看于是经常用来当作比赛模版的《挑战程序设计》
Q:除了算法还对什么感兴趣?
A:大数据,现在在搞Hadoop和Mapreduce。
Q:为啥对这感兴趣?
A:觉得未来会是个发展趋势,以后的数据量会越来越大。
Q:你知道Hadoop是哪年发明的吗?
A:(这么说很老?)不知道诶。
Q:怎样了解到的呢?
A:关注了一些公众号和B站up主,喜欢看数码科技类的东西。
Q:我是说怎样了解到Hadoop的?
A:这学期选了个大数据的课,在做一点小程序。
Q:提前批快结束了,所以一两天对比一下其他同学会给结果。
A:行。
Q:还有啥你觉得可以介绍自己的方面?
A:有时候上Github上看看软件,看见好玩的也研究一下。简单讲了在看的网络攻防的那个检测程序是否有木马的那个。
Q:反问?
A:昨天晚上问差不多了,就这些了。

写代码

形式:腾讯会议共享桌面写代码。

题目

给一个矩形区域,里面有很多个小矩形,求画出一个矩形不能与给定的小矩形们重叠。

方法1

首先想到的是从头到尾枚举左上角、右下角。

那要是边界不是整数型的呢?

方法2

记录每一个小矩形的上下左右边,枚举这个。

代码

#include <iostream>
#include <algorithm>
using namespace std;

struct node {
    double x1, y1, x2, y2;
} a[1000];

int main(int argc, const char * argv[])
{
    double m, n;
    int k;
    cin >> m >> n >> k;
    double up[1000], down[1000], left[1000], right[1000];
    up[0] = 0, down[0] = 0, left[0] = 0, right[0] = 0;
    up[k+1] = m, down[k+1] = m, left[k+1] = n, right[k+1] = n;
    for (int i = 1; i <= k; i++) {
        cin >> a[i].x1 >> a[i].y1 >> a[i].x2 >> a[i].y2;
        up[i] = a[i].x2, down[i] = a[i].x1, left[i] = a[i].y1, right[i] = a[i].y2;
    }

    sort(up, up + k);
    sort(down, down + k);
    sort(left, left + k);
    sort(right, right + k);

    double ans = 0;
    for (int x1 = 0; x1 <= k + 1; x1++) {
        for (int y1 = 0; y1 <= k + 1; y1++) {
            for (int x2 = x1 + 1; x2 <= k + 1; x2++) {
                for (int y2 = y1 + 1; y2 <= k + 1; y2++) {
                    // 判断重叠
                    ans = max(ans, (down[x2] - up[x1]) * (left[y2] - right[y1]));
                }
            }
        }
    }

    cout << ans << endl;
    return 0;
}

那要是小矩形有覆盖重叠呢?

覆盖一大一小的话就按大的算,重叠没想好。

方法3

如果是一维的话就2个指针i和j依次找出每一段空隙,二维就每个空隙在纵坐标里也找。
觉得有点麻烦,没写。

方法4

过了两天面试官又问我有没有新想法,我说了“离散化+DP”,于是诞生了这个毒瘤方法。

题解

开始动态规划的转移方程想得不对,后来灵机一动想到这么个神奇的东西:
先把所有点离散化,再记录每一个小网格的面积,把所有不能选的地方(小矩形所在处)全置为负无穷,然后用得到的新地图跑一遍最大子矩阵
离散化是把所有点排序,用连续的整数值代替坐标,方便后续计算。
最大子矩阵是个二维的最大子序列,具体见这个博客,前人写过的东西菜鸡我就不复述一遍啦。
支持面重叠,未考虑边重叠的原因:输入数据为double,而浮点数是不能比较等于的。精度损失是可以忽略不计的。
时间复杂度O(k3),空间复杂度O(k2)。

代码

有点长,但是认真写了注释。

#include <iostream>
#include <algorithm>
#include <map>
#include <cstring>
using namespace std;

const int maxk = 1005, inf = 0x7fffffff;
double n, m; // 场地边界
int k; // 矩形个数
struct square {
    double x1, y1, x2, y2; // 原对角
    int _x1, _y1, _x2, _y2; // 离散化后的对角
} a[maxk];
struct point {
    double x, y; // 点坐标
    int _x, _y; // 离散化后的点坐标
} b[maxk*2];
double pos_x[maxk*2], pos_y[maxk*2]; // 用序号找原位置
map<double, int> mp_x, mp_y; // 查找离散化坐标,与pos_x和pos_y相反
double g[maxk*2][maxk*2]; // 离散后的地图,存当前小块的面积
int ans_x1, ans_y1, ans_x2, ans_y2; // 答案的对角坐标id

void init() { // 初始化,置边界值
    pos_x[0] = 0;
    pos_x[k*2+1] = n;
    pos_y[0] = 0;
    pos_y[k*2+1] = n;

    b[0]._x = 0;
    b[k*2+1]._x = n;
    b[0]._y = 0;
    b[k*2+1]._y = m;
}

// 排序用
bool cmp_x(point p1, point p2) {
    return p1.x < p2.x;
}
bool cmp_y(point p1, point p2) {
    return p1.y < p2.y;
}

void lsh() { // 离散化
    sort(b, b + k * 2 + 1, cmp_x);
    for (int i = 1; i <= k * 2; i++) {
        b[i]._x = i;
        pos_x[i] = b[i].x;
        mp_x[b[i].x] = i;
    }
    sort(b, b + k * 2 + 1, cmp_y);
    for (int i = 1; i <= k * 2; i++) {
        b[i]._y = i;
        pos_y[i] = b[i].y;
        mp_y[b[i].y] = i;
    }

    for (int i = 1; i <= k; i++) {
        a[i]._x1 = mp_x[a[i].x1];
        a[i]._x2 = mp_x[a[i].x2];
        a[i]._y1 = mp_y[a[i].y1];
        a[i]._y2 = mp_y[a[i].y2];
    }
}

void make_map() {
    // 存每一个小格的面积
    for (int i = 0; i <= k * 2 + 1; i++) {
        for (int j = 0; j <= k * 2 + 1; j++) {
            g[i][j] = (pos_x[i+1] - pos_x[i]) * (pos_y[j+1] - pos_y[j]);
        }
    }

    // 被占用的赋值为负无穷
    for (int cnt = 1; cnt <= k; cnt++) {
        for (int i = a[cnt]._x1; i < a[cnt]._x2; i++) {
            for (int j = a[cnt]._y1; j < a[cnt]._y2; j++) {
                g[i][j] = -inf;
            }
        }
    }
}

void test() { // 测试用,输出离散化后的地图
    for (int i = 0; i <= k * 2 + 1; i++) {
        cout << pos_x[i] << ' ';
    }
    cout << endl;
    for (int j = 0; j <= k * 2 + 1; j++) {
        cout << pos_y[j] << ' ';
    }
    cout << endl;

    for (int i = 0; i <= k * 2; i++) {
        for (int j = 0; j <= k * 2; j++) {
            printf("%5lf ", g[i][j]);
        }
        cout << endl;
    }
}

double max_matrix() { // 最大子矩阵和
    double tmp[maxk*2], ans = -inf, ans_y1_tmp;
    for (int i = 0; i <= 2 * k; i++) {
        memset(tmp, 0, sizeof tmp);
        for (int j = i; j <= 2 * k; j++) {
            double sum = 0;
            for (int h = 0; h <= 2 * k; h++) {
                tmp[h] += g[j][h];
                sum += tmp[h];
                if (sum < 0) {
                    sum = tmp[h];
                    ans_y1_tmp = h;
                }
                if (sum > ans) {
                    ans = sum;
                    ans_y1 = ans_y1_tmp;
                    ans_y2 = h;
                    ans_x1 = i;
                    ans_x2 = j;
                }
            }
        }
    }
    return ans;
}

int main(int argc, const char * argv[])
{
    //freopen("in.txt", "r", stdin);
    cin >> k >> n >> m;
    init();
    for (int i = 1; i <= k; i++) { // 输入同时保存所有的点
        cin >> a[i].x1 >> a[i].y1 >> a[i].x2 >> a[i].y2;
        b[i*2-1].x = a[i].x1;
        b[i*2-1].y = a[i].y1;
        b[i*2].x = a[i].x2;
        b[i*2].y = a[i].y2;
    }
    lsh();
    make_map();
    //test();
    cout << "面积:" << max_matrix() << endl;
    //cout<< ans_x1 << ',' << ans_y1 << ' ' << ans_x2 << ',' << ans_y2 << endl;
    cout << "左上角:" << pos_x[ans_x1] << ',' << pos_y[ans_y1] << endl;
    cout << "右下角:" << pos_x[ans_x2+1] << ',' << pos_y[ans_y2+1] << endl;
    return 0;
}

总结

腾讯的面试简直一个比一个令人自闭。
但令人社死这是第一回嗷。

二面

面试

形式:腾讯会议,面试官没开视频

无自我介绍

Q:看了简历,觉得没什么项目。
A:嗯,没做什么比较大的,主要是些课程大作业,而且有些不太相关就没往上写。

网络

Q:网络序、主机序?
A:没听过。。
Q:http传一个int数?
A:会包装成一个包传,有首部有数据段,首部放序号效验码什么的,数据段放它。
Q:大小端?
A:在操作系统里学的,地址从大往小存还是从小往大存,不同电脑可能不一样。
Q:你搜一下ntohl()然后说说。
A:它把网络序转化为主机序,那应该是接收到网络包存到本地之后会根据大小端改变序号。
Q:因为主机序有大端有小端,网络序统一为大端。你觉得ntohl()大小端都具体怎样计算?
A:大端的直接加减一个系数,小端加减系数之后还要把序号反过来。
Q:嗯,其实没有系数。

Q:对于游戏来说有很大的访问量,如何设计一个非阻塞server?
A:游戏的话不同于平常做的东西只有一个服务器,它可能有很多个。
Q:但是每一个服务器依然会要同时服务上万个人。
A:多线程?
Q:如何安排?一个人一个线程吗?
A:我觉得可以,毕竟一个人不会同时点出两个操作来。
Q:觉得不太行
A:也没讲出来什么其他的

Q:游戏里很多用UDP,为啥呀?
A:因为UDP不保证可靠传输,游戏丢包掉个帧可能也看不出来吧。
Q:你说的视频是这样,很多时候游戏丢包不太行。
A:那就UDP不需要建立三次握手连接比较快。
Q:用UDP保证可靠传输?
A:给数据帧编号,没收到要重传,还可以弄个拥塞控制啥的。(恭喜您在应用层实现了TCP,不过为什么贵公司总是要做这种闲得蛋疼的事情呢?)
Q:重传时间怎样设计?
A:设个初始值,总是丢包就加倍。
Q:为什么是倍数和减半的关系?
A:一种二分的思想吧,最快地找到合适的等待时间。

操作系统

Q:写C++程序时候用变量,它们怎样通过逻辑地址找到物理地址?
A:全局变量和静态变量在数据段里,函数里的变量在栈里。它们都有一个相对的偏移地址,访问时候会根据这去找它真实的地址。
Q:想问的是怎样找的?
A:内存里是分成页的,每个页有目录。有高速缓存会先找这里的。

Q:安利《深入理解计算机系统》和软件工程方面的。
A:好的,仔细看看。

写代码

形式:腾讯会议共享屏幕

问题

给一串没规律的数,找中位数。
中位数:奇数个是中间的那个,偶数个是中间靠左的那个。

分析

A:最烂的方法复杂度是先排序再找中间的了。
Q:想想是所有数都要排吗?
Q:你打算用什么排序?
A:O(nlogn)的话快排。
Q:想想快排可不可以优化?
A:先把第一个数拿出来,所有比它小的放左边,比它大的放右边,多的一边继续。

代码

先上成品,dfs是找0-n里第k大的数:

#include <iostream>
using namespace std;

#define n 11

int nn = n;
int a[n] = {2,5,1,6,3,9,7,8,4,10,11};
int small[n], big[n];

void cpy(int* x, int* y, int k) {
    for (int i = 0; i < k; i++)
        x[i] = y[i];
}

int dfs(int r, int k) {
    int cnt1 = 0, cnt2 = 0;
    for (int i = 1; i < r; i++) {
        if (a[i] < a[0]) {
            small[cnt1++] = a[i];
        } else {
            big[cnt2++] = a[i];
        }
    }
    if (cnt1 == k) {
        return a[0];
    } else if (cnt1 >= cnt2) {
        //a = small;
        cpy(a, small, cnt1);
        nn--;
        return dfs(cnt1, k - 1);
    } else {
        //a = big;
        cpy(a, big, cnt2);
        nn--;
        return dfs(cnt2, k - cnt1 - 1);
    }
}

int main(int argc, const char * argv[])
{
    cout << dfs(n, (n+1) / 2 - 1) << endl;
    return 0;
}

Q:你写那么多n,我n要是改了你得改多少个?
A:行,改成宏定义。
Q:你这空间复杂度多少?
A:O(3*n),都在a一个数组里捯饬也不是不行,主要写着麻烦,就又开两个数组。
Q:用时间太长了。
A:确实,dfs结束判断开始写错了卡了很久。

反问

Q:具体干啥?网络前面说的那些函数是需要自己写的吗?我以为是有现成可以调的。
A:嗯所以让多看看工程方面的书,确实有些开源的但也要懂它的原理。比如看看这protobuf
Q:这是二面吗?官网上还是显示的初试。
A:不同部门面试次数不太一样,微信和这都是至少三轮面试的。
Q:就是这次如果通过的话还有一个主管面?
A:✅

总结

长达100min的超长之旅,但他说看到聪明的学生想多聊几句。
我:😟-> 😃
但还是挂了,结束了自闭之旅。不面了面不动了,去阿里了。

赞赏