各大公司面试题分类整理

用户头像
吕小毛
关注
发布于: 2020 年 05 月 06 日
各大公司面试题分类整理

前言

本人小白一枚,从18年入职工作到现在,工资不高,所在业务受重视程度更低。所以想趁着在家办公的时间,看看外面的世界。下面对面试过程中的问题进行分类汇总,这些问题的答案有个人认知、有参考他人的观点,也有一些直接引用别人的文章。本文给出的答案只是一个引子,如果想要深入探究还需要各位通过其他渠道进行详细了解。



由于本人知识有限,答案不免有不足或者错误。还望各位犀利指出,小白一定积极改正,进行勘误。





欢迎阅读原文:https://juejin.im/post/5e7d71296fb9a03c875c806e



面试题汇总

工作相关篇

Q1: 自己所做过的项目中难点以及如何解决的,介绍系统的业务架构和技术架构

这是一个必问题,所以需要提前准备好项目中的难点以及自己的解决方案。如果是自己遇到并解决的最好,如果自己没有遇到过,那么把项目中其他人解决的难点融会贯通也可。总之:要有亮点,而且自己是深入思考和深入理解的。



Q2: 服务突然RT增大,后续又恢复了,如何定位

  1. 可能是网络抖动,导致接口短暂超时

  2. gc影响

Java篇

Java基础

Q1: Java环境变量classpath和path的区别

  1. path环境变量:系统用来指定可执行文件的完整路径,即使不在path中设置JDK的路径也可执行JAVA文件,但必须把完整的路径写出来

  2. classpath环境变量:java查找类的路径



Q2: 重写和重载的区别,构造函数可以重写吗

class Person {
String name;
public Person() {}
public Person(String name) {this.name = name;}
public void talk() {System.out.println("person talk");}
}
class Student extends Person {
String stuNumber;
public Student() {}
public Student(String name, String stuNumber) {
super(name);
this.stuNumber = stuNumber;
}
@Override
public void talk() {System.out.println("student talk");}
}

重写:需要有继承关系,子类重写父类的方法。一般使用@Override注解标识,不标识也无所谓。上面代码中Student类就重写了Person类的talk方法。



重载:函数名相同,参数个数不同或者参数类型不同。注意方法返回值不同是不算重载的。上面代码中对构造函数就是通过参数个数不同进行重载。



构造函数不能被重写,因为重写要求方法名一致。而构造函数的方法名就是类名。子类不可能和父类同名,所以也不可能有相同的构造函数。所以构造函数不能重写,但是可以重载。



Q3: 介绍一些常用的Java工具命令

集合

Q1: HashMap实现原理



请参考:https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/



Q2: HashMap为什么线程不安全



请参考:https://blog.csdn.net/weixin_43092168/article/details/89791106



Q3: currentHashMap如何保证线程安全



请参考:https://baijiahao.baidu.com/s?id=1617089947709260129&wfr=spider&for=pc



Q4: Java容器有哪些?哪些是同步容器,哪些是并发容器?



详细可参考:https://blog.csdn.net/qq_20499001/article/details/89031480?fps=1&locationNum=2



Java容器有Map、List、Set



Java中同步容器分为2类:

  1. Vector/Stack/HashTable,其方法都是同步方法,使用synchronized修饰

  2. Collections 类中提供的静态工厂方法创建的类(由 Collections.synchronizedXxxx 等方法)



Java中的并发容器:

JDK 的 java.util.concurrent 包(即 juc)中提供了几个非常有用的并发容器。

  1. CopyOnWriteArrayList - 线程安全的 ArrayList

  2. CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。

  3. ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。

  4. ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。

  5. ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。

  6. ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。

  7. ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。

  8. ArrayBlockingQueue - 数组实现的阻塞队列。

  9. LinkedBlockingQueue - 链表实现的阻塞队列。

  10. LinkedBlockingDeque - 双向链表实现的双端阻塞队列。



线程

Q1: 如何让线程A在线程B执行之后再执行

  1. CountDownLatch。线程A中 latch.await(),线程B中latch.countDown()

  2. wait()、notify()。可能存在线程B的notify()先执行,导致线程A一直处于阻塞状态



