字节跳动上海创新业务(2021届实习岗)三轮技术面总结

  博主于2020.4月初投了字节跳动 上海创新业务 后端开发实习岗Java方向),并且在2020.5初拿到了客户端的实习offer,下面分享一下此次技术面试的相关内容。

一、笔试(2020.4.12)

  按各位前辈的说法,字节跳动的内推一般是没有笔试的,但是我还是有。。。最后发现原来我是直接在字节跳动官网投递,内推是在字节跳动内推官网,我还是太年轻了。。。
  笔试我现在只记得一道题,总共有四道题,当时AC了三道。不过已经得了一个月,如果后面想起来了,再来补吧。

1、尽量使用优惠卷

题干:
  给你一个优惠卷数组、准备购买商品的价格数组,并且规定每件商品只能使用金额不大于自身价格的优惠卷(否则商场还得给你找零😂),优惠卷可以重复使用,但是一件商品只能使用一张优惠卷。现在要求你计算使用优惠卷后最小的总价

输入示例:
  每个测试用例的第一行优惠卷的数量第二行是各优惠卷金额第三行商品数量第四行是各件商品的价格

3
1 2 3
4
2 4 1 5

思路分析:
  水题一道,显然每件商品应该寻找≤自身价格最大的金额的优惠卷。以我阅题无数道行来看,这道题的关键是优化查找,不能每件商品都去搜一遍优惠卷数组,时间复杂度为O(m*n)(m、n分别为优惠卷商品价格数组的长度)。
  我们首先对两个数组进行升序排列快速排序堆排序归并排序随便撸一个,时间消耗O(nlogn + mlogm)。然后使用两个指针,分别从优惠卷、商品价格数组头部开始扫描。
在这里插入图片描述
  这要计算的理由比较简单,假设我们计算price[i]用了某张最适合的优惠卷coupon[j],那么计算price[i + 1]用的优惠卷的金额必然不会比coupon[j]小,因此只要后移ptr1指针往后找即可。所以时间复杂度在O(m+n)级别。
  那么排序+计算总价格的时间复杂度在O(nlogn + mlogm + m + n)级别。这道题测试数据还是比较多的,我最开始写的是希尔排序,时间复杂度与步长有关,一般认为时间复杂度为O(n^(1.3—2))级别,没有通过,后面修改为快排才AC。

2、其它不记得了。。。

二、第一轮面试(2020.4.21)

1、自我介绍下吧

姓甚名谁,籍贯啥的,就读于武汉理工大学 计算机科学与计算学院 计算机专业 本科
大一学了C++、Java
大二学了数据结构、算法,下学期在LeetCode上刷了半年的算法题
大三学了操作系统、数据库原理、计算机网络等专业核心课程
同时自己自学了Java web开发技术,SSM、SpringBoot框架,做了一个电商项目
看过分析MySQL、Redis、JVM实现原理的相关书籍

2、数据库中的事务了解么?有啥特性?

ACID,原子性、一致性、隔离性、持久性
可以特别提下一致性、隔离性的理解

3、MySQL数据库的隔离级别是什么?有几种?分别是什么?为什么要隔离?

如果多事务并发执行,不隔离,会产生问题
①、脏写。比如事务A将某条记录的属性修改为a,接着事务B修改为b,事务A再查看。
	发现自己修改的内容被其他事务修改覆盖了。
②、脏读。事务A读到其它事务未提交的事务,比如事务B修改某条记录的属性为a,
	事务A第一次查看是a,此时a是事务B未提交的内容
③、不可重复度。事务A读到其它事务的修改,比如事务A第一次某条记录属性的值为a,
	事务B将其修改为b,然后提交了,此时事务A再去查看,发现变成b了,即不可重复度
④、幻读。事务A读到其它事务的数据插入、删除,比如事务A第一次select * from table while a = 'xxx',
	假设数据库没有符合条件的记录,然后事务B插入一条满足a = 'xxx'条件的记录并提交事务,
	事务A再次查看,发现突然又查到了。。。就像幻觉一下
注意:不可重复度侧重数据修改,幻读侧重数据插入、删除

