网易互娱 游戏研发 暑期实习面试面经

前言

昨天面了网易互娱的游戏研发工程师,回想下似乎问的问题并不多,整体也就 5 个问题,但每个问题展开之后里面的量好像也不少,现在整理一下,顺便解决下面试中没答上来的地方。

面试题

先是自我介绍。。。。。面试官了解到我对 Python 更加熟悉一些,于是问了比较多的 Python 相关的问题。

Python 中如何实现类成员的 public、private、protected

面试官:在 C++、Java中,定义一个类,其成员可以申明为 public、private、protected, 那么在Python 中如何实现类成员的 public、private、protected
答:Python 语言并不存在控制访问,所以无法直接定义类成员的 pubilc、private等属性,一般情况下是通过命名规约来达到这个效果,对于 private 的成员,一般命名以 _ 开头,而 public 的则正常命名即可。

面试官:那对于双下划线__开头的成员呢?

答:双下划线开头的成员感觉主要是用在类的继承的时候,如果不想子类直接访问到父类的这个成员,则可以以双下滑线开头。(修正,不仅仅是继承的时候,在该类中定义了双下划线开头的成员,实例化的对象也无法直接访问该成员)

面试官:如果我访问了这个双下划线开头的成员,会报什么错误?

这里我突然就愣住了,因为之前都没去尝试过,愣了一会儿之后,面试官给了些提示,如果你访问一个变量,但这个变量没有定义的话,那会报什么错误?

答:应该是变量不存在吧

面试官:那上面那个问题是报什么错误呢?

答:这么一说感觉应该是变量不存在

面试官:那再问问如果我非要访问这个双下滑线开头的成员,要怎么做?

这里真就不会了 orz

现在来补一下

'''
@Description: 
@Author: 妄想
@Date: 2020-05-29 15:28:30
@LastEditTime: 2020-05-29 15:32:52
@LastEditors: 妄想
'''
class base:
    def __init__(self):
        self.public = 1
        self._internal = 2
        self.__private = 3
    
    def public_method(self):
        print('public method')
    
    def _internal_method(self):
        print('internal method')
    
    def __private_method(self):
        print('private method')

b = base()
print(b.public)
print(b._internal)
print(b.__private)

运行一下

那我该如何访问它呢?查了下资料,发现双下划线开头的成员,会被重命名,变成 _类名 + 成员名,在这里就是b._base__private

print(b.public)
print(b._internal)
print(b._base__private)

函数也是类似

python 修饰器

面试官:了解过修饰器吗?

答:了解过

面试官:介绍下 Python 中修饰器(问题大概就是这个意思吧)

答:balabalabala。。。。(大致意思就是减少代码重复,封装成装饰器方便使用,类似于函数)

面试官:那一个装饰器的输入和输出是什么呢?

答:输入?可以是任意的参数吧,这个不是根据实际定义的吗?(这里我理解错了面试官的意思了- – ,因为之前在做 web 后端的时候,用了修饰器来做 token 验证,当时将 token 作为一个参数使用了)后面在面试官的引导下,反应过来了,输入和输出都是函数。

面试官:如果我不采用 @修饰器名称 的方式来调用修饰器,那该怎么做?

答:我印象中是直接当做函数调用就好了,但是好像并没成功 orz 。。

先来个一般写法

def foo(f):
    def inner():
        print('inner')
        return f
    return inner

@foo
def bar():
    pass

bar()

会发现输出了 inner,然后我当时写了这样的调用

def foo(f):
    def inner():
        print('inner')
        return f
    return inner

def bar():
    pass

foo(bar)

发现没输出 orz, 然后就过去了,后来仔细想想,虽说返回的是函数,但只是这个函数名啊,我并没有调用它哪来的输出啊,菜哭。。

改成这样就好了

bar = foo(bar)
bar()
# 或
foo(bar)()

python 迭代器

面试官:了解过迭代器吗?可以介绍下吗?

答:迭代器一般是用在对 Python 中的数据结构进行遍历,比如我现在有一个名为 li 的 List,我想对里面每个元素进行修改,那就需要遍历每个元素,这时通过 for number in li: 这条语句,便会生成一个迭代器,然后 number 为当前所在位置的元素。

面试官:那这跟直接索引遍历相比的好处是什么?

答:(这里凭感觉答的,也不清楚对不对)1. 按索引遍历的话首先需要知道这个数据结构的长度。2. 每次遍历的时候都需要通过索引来获取这个数据,而通过迭代器可直接获取到数据。

面试官:那有没有什么情况下是不能使用迭代器的呢?

答:这里我觉得存在删除操作的时候应该是不能使用迭代器的,如下面这段代码, 我想删除列表中大于 3 的元素

li = [1, 2, 3, 4, 5, 1, 4]
for i in li:
    if i > 3:
        li.remove(i)
print(li)