Q2: ThreadLocal的理解和适用场景



请参考:https://www.cnblogs.com/fsmly/p/11020641.html

Thread类里面有2个变量,threadLocals和inheritableThreadLocals,类型均为ThreadLocalMap。

为什么Thread类使用map,而不是直接一个value的存储( T value),如果是一个T value的话,这个值就是多个线程共享的,会出现问题。ThreadLocal就是为了解决该问题而来的



使用ThreadLocal注意事项:

ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的value不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。



Q3: 一个线程的生命周期有哪几种状态?它们之间如何流转的



请参考:https://www.cnblogs.com/sunddenly/p/4106562.html

Q4: 线程池提交流程



Q5: 线程池中任务队列已满,如何处理



  • AbortPolicy:直接抛出RejectedExecutionException异常。是Executors里面默认线程池的默认处理策略

  • DiscardPolicy:do nothing

  • DiscardOldestPolicy:抛弃最老的任务,执行新的

  • CallerRunsPolicy:调用线程执行



Q6: 保证线程安全的方式



  • automic:使用提供的原子类

  • syntronized:同步代码块

  • lock:锁

  • volatile:保证可见性

Q1: 同步方法的锁是类还是对象

同步方法默认用this或者当前类class对象作为锁;

同步代码块是通过传入的对象作为锁。



Q2: 同步方法A调用同步方法B,能进入嘛

可以,因为synchronized是可重入锁



Q3: synchonized和ReentrantLock的区别

不同点

  1. synchonized是java的关键字;ReentrantLock是类

  2. synchonized是非公平锁;ReentrantLock默认是非公平锁,但是有公平锁和非公平锁两种实现方式。

  3. synchonized可用在同步代码块、同步方法上;ReentrantLock的使用方式需要lock()unlock()



相同点

  1. 两者都是可重入锁

  2. 都是同步阻塞方式



Q4: 什么是活锁、饥饿、无锁、死锁?怎么检测一个线程是否拥有锁



死锁:

是指两个或两个以上的进程(或线程) 在执行过程中,因 争夺资源而造成的一种互相等待 的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。



死锁的条件:

  1. 互斥: 线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。



  1. 请求和保持: 线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。



  1. 不可剥夺: 线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。



  1. 循环等待



活锁

是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。



举个例子:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。



饥饿

是指如果线程T1占用了资源R,线程T2又请求封锁R,于是T2等待。T3也请求资源R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......,T2可能永远等待。



不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿,或者优先级低的线程容易产生饥饿



无锁:

无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS 原理及应用即是无锁的实现。



检测线程是否有锁

Thread.holdsLock(Object obj):当且仅当 当前线程拥有obj对象锁的时候,返回true

该方法用例断言打当前线程是否持有锁。assert Thread.holdsLock(obj);



Q5: synchronized实现原理



Q6: java中悲观锁、乐观锁的例子

  • Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

  • java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的



jvm

Q1: 介绍一些了解的JVM参数



Q2: 为什么 Java 要采用垃圾回收机制,而不采用 C/C++的显式内存管理



在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象 的内存时,该内存便成为垃圾。 垃圾回收能自动释放内存空间,减轻编程的负担,JVM的一个系统级线程会自动释放该内存块。释放无用内存,避免内存泄漏



Java禁止显示内存回收,jvm决定回收时机,而且避免开发人员忘记释放内存的问题



Q3: JVM内存模型

Q4: JVM内存分配策略



  1. 对象优先分配在Eden区,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。垃圾收集期间虚拟机对象全部无法放入Survivor空间,通过分配担保机制提前转移到老年代去。

  2. 大对象直接进入老年代

  3. 长期存活的对象进入老年代,可以通过-XX:MaxTenuringThreshold设置年龄

  4. 动态对象年龄判定。为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄



Q5: 有哪些垃圾回收算法

  • 标记-清除:会产生内存碎片

  • 标记-复制:年轻代采用该算法进行垃圾收集

  • 标记-整理:让所有存活的对象都向内存空间一端移动,延迟增大



类加载



关于类加载器看这一篇文章就够了。深入理解Java类加载机制。但是这篇文章最后有一个错误:下面图片中此时的counter2=1应该是counter2=0