四种隔离级别:
①读未提交。解决了脏写,存在脏读、不可重复读、幻读问题,同一条记录
	不能同时被多个事务修改,行锁就行
②读已提交(不可重复度)。解决了脏写、脏读,存在不可重复读、幻读问题,
	此级别独到的都是提交了的事务B构成的数据集
③可重复度。解决了脏写、脏读、不可重复读,存在幻读问题
④串行化。解决了脏写、脏读、不可重复读、幻读,所有事务一个一个排好队执行,没有并发问题,可是太慢了

4、熟悉MySQL的存储引擎吗?

熟悉innodb,了解一点myism

结构上区别,innodb中的主键(若没有主键,则使用表中的唯一字段,若唯一字段也没有,
	自动生成一个唯一id字段标记每一条数据)使用了聚簇索引,索引即数据,数据即索引;
	myism中的使用非聚簇索引,可以没有主键,数据单独保存在一个文件,索引是索引,数据、是数据;
功能上主要区别:innodb支持事务,myism不支持事务

如果只有查询,可用myism,有插入(特别是并发插入)一定要用innodb

5、什么是聚簇索引,联合索引?

MySQL中的索引分成主键索引、二级索引两大类,也可划分为聚簇索引和非聚簇索引两大类

主键索引(若没有主键,则使用表中的唯一字段,若唯一字段也没有,自动生成一个唯一id字段标记每一条数据),此索引是聚簇索引
二级索引根据非主键字段生成的索引

聚簇索引的特点是将所有数据复制到当前B+树的叶子节点,

一般的二级索引是根据索引字段排序,叶子节点只有索引字段值、主键值,
查完二级索引需要根据得到的主键值去查主键索引(聚簇索引),即要查两颗B+树

若某个二级索引是聚簇索引,则其叶子节点包含所有字段信息,查找即可得到所有需要的数据,
此时再去查找主键索引(聚簇索引),即只要查一棵B+树,但是聚簇索引需要浪费存储空间,因为要复制一遍数据

那么联合索引处于两种极端之间,根据多个字段建立索引,叶子节点含有建立索引的多字段、主键值,
这时就会产生一个问题,查完联合索引是否要去查主键索引?这时看你select的字段是啥,
如果聚合索引的索引字段包括了你要查的字段(索引覆盖),此时不需要去查主键索引,
否则你仍然要根据联合索引叶子节点的主键值查主键索引,因此当你查找sql使用联合索引,尽量将
需要查找的字段设置为联合索引中的值(索引覆盖),可以减少查主键索引

6、select * frow table where 字段a < A and 字段b > B,假设它使用了联合索引<a, b>,大致查找过程什么?

联合索引对应的B+树根据a、b升序排列,存储引擎访问联合索引B+树,筛选字段a < A的记录
对应的主键值集合,筛选字段b > B的记录对应的主键值集合,然后取交集。不过也可能直接遍历
一一判断是否同时符合字段a < A and 字段b > B两个条件。

使用联合索引,需要考虑是否会发生索引覆盖,由于select * 所有字段,
一般的联合索引得到主键值集合,再去查找主键索引得到所有字段

7、MySQL执行sql的主要流程?

MySQL是基于C/S模式开发的数据库管理系统,客户端把sql传给服务器,服务器先要验证身份,
服务器得到sql,先查服务器中的缓存,查到了直接返回,然后对sql进行词法、语法、语义分析,
再接着进行sql优化,交给sql执行器,最终给到存储引擎,存储引擎查到数据返回给客户端。

(数据库这块,中间被面试官打断了两次,说深度可以了可以了。。。可我硬是给扯完了😂😂😂)
8、Redis字符串是如何存放的?

Redis自定义了sds数据结构用于存放字符串,该结构主要有len,buf(C格式的字符数组)、
capacity是buf字符数组的长度。由于该结构使用属性len来记录字符串的长度,所以是二进制安全,
因为即使字符串中含有'\0'字符(在C语言表示字符串结束),也不影响长度计算,当你获取sds字符串
的长度时,直接访问len属性,并不需要计算长度。

9、TCP/IP协议了解么?四层/七层模型是啥?

