图解学习网站:
大家好,我是小林。
26届暑期实习进展到现在,也算到末尾了,但是并不是完全没机会,我一直鼓励大家即使感觉是末尾阶段,也不要开摆直接躺平。
相反这个时候大部分同学在观念上已经没机会,于是行动就扼杀在了自己的想法中,而持续坚持投递,不下牌座的同学一定是少数人,往往这时候更容易捡漏,我见过太多坚持到最后的同学才拿到满意的 offer。
校招实习这件事,能积累实习是能加分,没有也不一定是减分的,没有实习的话,可以从项目经历上入手,比如大多数同学只准备1-2个项目,那你直接准备3个,甚至4个项目,增加的简历的丰富度,来提升你的项目开发经验,也是能从简历池中突围的。
也有同学问了,简历写那么多项目,超过1页怎么办?
其实现在投递简历都是线上为主了,所以你简历2页也是没问题的,只要2页简历90%以上的内容是跟开发技术相关的就没问题,而如果非技术相关的校园经历,如果内容占比50%,这种就不行的,因为这一块经历对于开发岗来说,没什么价值,不容易引起面试官提问的经历,就没办法在面试中发挥出来。
正好,最近看到腾讯26届暑期补录了,还有 1000 个实习offer 虚位以待。
其中技术类的待发占比是最高的,像后台前端开发、移动客户端开发、PC客户端开发、测试开发这些技术类岗位依然有机会。
看到机会,冲就完了!
再来聊聊腾讯的面试风格,针对后端开发,在校生是学Java为主,腾讯大部分后台开发还是以CPP+Go为主,所以如果部门不是用Java的,可能面试官对Java也不是特别熟悉,这样他就会少问Java,而是去问计算机基础+后端组建+算法这些通用的问题了。
这次就分享腾讯校招后台开发面经,部门是用C/CPP为主,面试官就是属于对Java不熟悉的,就问了一些基本问题,然后就手撕算法,问的问题相对较少,同学感觉要寄了。
腾讯(一面)多态是什么?多态的实现原理?
多态指父类引用可以指向子类对象,并在运行时根据实际对象类型调用相应方法。
Java 通过以下两种方式实现多态:方法重写和接口实现。
方法重写:子类重写父类的方法,使得相同方法名在不同子类中有不同实现。
classAnimal{
publicvoidsound{
System.out.println("动物发出声音");
}
}
classDogextendsAnimal{
@Override
publicvoidsound{
System.out.println("汪汪汪");
}
}
classCatextendsAnimal{
@Override
publicvoidsound{
System.out.println("喵喵喵");
}
}
调用例子:
Animal dog = newDog;
Animal cat = newCat;
dog.sound; // 输出:汪汪汪
cat.sound; // 输出:喵喵喵
接口实现:不同类实现同一接口,通过接口引用调用实现类的方法。
interfaceShape{
doublearea;
}
classCircleimplementsShape{
privatedoubleradius;
publicCircle(doubleradius){ this.radius = radius; }
@Override
publicdoublearea{ returnMath.PI * radius * radius; }
}
classRectangleimplementsShape{
privatedoublewidth, height;
publicRectangle(doublewidth, doubleheight){
this.width = width;
this.height = height;
}
@Override
publicdoublearea{ returnwidth * height; }
}
调用例子:
Shape circle = newCircle(5);
Shape rectangle = newRectangle(3, 4);
System.out.println(circle.area); // 输出:78.5398...
System.out.println(rectangle.area); // 输出:12.0
Java 多态的核心在于动态绑定机制,其实现依赖于以下两个关键组件:
方法表:每个类在加载时,JVM 会为其创建一个方法表(虚拟方法表),方法表中存储了类的所有虚方法(可被重写的方法)及其实际实现的内存地址,子类方法表会继承父类的方法表,并替换被重写的方法的地址。
动态绑定:当通过父类引用调用方法时,JVM 会在编译阶段,检查父类是否存在该方法(静态类型检查),在运行阶段,通过对象的实际类型找到对应的方法表,并调用其中的方法。
总的来说,Java 多态通过方法重写和动态绑定实现了 "同一接口,多种实现" 的设计理念,使得代码更具灵活性和可维护性。其核心机制是 JVM 在运行时通过方法表动态解析方法调用,确保正确执行子类的重写方法。
进程和线程区别是什么?
本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
稳定性方面:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不会影响其他进程。
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线
MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B+Tree 如图所示:
比如,我们执行了下面这条查询语句:
select* fromproduct whereid= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);
在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
tcp四次挥手过程?为什么需要四次?
具体过程:
客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:
如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,
从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送,因此需要四次挥手。
hashmap扩容过程是怎样的?
hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤:
第1步是对哈希表长度的扩展(2倍)
第2步是将旧哈希表中的数据放到新的哈希表中。
因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
如我们从16扩展为32时,具体的变化如下所示:
因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
智力题
问题:一个装满水10升容量瓶子,一个7升空瓶子,一个3升空瓶子,平分为两瓶5升水。
算法
最长递增子序列