r
南京夫子庙茶颜悦色店 摄于2023.8.27
大家好,我是飞宇。
今天分享一份校招面试面经问题和答案,希望对大家有所帮助!分享人是一位2024届的学弟,投递岗位为字节跳动抖音电商部门,考察语言主要为C/C++、Golang以及其余计算机基础知识等。
最近李伟老师讲了《C++ Primer 5th》这本书的视频,他是美国微软高级工程师,清华大学博士,帮忙推广一下,感兴趣的可以看看。
互联网圈有个梗就是"两大码农工厂:南华科、北北邮",就是说这两所高校的毕业生从事互联网工作的特别多,北邮虽然是211,但在互联网圈子里比很多985学校的牌子都好使,一些互联网公司甚至有北邮帮的说法,你的组长甚至leader搞不好都是你的往届学长。
以前就聊过不建议普通人随便去提前批随便面试这个话题:再次认真聊一聊,为什么我建议你不要随大流去参加提前批?,但北邮本硕的学历背景 + 2段大厂实习在手,肯定不是普通人了,这个含金量还是很够的,加上后台有人私信留言问有没有提前批上岸的经验,今天就来分享一下一位师弟的提前批上岸面经。
抖音电商面试一共是三轮技术面+一轮HR面,一共超过 40 个问题,我系统整理了一遍,并将其中一些比较不错的问题,摘录到了自己开发的网站上。
InterviewGuide大厂面试真题网站:https://top.interviewguide.cn
今天把师弟的三轮技术面经按照科目分享一下其中的编程语言和算法类的问题,计算机基础知识问题考察明天再发,因为篇幅太长了,一次发估计很多人都看不完,希望这些内容能对大家有帮助。
以下是这位师弟的面经以及部分参考答案:
大家好,应大佬邀请,我来分享一下自己的面经。
简单介绍一下自己的背景,北邮计算机本硕,目前研二在读,以前本科毕业后gab过一年,工作一年后又来读研了,目前B站+百度分别实习了三个月,我本来打算在B站一直实习下去的,但后来请教了秀哥,秀哥建议我换一家体量更大的公司继续实习,两段实习还是比一家强,后来就跑去百度实习了。
自己能在提前批拿到offer也没想到,原本以为自己是直接挂的,但没想到上岸了,最开始是不打算找提前批的,但秀哥建议我去试试,不要浪费自己的学历背景+实习经历,我就像被打了鸡血一样就去试了。
可能是踩了狗屎运,居然上岸了。
下面是我的面经,我是C++/Golang双语言技术栈,其实我最开始是学习C++的,后来在B站实习时接触到Go语言,后来就学了Go语言,变成双语言技术栈了。
1、问:看你又会C++又会Golang,哪门语言了解的多一些?
答:主要是C++,Go是在B站实习期间学习的,当时主要是为了做业务,很多细节都没怎么用心学,后面才慢慢补上的。
2、问:如何学习C++的?看过哪些书?
答:主要是《C++ Primer 5th》、《Effective C++》、《More Effective C++》、《深度探索C++对象模型》、《ST源码剖析》以及《C++并发编程实践》,主要是这几本书。
3、问:move语义了解吗?有什么作用?
答:move语义是一种高效的资源转移机制,实现移动语义,可以帮助我们避免不必要的拷贝操作,提高程序性能,显著提高效率。
阿秀补充:
1、std::move的使用场景
当需要将资源从一个对象转移到另一个对象时,可以使用std::move。例如,在容器中移动元素、在算法中交换数据等。需要注意的是,只有可移动的对象才能使用移动语义,否则可能导致未定义行为。
2、避免不必要的拷贝
使用移动语义可以避免不必要的拷贝操作,从而提高性能。例如,在复制一个大型对象时,如果使用移动语义,只需要进行一次内存分配和一次指针拷贝,而不需要进行多次拷贝操作。
3、以下是C++中move语义的一些关键点:
移动构造函数:std::move可以将一个左值转换为右值引用,从而实现资源的转移。例如:
class MyClass {
public:
MyClass(int x) : data(new int(x)) {}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
}
private:
int* data;
};
移动赋值运算符:std::move也可以用于将一个对象的资源转移到另一个对象。例如:
MyClass a(1);
MyClass b(std::move(a)); // 使用std::move实现移动赋值
4、问:说一下程序的内存分区?
答:从高地址和低地址分别向中间拓展,由上而下分别是堆、栈、自由存储区、全局/静态存储区、常量存储区和代码区。
阿秀补充
栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
堆:就是那些由 new
分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new
就要对应一个 delete
。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收
自由存储区:如果说堆是操作系统维护的一块内存,那么自由存储区就是C++中通过new和delete动态分配和释放对象的抽象概念。需要注意的是,自由存储区和堆比较像,但不等价。
全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0。
常量存储区:这是一块比较特殊的存储区,这里面存放的是常量,不允许修改
代码区:存放函数体的二进制代码
5、问:C++11中的新特性说说?
答:老八股文了balabalab
阿秀补充
C++11新特性主要有:
6、问:auto是怎么实现识别自动类型的?
答:不太清楚,回答了个“只知道是根据初始化表达式自动推导出变量的类型”的
阿秀补充
auto在C++11中是一种新的类型说明符,它可以根据初始化表达式自动推导出变量的类型。
auto的工作原理是:
例如:
cpp auto x = 5; // x是int类型
auto y = 1.5; // y是double类型
auto z = x + y; // z是double类型
std::vector<int>
vec; auto itr = vec.begin(); // itr是std::vector<int>::iterator类型
在上面例子中,编译器通过查看初始化表达式的类型,推导出auto变量的实际类型。
需要注意的是,auto只在声明时确定一次变量类型,之后变量类型不再改变,并且auto变量必须初始化,才能推导出类型。
7、问:模板是怎么实现转化成不同类型的?
答:不懂呜呜呜
阿秀补充
模板是一种通用的编程技术,可以用于实现泛型编程,即编写一个通用的类或函数,可以适用于多种不同的数据类型。
在C++中,模板是通过模板参数来实现的。模板参数可以是一个类型、一个整数或者一个枚举类型。在使用模板时,需要为每个要使用的类型提供一个对应的模板参数。
参考代码:
template
class MyClass {
public:
void setValue(T value) {
this->value = value;
}
T getValue() const {
return value;
}
private:
T value;
};
8、问:push_back和emplace_back的区别?
答:这个我知道,emplace_back通常在性能上优于push_back,可以避免不必要的复制或移动操作。
9、问:刚才你说看过C++并发编程实战,那写个题吧,很简单的,创建三个线程,依次打印1到100.
答:很常见的题,用thread就可以做,5分钟搞定!
这里阿秀补充一个无序版本的线程打印
#include
#include
using namespace std;
void print_nums(int start, int end) {
for (int i = start; i <= end; i++) {
cout << i << " ";
}
cout << endl;
}
int main() {
thread t1(print_nums, 1, 25); // 创建第一个线程,打印1到25
thread t2(print_nums, 26, 50); // 创建第二个线程,打印26到50
thread t3(print_nums, 51, 100); // 创建第三个线程,打印51到100
t1.join(); // 等待第一个线程执行完毕
t2.join(); // 等待第二个线程执行完毕
t3.join(); // 等待第三个线程执行完毕
return 0;
}
10、问:看你在B站实习用的是Go语言是吧?怎么学的?
答:我是直接看的github上的go语言极简入门,后来又看了《Go语言设计与实现》
11、问:问你一个简单的问题,你应该知道map是无序的,每次迭代map的顺序可能不同。如果需要按特定顺序遍历map,应该怎么做呢?
答:可以考虑借助切片来做,将键保存在切片中,然后把切片排序,最后根据排序后的顺序遍历map访问对应的值,这样可以达到目的。
阿秀补充
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{
"b": 2,
"a": 1,
"c": 3,
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
}
12、问:你刚才提到了排序是吧,一般用sort函数,其中的排序函数是什么排序?
答:内容os:这个我还真研究过。应该是快排、插入排序、堆排这三种,具体来说sort()源码中采用的是一种叫做IntroSort内省式排序的混合式排序算法,简单来说就是数据量达到一定规模就用快排或者堆排,没达到就用插入排序,但这个规模量我记不清了,大概是这个意思;快排和堆排的取舍则是根据最新递归深度的阈值来判断的。
阿秀补充
IntroSort内省式排序的混合式排序算法,
(为什么用插入排序?因为插入排序在面对“几近排序”的序列时,表现更好,而快排是通过递归实现的,会为了极小的子序列产生很多的递归调用在区间长度小的时候经常不如插入排序效率高)
第二步,如果说我们的元素规模大于16,那就需要去判断如果是不是能采用快速排序,怎么判断呢?快排是使用递归来实现的,如果说我们进行判断我们的递归深度有没有到达递归深度的限制阈值2*lg(n),如果递归深度没达到阈值就使用快速排序来进行排序
第三步,如果说大于我们的最深递归深度阈值的话,这个时候说明快排复杂度退化了(比如很不巧基准元素多次选取到了当前区间中最小或最大的元素。这种情况下,每次划分只能将区间缩小1个元素,造成递归深度过深),就会采用我们的堆排序,堆排序是可以保证稳定O(nlogn)的时间复杂度的。
13、问:如何移除切片中的数据?你有哪些方式?
答:切片的切片操作或者append都可以。
阿秀补充
append这个简单,切片的切片其实就是使用切片的切片操作将切片分为两部分,将想要移除的元素从中间移除即可,比如我想移除第三个元素:
package main
import "fmt"
func main() {
numbers := []int{1, 2, 3, 4, 5}
index := 2
numbers = append(numbers[:index], numbers[index+1:]...)
fmt.Println(numbers) // 输出: [1 2 4 5]
}
一共考察了4道题,一面2道,二面2道,三面没考,有剑指offer原题,也有力扣原题和变形题,都不算难,感觉刷力扣刷 500+ 以上都能顺利做出来。
盛最多水的容器:https://top.interviewguide.cn/issue/665
输出二叉树的左视图:https://top.interviewguide.cn/issue/697
链表判断有没有环:https://top.interviewguide.cn/issue/715
二叉树转双向链表:https://top.interviewguide.cn/issue/720
问:介绍下你的两段实习经历?
问:有什么收获?
—— EOF —— 你好,我是飞宇,本硕均于某中流985 CS就读,先后于百度搜索以及字节跳动电商等部门担任Linux C/C++后端研发工程师。
同时,我也是知乎博主@韩飞宇,日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。
我组建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起进群交流。
欢迎你添加我的微信,我拉你进技术交流群。此外,我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推机会。
加个微信,打开另一扇窗