输出一下发现结果为 [1, 2, 3, 5, 1]

面试官:那添加的时候呢?

答:添加的情况没遇到过,不太清楚,但感觉应该也不太行。(个人感觉,一般来说添加是需要满足某种情况才进行添加,但是在迭代过程中进行添加,会导致迭代器需迭代的元素不断增加,如果操作不当可能会陷入死循环)。

C++ 结构体对齐

本以为结构体对齐很简单的,然而。。我天真了 – –

面试官:那接下来问问 C++ 方面的内容,了解过结构体的对齐吗?

答:了解过, balabalabala。。。。

面试官:那来算一下这个结构体的大小吧

struct Node
{
    short a;
    long b;
    char c;
    char d;
    int e;
    int f;
    short i;
    short j;
    long k;
};

答:(算就算吧),算了一会儿,算出来一个 34 字节

面试官:怎么得出来的

答:首先 short 占 2 字节, 然后 long 占 8 字节, 后面加起来总共是 22 字节,对齐一下就是 24 字节,加起来总共 34 字节。(后来验证了下,发现自己错了 orz)

面试官:你确定这里第一个 short 只占 2 字节吗?

答:balabalabala。。。(我还觉得自己很对,后来下来验证了下发现自己错了 orz)

面试官:这里操作系统是怎么读取数据的?一次能读两个字节吗?

答:(这里我已经懵逼了)emmmmm。。。不太清楚。。我记得这里应该是读取一个起始位置 + 偏移量来获取这个数据。

面试官:下去之后有兴趣再去了解了解吧

这里开始补充,发现 mingw64 long只占 4 个字节???

/*
 * @Description: 
 * @Author: 妄想
 * @Date: 2020-05-29 16:42:59
 * @LastEditTime: 2020-05-29 16:54:49
 * @LastEditors: 妄想
 */ 
#include<iostream>
#include<stdio.h>
using namespace std;

struct Node
{
    short a;
    long long b;
    char c;
    char d;
    int e;
    int f;
    short i;
    short j;
    long long k;
};

// 2 + 8 + 22
// 34

int main(){
    printf("%d\n", sizeof(Node));
    printf("%d %d %d %d %d %d %d %d %d\n", 
        &Node::a, &Node::b, &Node::c, &Node::d, &Node::e, &Node::f, &Node::i, &Node::j, &Node::k);
    printf("char:%d, short:%d, int:%d, long:%d, long long:%d\n", sizeof(char), sizeof(short), sizeof(int), sizeof(long), sizeof(long long));
    return 0;
}

看一下输出

这个 long 只占 4 字节有点迷,没办法,就先改成 long long 吧。

可以发现,变量 b 的起始位置是 8 而不是 2,e 的起始位置是 20 而不是 18。

查了下相关博客

  1. 结构体中元素需被放置在自身对齐大小的整数倍的位置上
  2. 如果结构体的大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾

排序算法

面试官:接下来问问算法方面的东西吧,说一说快排的流程。

答:(很尴尬。。。已经很久没有接触过排序算法了,近期准备面试也没看过排序方面的东西,所以想了一段时间,好歹还是想起来了) balabalabala。。。。

面试官:那时间复杂度呢?

答:最差 \(O(n^2)\) ,平均 \(O(n\log n)\)

面试官:什么情况下最差呢?

答:数组完全逆序排序的时候

面试官:那怎么选取主元能使得快排的性能最好呢?

答:(这个之前在算法导论上看到过,然而已经忘了。。。想了想,如果说每次划分正好在数组的正中间,那么可达一个较优的情况所以就答了中位数)

算法题

写了道链表翻转,结果很尴尬,忘了将翻转后的链表最后一个元素的 next 设置为空,导致一直超时 orz 。。。。。过程中边 debug 边跟面试官讨论,最后改出来了。。害,太久没手写过数据结构了,这么一道简单的题还写出 bug。。。哭了

提问

问了下想到网易做游戏开发需要学些什么(毕竟当年是想做游戏开发才选的计算机,而且大一刚入学的目标就是网易游戏,虽然后来搞了近两年机器学习。。),面试官说是并没有太指望应届生有游戏开发相关的知识,要求就是基础要扎实,能够做到他们交给我们的东西我们能学会。(不说了,接着补基础去了。。。)

小节

一次体验极好的面试,总共面了一个小时多一点,过程中有答不上来的地方面试官会慢慢引导自己,结束之后感觉好像答的还不错,梳理一下发现问题还是有点多 orz。。面试之前看了不少网易游戏研发的面经,照着面经准备了几天,发现被问到的面经上都没出现,真就全靠平时积累。。。看来还有好多地方需要补一补。。说是一周内会出结果,只能看命了。。

从三月份开始投算法岗,一直到之前改投开发岗,挂了无数次,看着周围一些同学都拿了不少 offer,压力有点大。。