Q1: classLoader和Class.forName()的区别



java中class.forName()和classLoader都可用来对类进行加载。

class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。

而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象

MySQL篇

Q1: MySQL存在哪些索引类型

  1. 唯一索引

  2. 全文索引

  3. 联合索引

  4. 普通索引



Q2: InnoDB为什么采用B+树的索引结构



请参考腾讯技术工程的文章:深入理解 Mysql 索引底层原理



Q3: 介绍聚簇索引、非聚簇索引、索引覆盖



请参考文章:mysql聚簇索引和非聚簇索引



Q4: 如何提高SQL性能,工作中SQL优化经验



提高SQL的性能我认为一定要让MySQL选择正确的索引。



在工作中,小白曾优化过系统中的SQL。其体现主要表现在以下几个方面(这只是小白在工作中遇到的,跟各位遇到的应该会有不同哦):

  1. MySQL错误选择索引

  2. 字段类型错误导致没走索引

  3. 本可使用索引覆盖的SQL语句,却回表查了多余数据



Q5: MySQL数据库隔离级别



请参考:Mysql数据库隔离级别



Spring篇

Q1: 介绍Spring IOC和AOP



请参考:深入理解Spring两大特性:IoC和AOP



IOC:控制反转。IOC之前对象A依赖对象B时,需要A主动去通过new创建B的实例,控制权是在对象A上。IOC就是将对象的控制权交给IOC容器处理。



AOP:面向切面编程(AOP)就是纵向的编程。比如业务A和业务B现在需要一个相同的操作,传统方法我们可能需要在A、B中都加入相关操作代码,而应用AOP就可以只写一遍代码,A、B共用这段代码。并且,当A、B需要增加新的操作时,可以在不改动原代码的情况下,灵活添加新的业务逻辑实现。AOP主要一般应用于签名验签、参数校验、日志记录、事务控制、权限控制、性能统计、异常处理等



Q2: AOP如何实现



Spring通过动态代理实现AOP。



请参考:从代理机制到Spring AOP



Q3: Spring如何实现事务管理



请参考:可能是最漂亮的Spring事务管理详解



Q4: Spring事务传播机制



请参考:Spring事务传播行为详解



Redis篇

Q1: redis如何实现分布式锁



使用setnx key value实现,并使用expire key 设置超时时间。



这种方式存在的问题:这2步操作由于不是一个事务,所以可能出现设置超时时间失败的问题。如果超时时间设置失败则会导致该key永不过期,占用内存。



解决方式:

  1. 可使用lua脚本自己编写,使之变成一个原子性操作

  2. redis提供了SET key value [EX seconds] [PX milliseconds] [NX|XX]



Q2: redis有支持几种数据结构



String、List、Set、Zset、Map、GEO、Bitmap



Q3: 字符串数据结构底层实现



Redis的字符串底层由SDS简单动态字符串实现。



特性

  1. 空间预分配:当修改字符串并需要空间扩展时,分为以下2种情况:

  2. 扩展之后空间小于1M,则预分配与已使用空间一样大小的空间(已使用空间包含扩展之后的字符串)

  3. 扩展之后空间大于1M,则直接分配1M

  4. 惰性释放:为了避免释放之后再扩展的问题,redis采用了惰性释放策略,使用free字段来记录未使用的长度,等待使用。同时也提供了相关的API再有需要时释放空间。



Q4: Map数据结构底层实现



Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。



字典数据结构中是一个哈希表数组,共有2个哈希表。在扩容时使用。



Redis的hash算法使用MurmurHash2算法。



Q5: Redis过期键删除策略



定时删除: 在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。

优点: 内存友好,可以实时释放内存

缺点: 对CPU不友好,为了删除过期键,大量占用CPU



惰性删除: 放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键

优点: 对CPU友好,只有使用到的时候才判断

缺点: 对内存不友好

实现: 通过expireIfNeeded函数,读写redis命令之前都会调用该函数,判断是否需要过期该键



定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定

优点: 兼顾内存和CPU

实现: Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用。函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。



Q6: redis的淘汰策略



  • noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)

  • allkeys-lru:从所有key中使用LRU算法进行淘汰

  • volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰

  • allkeys-random:从所有key中随机淘汰数据

  • volatile-random:从设置了过期时间的key中随机淘汰

  • volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰



Q7: zset的底层实现



跳跃表。可参考通俗易懂的Redis数据结构基础教程]



Kafka篇

Q1: Kafka如何保证高性能和可靠性



请参考:Kafka 数据可靠性深度解读



画外音:该文章需要细细阅读,理解每一块内容。只要明白了该文章里所说内容,应对面试中的Kafka面试题应该不成问题。



Q2: Kafka支持事务嘛



请参考:Kafka 设计解析(八):Kafka 事务机制与 Exactly Once 语义实现原理



Q3: Kafka中zookeeper的作用

  • 再平衡消费者

  • 选主

编程题

基本都是Leetcode的中等难度的题目,各位小伙伴可以刷起来了



Q1: 求A/B,不能使用除法



详细讲解请看:两数相除

public int divide(int dividend, int divisor) {
if (dividend == 0) {
return 0;
}
if (dividend == Integer.MINVALUE && divisor == -1) {
return Integer.MAXVALUE;
}
int flag = -1;
if ((dividend > 0 && divisor > 0) || (dividend < 0 && divisor < 0)) {
flag = 1;
}
long a = Math.abs((long) dividend);
long b = Math.abs((long) divisor);
return flag * getRes(a, b);
}
private int getRes(long a, long b) {
if (a < b) {
return 0;
}
int count = 1;
long tmp = b;
while (a > b << 1) {
b = b << 1;
count = count << 1;
}
return count + getRes(a - b, tmp);
}



Q2: 给定包含n个数字的数组,将这些数字进行拼接,求拼接成的最大数值



Q3: 链表中找到倒数第K个节点



Q4: 买卖股票的最佳时机



为了找到最大利润,我们需要找到最小的买入价格。假设nums[i]是最低的买入价格,nums[j]是最高的买入价格。当满足i < j时的最大利润即为nums[j] - nums[i]



详细描述请参考:121. 买卖股票的最佳时机

public int maxProfit(int[] prices) {
int len = prices.length;
if (len <= 1) {
return 0;
}
// 存储最小的买入价格
int minBuyPrice = prices[0];
// 存储最高的卖出价格
int maxSellPrice = 0;
for (int i = 1; i < len; i++) {
// 计算最高的卖出价格:当前最高卖出价格与当天的卖出价格对比。其中price[i]-minBuyPrice就是当天的卖出价格
maxSellPrice = Math.max(maxSellPrice, prices[i] - minBuyPrice);
// 更新最小的买入价格
minBuyPrice = Math.min(minBuyPrice, prices[i]);
}
return maxSellPrice;
}



Q5: 复制带随机指针的链表



Q6: 消除816。例如原字符串12881616,最终返回12。12881616 -> 12816 -> 12

提示:使用栈数据结构



Q7: 数组中奇数、偶数数量相同,交换内容最终使得奇数下标存储奇数,偶数下标存储偶数



Q8: leetcode 154题



链接:154. 寻找旋转排序数组中的最小值II



解题思路,欢迎查阅小白的个人博客:154. 寻找旋转排序数组中的最小值II



Q9: 单链表结构,每个节点表示0-9的数字。给定2个链表,进行求和。例如 9->9和2->1。求和结果是1->2->2



三种思路,请参考:Leetcode 445. 两数相加 II



Q10:实现一个LRU数据结构,插入和查询要求$O(1)$时间复杂度

  1. 可以使用LinkedHashMap直接实现。

  2. 使用一个LinkedList存储key的顺序吗,到达O(1)的插入,使用map存储k-v映射关系,达到O(1)的查询复杂度