TCP/IP是一个协议簇,包括http、tcp、ip、rip等众多协议
四层是实际商业标准模型,分别是应用层、运输层、网际层、网络接口层,
七层是国际标准组织OSI提出的ISO参考模型,分别是应用层、会话层、表示层、运输层、网络层、数据链路层、物理层
在计算机网络学习中,一般划分为是应用层、运输层、网际层、数据链路层、物理层

划分层次,主要是解耦、模块化,各层只专注自己的功能实现,便宜开发、维护

10、TCP是可靠传输?如何实现的?

TCP是可靠传输,面向连接,UDP是不可靠传输,无连接。
TCP是可靠传输,主要是用于协议中设定了通信前要三次握手建立连接,通信完要四次挥手释放链接,
以及协议中设定了流量控制、拥塞避免、自动重传,滑动窗口算法

11、介绍一下TCP的滑动窗口

滑动窗口是一个发送端虚拟构建的一个控制发送流量的窗口,主要包括起始序号,窗口大小。
比如刚开始startIndex = 0,size = 300,表示发送端最多只能发送100个字节,假设
发送端发送的一个包起始序号为0,长度为50,起始序号为50,长度为100的两个包,如果
接收端接收到第一个包,则需要反馈发送端,确认序号ack=50,表示期待的下一个包的起始下标,
此时窗口更新为startIndex = 50, size = 300

如果接收端接收不过来了,可以反馈发送端减小滑动窗口的大小,亦或是发送端发现超时未收到
回复(可能是网络拥塞),会自动缩小滑动窗口的大小

12、三次握手是什么?为什么要进行三次?四次挥手是什么?为什么要进行四次?

三次握手是TCP协议规定的通信双方建立连接的过程,主要流程如下:
A发送SYN=1,seq=x给B,(喂,B你听得到我说话嘛?)
B回答SYN=1,ACK=1,seq=y,ack=x+1给A,(喂,我听得到,A你听到我说话嘛?)
接着A发ACK=1,seq=x+1,ack=y+1给B(B,我也听得到)

三次少一次都不行,A要确定B听得到自己说话(发包),B也要确定A听得到自己说话(发包)

四次挥手是TCP协议规定的通信双方释放连接的过程,主要流程如下:
A发送FIN=1,seq=x给B,(喂,B,我要关连接了)
B发送ACK=1,seq=y,ack=x+1(喂,A,我知道了)
(可能B还有数据要发送...发完数据后,告诉A,我也要释放连接了)
B发送FIN=1,seq=y+k给A,(喂,A,我也要关连接了)
A发送ACK=1,seq=x+m(喂,A,下次再见~)

同样四次挥手,一次都不能少,A发送的关连接请求FIN=1需要得到B的答复,
此时只能说明A没有数据要发了,但是B可能还有数据要发,所以发完数据后B也要
给A发送FIN=1关连接请求,并且得到A的回复确认,双方才能真正关连接

13、浏览器输入域名到看到页面这段时间用了哪些协议?主要干了什么事?

第一步:域名解析为IP地址。首先访问浏览器的域名缓存,没找到就去访问计算中的域名缓存、host文件,
再没找到就需要使用DNS协议(传输层用的UDP协议)访问本地域名解析服务器,如果本地域名解析服务器也不知道,
则需要访问根域名服务器、顶级域名服务器、二级域名服务器,直到找到域名对应的IP地址
(中间有包的转发,会用到RIP等路由选择协议,ARP协议,通过IP地址查MAC地址)

第二步:与服务器建立连接。(假设输入域名使用http或https协议访问),http/https协议是应用层协议,
它传输层用的是TCP协议,TCP协议在双方通信前,需要进行三次握手,建立连接

第三步:浏览器、服务器传输页面数据。

第四步:释放链接,TCP协议需要进行四次挥手释放连接。

14、写下三个线程轮流打印A、B、C

这道题我直接说不会,因为用Java写那种没有代码提示、自动导包的编辑器里写代码,实在是。。。
其实这种控制线程执行顺序的题是比较简单的,我们设置一个变量m,
m % 3 == 0时,线程1打印A
m % 3 == 1时,线程2打印B
m % 3 == 2时,线程3打印C
每次一个线程打印后m += 1
三个线程需要公用同一个锁对象,每次判断m前需要获取锁对象。

