面试记录1:华为技术一面
1 面试背景
- 面试公司:华为技术有限公司
- 面试岗位:推荐搜索
- 面试类型:技术一面
- 面试时间:2024-06-13 9:00~10:00
- 面试结果:通过 😊
2 整体感受
感觉面试官一进来就很面善哈哈哈哈,整体感觉面试过程很让人舒服,面试官说话的语气也很好。
面完整体感觉还是挺不错的,希望能通过这一轮面试。
需要加强的地方:
- 需要了解一下 C++ 和 Python 语言的特性,面试真的会被问到
- 基础知识还是挺重要的,系统能力应该注意
3 提问的问题
首先让你进行自我介绍,感觉现在自己自我介绍一点都不紧张,很放松,达到了自己想要的效果。
3.1 针对简历
问:你当过就业市场部副部长,还举办过第一届双选会?
答:是的,(说完之后面试官笑了笑,哈哈哈哈,感觉挺逗的,当时我应该再回复几句就更好了哈哈哈,面试官人真好)
问:你简历中参加了博弈大赛,获得了两个项目的一等奖,能具体说一下队伍分工吗?
答:我们队伍一共3个人,我作为队长主要负责整体进度规划和核心代码撰写,另一个人主要负责神经网络的训练,最后一个人主要负责棋力水平的测试。
问:你说你们训练使用了分布式训练,能具体说一下吗?
答:我们学校的机房中机器大部分都是没有GPU的机器,只有CPU,所以我们使用机房中的20~30台机器收集数据,并将收集好的数据给实验室中的一台3060ti的服务器进行训练
问:你们这个3060ti服务器上有几张卡?
答:只有一张卡
问:只有一张卡(面试官此时有些惊讶),那你们训练了多少时间啊?网络的参数大概有多少?
答:训练了3个星期左右,网络参数的话没太关注,因为我们使用的是AlphaGo的网络结构,没有进行改进。
3.1.1 网络的参数规模
Number of parameter: 3.08M
1 | total = sum([param.nelement() for param in self.trainer.net_work.parameters()]) |
问:你了解过什么深度学习框架吗?
答:主要使用过pytorch框架
问:那你知道pytorch中有一个关于分布式计算的接口叫什么吗?
答:这个不太清楚,因为实验室资源有限,确实没有使用过这个功能(此时面试官哈哈哈哈笑了,感觉很放松耶)
3.1.2 torch.distributed
torch.distributed 包为在一台或多台机器上运行的多个计算节点上的多进程并行结构提供PyTorch支持和通信原语。 torch.nn.parallel.DistributedDataParallel()类就是基于此功能构建的,作为任何PyTorch模型的包装来提供同步分布式训练。这不同于 Multiprocessing package - torch.multiprocessing 和 torch.nn.DataParallel() 提供的并行结构,因为它支持多台联网的机器而且用户必须显式地为每个进程启动主要训练脚本的副本。
3.2 C++ 代码知识
问:你主要使用什么语言多一点?
答:C++ 吧
问:你上面不是有个项目,涉及神经网络吗,那不应该是 python 吗?
答:是的,但是我写算法题什么的都是使用 C++,而且那个也是去年做的项目了,所以现在对 C++ 比较熟悉一些
问:那我问你一些关于 C++ 语法的问题吧,你知道 C++11 引进了什么特性吗?
答:(当时其实心里一点底都没有,但当时只能硬说了)我记得有一个 auto 关键字,好像还有一个 lamda 表达式
3.2.1 C++11新特性
- nullptr:替代 NULL,专门用来区分空指针、0。nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
- 类型推导:C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。
- auto 进行类型推导
- decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的,它可以使编译器自动分析表达式的类型并得到它的类型,最关键是它不会去计算表达式的值。它的用法和 sizeof 很相似:
decltype(表达式)
- 区间迭代 - 基于范围的 for 循环
- Lambda表达式: Lambda 表达式实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
- 正则表达式:提供了正则表达式库,用于操作 std::string 对象
问:既然说到lamda表达式,那你能说说lamda表达式什么吗?
答:(这个真的一点都不知道)我主要是在python中使用过,C++中很少使用,….
问:那你说用了lamda表达式有什么感受吗?
答:感觉代码更短更简洁了(面试官笑了,我也笑了哈哈哈哈)
3.2.2 Lamda表达式
Lambda 表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。Lambda表达式的基本语法如下:
1 | [capture list] (parameter list) -> return type { function body } |
- capture list 是捕获列表,用于指定 Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。捕获列表可以为空,表示不访问任何外部变量,也可以使用默认捕获模式 & 或 = 来表示按引用或按值捕获所有外部变量,还可以混合使用具体的变量名和默认捕获模式来指定不同的捕获方式。
- parameter list 是参数列表,用于表示 Lambda表达式的参数,可以为空,表示没有参数,也可以和普通函数一样指定参数的类型和名称,还可以在 c++14 中使用 auto 关键字来实现泛型参数。
- return type 是返回值类型,用于指定 Lambda表达式的返回值类型,可以省略,表示由编译器根据函数体推导,也可以使用 -> 符号显式指定,还可以在 c++14 中使用 auto 关键字来实现泛型返回值。
- function body 是函数体,用于表示 Lambda表达式的具体逻辑,可以是一条语句,也可以是多条语句,还可以在 c++14 中使用 constexpr 来实现编译期计算。
3.2.2.1 Lambda表达式的捕获方式
- 值捕获(capture by value):在捕获列表中使用变量名,表示将该变量的值拷贝到 Lambda 表达式中,作为一个数据成员。值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化。值捕获的变量默认不能在 Lambda 表达式中修改,除非使用 mutable 关键字。
1 | int x = 10; |
- 引用捕获(capture by reference):在捕获列表中使用 & 加变量名,表示将该变量的引用传递到 Lambda 表达式中,作为一个数据成员。引用捕获的变量在 Lambda 表达式调用时才确定,会随着外部变量的变化而变化。引用捕获的变量可以在 Lambda 表达式中修改,但要注意生命周期的问题,避免悬空引用的出现。
1 | int x = 10; |
- 隐式捕获(implicit capture):在捕获列表中使用
=
或&
,表示按值或按引用捕获 Lambda 表达式中使用的所有外部变量。这种方式可以简化捕获列表的书写,避免过长或遗漏。隐式捕获可以和显式捕获混合使用,但不能和同类型的显式捕获一起使用。例如:
1 | int x = 10; |
3.2.2.2 Lambda表达式的优点
简洁:Lambda表达式可以省略函数名和类名,直接定义和使用,使得代码更加简洁和清晰。
灵活:Lambda表达式可以捕获外部变量,可以作为函数参数,也可以作为函数返回值,使得代码更加灵活和方便。
安全:Lambda表达式可以控制外部变量的访问方式,可以避免全局变量的定义,可以避免悬空指针和无效引用的产生,使得代码更加安全和稳定。
问:NULL和nullptr有什么区别?
答:nullptr应该是指空指针,NULL应该是指无效的数据类型(当时这个一点不知道哈哈哈哈)
问:你一般用哪个?
答:我只使用后面那个,前面那个只听说了,自己没有使用过
3.2.3 C++中NULL和nullptr的区别
在编写C程序的时候只看到过NULL,而在C++的编程中,可以看到NULL和nullptr两种关键字,其实nullptr是C++11版本中新加入的,它的出现是为了解决NULL表示空指针在C++中具有二义性的问题。
在C语言中,NULL通常被定义为:#define NULL ((void )0)
,所以说NULL实际上是一个空指针,如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
1 | int *pi = NULL; |
但是问题来了,以上代码如果使用C++编译器来编译则是会出错的,因为C++是强类型语言,void*是不能隐式转换成其他类型的指针的,所以实际上编译器提供的头文件做了相应的处理:
1 |
可见,在C++中,NULL实际上是0.因为C++中不能把void*类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题,程序执行的结果会与我们的想法不同,举例如下:
1 |
|
在这段代码中,我们对函数func进行可重载,参数分别是void*类型和int类型,但是运行结果却与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的,这就是用NULL代替空指针在C++程序中的二义性。
为解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void*作为形参的函数版本。
NULL在C++中就是0,这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用。
问:引用和指针有什么区别?
答:在使用上,引用使用&符号,指针使用*符号。
3.2.4 引用和指针的关系
相同点:二者都是指一块区域,都可以对这个区域的值进行更改,具有更改值的类似的作用。
不同点:
引用必须初始化,指针可以不初始化
引用必须与一个确定的单元关联,不可以指向空的地方,但是指针可以指向空的地方
引用没有占用空间,也就是只是换了个名字,在内存里面找不到引用这个地方的位置,但是指针是实实在在的存在的,需要占用一定的空间。也可以把引用视为指针常量,在编译器优化后它不占内存
这个空间是指是否占用代码空间。
指针的大小确定,引用的大小根据所引用的类型所确定
引用的大小与类型有关,如int类型它也是int,char类型它也是char,但是指针大小是具体的,它只负责指路,大小与类型无关。
指针可以多级引用,但是指针不可以
引用只能指向一个对象,但是指针可以指向多个对象(指针指向的对象是可以发生变化的)
问:堆和栈有什么区别?哪个的空间较大?
答:堆上的空间是需要动态申请的,手动释放,而栈上的内存空间是静态申请,自动释放的
问:堆的空间大概有多大,我可以在堆上申请一个1G的内存吗?
答:(很不确定)应该不能吧,balabala,但是答案见下面,实际上可以
3.2.5 C++中堆和栈的区别
栈:是由编译器在需要时自动分配,不需要时自动清除的变量存储区。通常存放局部变量、函数参数等。
堆:是由new分配的内存块,由程序员释放(编译器不管),一般一个new与一个delete对应,一个new[]与一个delete[]对应。如果程序员没有释放掉,资源将由操作系统在程序结束后自动回收。
堆 | 栈 | |
---|---|---|
管理方式 | 堆中资源由程序员控制(容易产生memory leak) | 栈资源由编译器自动管理,无需手工控制 |
内存管理机制 | 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存 空间,另外系统会将多余的部分重新放入空闲链表中) | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了) |
空间大小 | 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大 | 栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置)。 |
碎片问题 | 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 | 对于栈,它是一个先进后出的队列,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别) |
生长方向 | 堆向上,向高地址方向增长。 | 栈向下,向低地址方向增长。 |
分配方式 | 堆都是动态分配(没有静态分配的堆) | 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 |
分配效率 | 堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。 | 栈是极其系统提供的数据结构,计算机在底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令。 |
堆是自低地址向高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。堆的大小受限于计算机系统中有效的虚拟内存。一般来讲在32位系统下,堆内存可以达到2.9G的大小。(除去1G的内核空间,几乎占满3G的用户空间)
3.3 操作系统
问:了解过虚拟内存吗?
答:(这个讲的比较好,在此不进行赘述了)
3.4 数据结构
问:了解过快速排序吗?介绍一下主要思想
答:快速排序是基于分治的思想,每次把哨兵元素都放在最终排序后的的位置上(然后给了一个序列,要求写出第一次快速排序之后的结果序列)
问:知道循环队列吗?说一下队列判空的条件?
答:Q.front == Q.rear
(当时听到问这个的时候特别自信,因为我复习过,所以说复习还是有用的)
3.5 思维题
问:现在给你一个无序的数组,要求求出所有数字中重复出现的最大次数是多少?
答:可以遍历整个数组,然后使用一个map存储每个数字出现的次数
问:如果现在给你加一个限制,要求不能使用额外的存储空间,该怎么处理?
答:可以先对数字进行从小到大排序,然后使用双指针,从第i个元素开始,向后更新j,直到j指向的元素不等于i所指向的元素,然后更新答案
4 手撕代码
- 这次真的是牛了,面试官从9:36发题,然后说10点截止,我好像没用10分钟就做出来了,而且代码一次就跑过,没有进行任何debug。
- 现场考代码的时候就是要保持头脑清醒,题目一般都不难,但是要注意一些边界情况。
给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回数组中连续 1 的最大个数。
示例 1:
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:[1,1,1,0,0,1,1,1,1,1,1],粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:
输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1],粗体数字从 0 翻转到 1,最长的子数组长度为 10。
1 |
|