public class Main {
// 使用map存储结构中已有的key,便于O(1)的查询复杂度
HashMap<String, Integer> map = new HashMap<>();
// 存储key的顺序,对于最近访问的key,移动到队列的头部
LinkedList<String> lruKeys = new LinkedList<>();
// LRU结构的大小
int size = 4;
public static void main(String[] args) {
Main main = new Main();
main.set("math", 100);
main.set("chiness", 200);
main.set("english", 210);
main.print();
System.out.println("-------");
main.set("music", 250);
main.set("draw", 250);
main.print();
System.out.println("-------");
main.get("english");
main.print();
}
private void print() {
for (String key : lruKeys) {
System.out.println(key + " = " + map.get(key));
}
}
private void set(String key, int val) {
if (map.containsKey(key)) {
map.put(key, val);
moveToFirst(key);
} else {
if (lruKeys.size() == size) {
String removeKey = lruKeys.removeLast();
map.remove(removeKey);
}
map.put(key, val);
lruKeys.addFirst(key);
}
}
private int get(String key) {
if (map.containsKey(key)) {
moveToFirst(key);
return map.get(key);
}
return 0;
}
private void moveToFirst(String key) {
lruKeys.remove(key);
lruKeys.addFirst(key);
}
}

Q11:最长回文子串



Leetcode 5:最长回文子串



思维题

Q1: 36辆车,6个跑道,最少多少次可以筛选出跑的最快的3辆车(不可用表计时)



8次 = 6 + 1 + 1。



分析:

  1. 首先将36辆车随机分成6组进行比赛,对每组进行排名并选择出每组的第一名(6次)

  2. 将步骤1结果的6辆车再进行一轮比赛,并选择出前三名,假设分别为A车、B车、C车(1次)

  3. 然后从A车所在的组(步骤1的分组)选出第2、3名,从B车所在的组(步骤1的分组)选出第2名。(因为A车第一,所以存在A车所在组的第2、3名比B、C车块的可能性;同理B车所在组的第2名存在比C车块的可能性;由于只选最快的3辆车,所以无需从C车所在组进行选车)

  4. 然后将步骤3选择出来的3辆车再加上A车、B车、C车进行一次比赛,选择出前三名(1次)

  5. 步骤4的结果就是最快的3辆车



Q2: 1000w个数据中找出重复的



使用bitmap。不要使用布隆过滤器,因为布隆过滤器并非100%准确。



Q3: 有2个玻璃球和一栋256层的高楼,如何快速定位到使得玻璃球摔碎的最低楼层



方案一:拿玻璃球一层层由低到高测试。



方案二:二分法。但是如果在中间的时候玻璃球碎了,那就无法再二分了,只能一层层的实验。



方案三:拿玻璃球测试楼层为n,2n,3n....这种。如果玻璃球在2n层摔坏了,则那另一个玻璃球从n+1 到 2n-1的楼层逐层实验。



其实可以看出来方案三中n=1时,就是方案一逐层实验;当n=128时,就是方案二。那我们如何求出n来使得结果最优呢?



假设使得玻璃球碎的楼层是256,步长n。则此时的次数为:256/n+n。要想该数值最小,则需要256/n = n。所以n = 16。



面试心得

面试前

  1. 准备简历,并保证简历中没有错别字。

  2. 简历中技能栏和项目栏中的项目一定要熟练。对于某技术熟悉就是熟悉、了解就是了解,要实事求是。否则被面试官问住是一件很尴尬的事情

  3. 准备1~2分钟的自我介绍

  4. 先拿几家不想去公司试试手,因为一开始面试被pass的几率很大

  5. 临近面试时,提前准备几个给面试官的问题,面试之后进行沟通。



面试中

  1. 不要紧张,遇到问题会就是会,不会就是不会。即使不会也可以说一下自己的思考和理解。

  2. 针对编程题,需要充分考虑边界问题和异常情况



面试后

  1. 对刚才的面试进行复盘。针对不会的问题或者答得不好的问题,进行总结并收集相关知识点。



发布于: 2020 年 05 月 06 日 阅读数: 2504
用户头像

吕小毛

关注

心之所向,素履以往 2018.12.28 加入

一枚正在成长的程序猿

评论 (7 条评论)

发布
用户头像
厉害了!
2020 年 05 月 07 日 13:17
回复
感谢支持
2020 年 05 月 07 日 18:06
回复
用户头像
非常有价值啊~
2020 年 05 月 07 日 08:49
回复
感谢支持,有问题随时交流哈
2020 年 05 月 07 日 18:06
回复
用户头像
🐴
2020 年 05 月 06 日 22:15
回复
😁
2020 年 05 月 07 日 18:07
回复
没有更多了
各大公司面试题分类整理