三面的时候,又碰到这道题,躲肯定是躲不过去的。。。

15、了解设计模式么?Java GUI用了哪些设计模式?

当时并没有阅读过相关书籍(正在补。。。),只知道观察者模式、单例模式、适配器模式。
顺带把单例模式的懒汉式、饿汉式两种实现方式说了一下。

他这里问GUI,是由于我简历写一个项目用了它,我当时只说了适配器模式(GUI中有很多事件适配器)。。。

16、单例模式你知道几种实现方式?

单例模式的饿汉式,设置一个静态属性,并且new实例,将构造方法申明为私有,由于类
一般加载一次,所以在jvm加载该类就创建了一个实例,以后直接访问这个静态变量即可
class MyClass{
    // 类加载的时候,直接生成一个实例
    public static MyClass single = new MyClass();
    //构造器私有
    private MyClass(){}
}

单例模式的懒汉式,编写一个静态方法,第一次调用的时候调用构造方法,后面再次调用直接返回
之前创建的实例,此种方法存在线程同步问题,需要使用synchronized关键修饰(静态方法,将使用class锁)
class MyClass{
    // 指向唯一生成的实例
    public static MyClass single = null;
    //构造器私有
    private MyClass(){}
    // synchronized修饰静态方法,使用的是class锁
    public static synchronized MyClass getInstance() {
        if (single == null) {
            // 第一次需要生成一个实例
            single = new MyClass();
        }
        return single;
    }
}

17、Java中的HashMap容器是怎么实现?

通过维护一个table数组(hash桶数组),每次放入一个key-value,根据key计算得到一个
hash值,然后用余数法,散列到table[hash % 数组长度]的位置

hash冲突问题,存在多个key计算hash散列到同一个table[index](hash桶),此时使用
链表将key-value串起来,并且在JDK 1.8的时候,引入了红黑树来解决链表过长降低效率的问题。
(不好意思,前面分析过JDK 1.8 HashMap容器的源码,还写了博客https://hestyle.blog.csdn.net/article/details/105723602

18、ConcurrentHashMap有什么特点?实现原理?

ConcurrentHashMap支持多线程并发读写,在JDK 1.8之前的版本存在段,
多线程读写时分段进行上锁,与hashtable锁整个容器的相比,大大提高了并发读写效率。
在JDK 1.8的时候,移除了段,将锁的粒度缩小为table[index]即hash桶,
并且尽可能减少synchronized同步代码块中的代码,进一步提高了并发读写效率

(不好意思,前面分析过JDK 1.8 ConcurrentHashMap容器的源码,还写了博客
https://hestyle.blog.csdn.net/article/details/105723602

19、JVM中为啥要分代回收?有哪几种内存回收算法?

因为根据实际运行状态发现,大部分对象用到的时间很断,多有少部分对象会经常使用,
因此我们需要将那些用几次就不用的对象回收,(二八原则貌似啥地方都可以套。。。)

内存回收算法主要有标记-清除算法,复制算法、标记-整理算法
标记-清除算法,直接标记需要回收的对象,然后清除,会产生大量的碎片,(回收的小空间不是连续的)
复制算法,将空间分成两部分,每次只用其中一块,回收时,将存活的对象复制另外一块。(减少了可使用空间)
标记-整理算法,标记需要回收的对象,然后将存活的对象复制另一端。(耗时稍长一点)

在这里插入图片描述
20、给你一个单链表,翻转[m, n]区间的节点,程序实现一下吧

这道题显然是一道水题,不过在这一面是第一次手撕代码,有点紧张,思路有没有分析,
上来就写代码,写了10几分钟还是没调试成功。。。

比如输入链表1->10->5->20->30->8->90,要求翻转2-4这一段,
翻转后的链表1->30->5->10->30->8->90

需要分成三段[1, m) [m, n],(n, len]
我当时确实是分三段,写复杂的地方在于构造新链表的时候一个个的调整指针,并且设置一大堆临时变量。。。
其实[1, m)(n, len]两段是串接好的,不需要要修改,只要把[m. n]中间段翻转即可
蠢哭。。。

三、第二轮面试(2020.4.24)

1、前面面试感受如何?

总的来说就是理论部分答得还行,但是手撕代码部分准备不充分,有点自乱阵脚。

2、学过哪些课程?数据结构、数据库原理、计算机网络学过吗?
3、学过数据库原理相关的课程吧?第三范式是啥?

第一范式(1NF):表结构,并且表中所有字段都是不可拆分。
第二范式(2NF):满足1NF,表中任意非主属性对码(候选码)是完全函数依赖。
第三范式(3NF):满足2NF,表中任意非主属性对码(候选码)不存在传递依赖
BC范式(BCNF):满足3NF,表中任意主属性对码(候选码)不存在传递依赖
(其实这里我不记得,不过面试官引导我从第一范式开始,最终还是答出来了🤦‍♂️捂脸)

可以看出四个范式约束是越来越严格,一个表中的字段与码的关系越来越紧。
如果设计数据库时不考虑任何范式,也就是表中的字段任意放置,可能会造成插入删除、删除异常、更新异常、数据冗余。
比如某张表(学号(主键),姓名,学生学院名,院系主任,课程号,课程名,分数,课程所属院系名)
显然
①、数据冗余。学生每一条选课信息都要与学生所属院系、课程所属院系等信息绑定
②、插入异常。如果需要插入某个院系暂时没有学生,我要插入一个院系、院系主任,这显然存在插入异常,
	因为该记录学号(主键)没有值,但是主键不能为null
③、删除异常。假设我要删除某个院系的所有学生,但是保留院系名称、院系主任,
	在这张表显然是做不到的,因为删除所有学生,则院系信息必然也删除了
④、更新异常。如果我要更新某个学生所属院系,由于一个学生肯定不止一条选课记录,
	所以我们需要修改多条记录,但显然只是想改一条记录。
因此我们需要根据范式约束,将表垂直拆分为学生信息表、院系表、院系课程表、学生选课表

但是范式约束越强的话,就要分更多的表,本来有些只需要访问一张表的操作,现在
需要访问多张表,降低了效率,所以需要根据自己的业务需求进行平衡。

4、某个用户表,如何查出去重后的名字?如何查出出现多次的名字?

# MySQL中有一个去重关键字distinct,只要修饰name字段即可插入去重的名字
SELECT DISTINCT(name) 
FROM table_name

# 根据名字分组,使用having分组条件约束出现大于1的名字
SELECT name 
FROM table_name
GROUP BY name
HAVING COUNT(name) > 1

5、接触过Linux吗?用过哪些命令?

文件相关cd、mp、mv、tar、vim相关
ps、kill等
只会常见的命令🤦‍♂️

6、如何查看磁盘使用率?内存使用率?

Ubuntu上有一个top密令,会列出当前系统的内存、磁盘、进程等动态信息

7、一个单核CPU能否进行多进程任务?

可以在宏观上做到并发,但是微观上其实还是串行

也就是通过操作系统进行多进程的调度,将时间划片,分给需要执行的进程,
这样在宏观上看起来是多个进程在同时执行,但是只能称为并发,真正的并行需要多核或者多CPU

8、给你一个数组(元素不重复、无负数),如何找出最长的连续序列,请程序实现

最开始的思路是先排序,再扫描升序序列,这样连续的升序序列很容易计算。
不过时间复杂度在O(nlogn + n)级别

不过面试官提示内存不限制,优化程序,降低时间复杂度。我们可以从排序方式下手,
nlogn级别已经是较高效的了,但是还漏了计数排序,将元素值与数组下标关联,从而
将排序时间复杂度将为O(n),但是需要额外的空间(与数组元素最大值有关)

计数排序就不说了,写下最长的连续序列查找,nums[i] = 0表示i不再给定数组中
int maxLength = 0, startNum = 0;
for (int i = 0; i < length; ++i) {
	// 找到下一个在数组的元素
	while (i < length && nums[i] != 1) {
		++i;
	}
	// 计算以i为起始的连续序列长度
	int begin = i;
	while (i < length && nums[i] == 1) {
		++i;
	}
	// 更新最长的连续序列长度
	if (i - begin > maxLength) {
		maxLength = i - begin;
		startNum = begin;
	}
}

四、第三轮面试(2020.4.29)

1、自我介绍
2、Java进行线程同步的方式

synchronized与ReentrantLock
synchronized是关键字,可修饰方式、代码块
分为同步代码块(手动指定锁对象)、同步非静态方法(默认为this对象锁)、同步静态方法(默认为class对象锁)
ReentrantLock是Lock实现类,需要手动获取、释放锁资源。

3、写下两个线程轮流打印1、2、3、4…程序

一面没答,三面还是碰上了。。。
其实也不难,要用一个锁,一个计数变量,两个线程死循环获取这个锁资源,满足条件则打印计数变量
获取锁资源后,再判断计数变量的奇偶性,如果是线程1获得锁,并且计数变量是偶数,则线程1打印计数变量,然后释放锁
如果是线程2获得锁,并且计数变量是奇数,则线程1打印计数变量,然后释放锁
其它情况获取锁资源的资源直接释放锁,
最后再加个while死循环即可

4、Redis有几种对象类型?

字符串(sds)
列表(zipList或linkedList)
hash(zipList或hashtable)
集合(intset获取hashtable)
有序集合(zipList或skipList+hashtable)

5、有序集合底层数据结构什么?

有序集合有两种数据结构,
一种是zipList,当集合中元素较少,直接开辟一个连续的内存存放
另外一种是skipList+hashtable,跳跃表保持元素递增,Hashtable用于O(1)级别查找元素

6、Redis持久化是如何进行的?有几种方式?区别是什么?

一共有两种持久化方式,
RDB,将当前数据库的数据(状态)完整写入到一个文件中,恢复时直接读入内存即可
AOF,将数据库执行过的命令记录在文件中,恢复时直接将操作重新执行一遍。由于可能存在
	操作膨胀的状况(比如返回插入、删除),所以对操作需要进行压缩(直接压缩为一条插入)
RDB存储数据,恢复直接读取到内存,效率较高,AOF记录数据库执行过的命令,恢复时,
数据库需要重新执行一遍所有操作,效率较低。

7、Redis淘汰缓存的策略?

一共有三种淘汰策略
第一种,使用定时器,到时自动销毁(非常耗资源,加入数据库有1万个key-value,就来1万个定时器?)
第二种,默认不做过期处理,当查询数据的时候判断是否过期,过期就报错,否则返回数据
第三种,使用一个专门的线程检查各个key-value是否过期,过期就删除,并且在查询的时候
	再判断是否过期,过期就报错,否则返回数据(前两种方式的结合)

8、LRU知道么?如何实现?查找、插入、删除时间复杂度是多少?

LRU是Least Recently Used的缩写,即最近最少使用
实现可用一个双链表,每次插入元素放入头部,访问元素也将其移动到头部,每次淘汰都是淘汰
尾部的节点。因此插入、删除时间复杂度为O(1),查找时间复杂度为O(n)

其实同时可以用一个hashmap,将查找时间复杂度降为O(1)

与之对应的还有一个LFU,即最少使用频率淘汰,之前刷题刷到过
https://blog.csdn.net/qq_41855420/article/details/88909721

9、手撕快排

快排就不用多讲吧,但是手撕代码的那个在线编辑界面,不能单步调试。。。

快排的思路是每次只归位一个元素,并且将比它小的元素,比它大的元素区分开。
假设要排序的段为nums[left, right],我们把nums[left]归位,比nums[left]小的
放在nums[left]前面,比如nums[left]大的放在它后面,假设归位后nums[left]下标为mid
则归位后顺序如下 nums[left ... mid - 1,mid, mid + 1 .. right]
注意这里的nums[mid]是未调整前的nums[left]
然后递归处理nums[left, mid - 1],[mid + 1, right]

五、总结

1、算法、数据结构是重中之重
2、专业核心课程数据库原理、计算机网络、操作系统也很重要
3、手撕代码很重要,不过思路更重要,先想好思路再写代码
4、即使程序不能正常执行、没有单步调试的环境,也要采取简单的调试方式,比如某段打印标记、打印某个变量的值。这其实是在考你在遇到bug的时候,如何解决问题
5、要自己掌握主动权,尽量往你熟悉的点引

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章