本文已参与「新人创作礼」活动,一起开启掘金创作之路。
面试复习。加油。
一、算法刷题(刷几道中等的 mid)
1.最长递增子序列问题M300
题目描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
🌸「示例:」
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
解答
定义dp数组:dp[i] 表示以 nums[i] 这个数结尾的最⻓递增⼦序列的⻓度。
class Solution {
public int lengthOfLIS(int[] nums) {
// 定义dp数组:dp[i] 表示以 nums[i] 这个数结尾的最⻓递增⼦序列的⻓度。
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
Arrays.sort(dp);
return dp[dp.length - 1];
}
}
2.最大子数组和问题 E53
题目描述
给你一个整数数组
nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
🌸「示例:」
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
解答
class Solution {
public int maxSubArray(int[] nums) {
//定义dp数组:dp[i]表示以nums[i]结尾的最大和连续子数组
int[] dp = new int[nums.length];
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = nums[0];
for(int i = 1; i < nums.length; i++){
dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);
}
Arrays.sort(dp);
return dp[dp.length - 1];
}
}
3.最长公共子序列问题M1143
题目描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
🌸「示例:」
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
解答
class Solution {
int[][] dp;
public int longestCommonSubsequence(String text1, String text2) {
dp = new int[text1.length()][text2.length()];
for(int[] ints : dp){
Arrays.fill(ints, -1);
}
return dp(text1, 0, text2, 0);
}
int dp(String text1, int i, String text2, int j){
if(i == text1.length() || j == text2.length()){
return 0;
}
if(dp[i][j] != -1){
return dp[i][j];
}
if(text1.charAt(i) == text2.charAt(j)){
return 1 + dp(text1, i + 1, text2, j + 1);
} else {
dp[i][j] = max(
dp(text1, i + 1, text2, j),
dp(text1, i, text2, j + 1),
dp(text1, i + 1, text2, j + 1)
);
return dp[i][j];
}
}
int max(int a, int b, int c){
return Math.max(a, Math.max(b, c));
}
}
4.和为k的子数组M560
题目描述
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
🌸「示例:」
示例 1:
输入: nums = [1,1,1], k = 2
输出: 2
示例 2:
输入: nums = [1,2,3], k = 3
输出: 2
解答
解法1
class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
for (int i = 0; i < nums.length; i++) {
int sum = 0;
for (int j = i; j < nums.length; j++) {
sum += nums[j];
if (sum == k) {
count++;
continue;
}
}
}
return count;
}
}
5.赛马
64 匹马,8 个赛道,找出前 4,采用什么策略 & 最少要几次?(字节老喜欢问这个了,不过这次问的老细了,尤其是第10和第11次是怎么选的,为啥要多跑那一次什么的)
第一步:淘汰一半,64 进 32(共 8 场比赛)
第二步:每组第一名进行比赛,32 进 10(共 1 场比赛)
第三步:可能可以结束 (共 1 场比赛)
第四步:一定结束 (共 1 场比赛)
6.最小路径和M64
题目描述
给定一个包含非负整数的
m x n网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明: 每次只能向下或者向右移动一步。
解答
// 典型的动态规划问题
//时间复杂度 O(M×N) : 遍历整个 grid 矩阵元素。
//空间复杂度 O(1)O(1) : 直接修改原矩阵,不使用额外空间。
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0){
continue;
} else if (i == 0){
grid[i][j] = grid[i][j - 1] + grid[i][j];
} else if (j == 0){
grid[i][j] = grid[i - 1][j] + grid[i][j];
} else {
grid[i][j] = Math.min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
}
}
}
return grid[m - 1][n - 1];
}
7.整数反转M7
题目描述
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。
示例 1:
输入:x = 123
输出:321
示例 2:
输入:x = -123
输出:-321
示例 3:
输入:x = 120
输出:21
示例 4:
输入:x = 0
输出:0
提示:
-231 <= x <= 231 - 1
解答
class Solution {
public int reverse(int x) {
int rev = 0;
while(x != 0){
if (rev < Integer.MIN_VALUE / 10 || rev > Integer.MAX_VALUE / 10){
return 0;
}
// 弹出 x 的末尾数字 digit
int digit = x % 10;
x /= 10;
// 将数字 digit 推入 rev 末尾
rev = rev * 10 + digit;
}
return rev;
}
}
8.无重复字符的最长子串M3
题目描述
给定一个字符串
s,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解答
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) return 0;
HashMap<Character, Integer> map = new HashMap<>();
int max = 0;
int left = 0;//滑动窗口左指针
for (int i = 0; i < s.length(); i++) {
//1、首先,判断当前字符是否包含在map中,
//如果不包含,将该字符添加到map(字符,字符在数组下标)
//2、如果当前字符 ch 包含在 map中,此时有2类情况:
if (map.containsKey(s.charAt(i))) {
left = Math.max(left, map.get(s.charAt(i)) + 1);
}
map.put(s.charAt(i), i);
max = Math.max(max, i - left +1);
}
return max;
}
}
9.两数相加M2
题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
解答
class Solution2 {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 新链表头指针,用来指向头结点,返回结果。
ListNode pre = new ListNode(0);
// 可移动的一个指针
ListNode cur = pre;
// 进位数
int carry = 0;
while (l1 != null || l2 != null) {
int x = l1 != null ? l1.val : 0;
int y = l2 != null ? l2.val : 0;
int sum = x + y + carry;
// 计算进位数
carry = sum / 10;
// 实际存入链表的数
sum = sum % 10;
cur.next = new ListNode(sum);
cur = cur.next;
if (l1 != null){
l1 = l1.next;
}
if (l2 != null){
l2 = l2.next;
}
}
if (carry == 1){
cur.next = new ListNode(1);
}
return pre.next;
}
}
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
10.三数之和M15
题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
解答
二、基础(对照简历复习复习)
Java基础
熟悉 Java 基础知识,如面向对象、并发编程、异常处理、集合类、Java8新特性等
面向对象
为什么重写equals()就一定要重写hashCode()方法?
JVM
类加载机制
双亲委派机制
并发编程
多线程
线程相关的基本方法有 wait(等待)、notify(唤醒)、notifyAll、sleep(睡眠)、join(加入)、yield(让步)等,这些方法控制线程的运行,并影响线程的状态变化。
| VS | ReentrantLock | synchronized |
|---|---|---|
| 显式或隐式 | 通过lock方法加锁,unlock方法解锁。 | 隐式获取和释放锁 |
| 属于 | API级别的 | JVM级别的 |
| 是否公平 | 可以指定fair参数来决定 | 非公平锁 |
| 出现锁竞争 | 竞争失败时可以阻塞等待,也可以通过trylock方法直接返回退出 | 竞争失败时只能阻塞等待 |
| 等待机制 | Condition类 | wait/notify等待机制 |
| 底层实现 | 同步非阻塞,采用乐观并发策略 | 同步阻塞,采用悲观并发策略 |
线程池
(1)基础
// ThreadPoolExecutor 的构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
........................
}
| 序号 | 参数 | 说明 |
|---|---|---|
| 1 | corePoolSize | 核心线程数量 |
| 2 | maximumPoolSize | 最大线程数量 |
| 3 | keepAliveTime | 当前线程数大于corePoolSize时,空闲线程的存活时间 |
| 4 | unit | keepAliveTime的时间单位 |
| 5 | workQueue | 任务队列,被提交但尚未被执行的任务存放的地方 |
| 6 | threadFactory | 线程工厂,用于创建线程 |
| 7 | handler | 任务拒绝策略 |
(2)工作流程?
- 如果正在运行的线程数量少于核心线程数,线程池就会立刻创建线程并执行该线程任务。否则,该任务放入阻塞队列中。
- (关于阻塞队列)阻塞队列已满 且 正在运行的线程数量少于 最大线程数时,线程池创建非核心线程并立刻执行该线程任务。否则,拒绝执行该任务并抛出异常。
- (线程任务执行完毕后)该任务被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
- (关于空闲时间)线程处于空闲状态的时间超过 空闲线程存活时间时,正在运行的线程数量超过 核心线程数时,停止该空闲线程。所以,线程池中的所有线程任务都执行完之后,线程池会收缩到 核心线程数大小。
(3)拒绝策略?
- 直接抛出异常。
- 移除最早的一个线程任务。
- 丢弃当前的线程任务而不作任何处理。
- 自定义拒绝策略。
(4)5种常用的线程池
- 可缓存
- 固定大小
- 可任务调度
- 单个线程
- 足够大小
异常处理
Q1:return 和 finally的执行顺序 ?
答:
return先执行,将方法停下后执行 finally ,最后再执行 return 将值返回。
public class Return_Finally {
public static void main(String[] args) {
System.out.println(A.a());
}
}
class A {
public static int a() {
int i = 1;
try{
// return 先执行,将方法停下后执行 finally
// 最后再执行 return 将值返回
return i;
}finally {
System.out.println("f1==>>"+i);
++i;
System.out.println("f2==>>"+i);
}
}
}
结果:
Q2:列举几个受检异常?
答: 除了Exception中的RuntimeException及RuntimeException的子类以外,其他的Exception类及其子类(例如:
IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
集合类
Q1:HashMap 和 TreeMap ?
(1)对hash算法的理解?
把任意长度的输入,通过hash算法变成固定长度的输出。存在hash冲突问题。 hash冲突理论上不可避免,只能尽量避免。比如抽屉原理(10个苹果9个抽屉,一定有一个抽屉里的数量大于1)。好的hash算法应该考虑的点:效率高计算hash值,不可逆推出原文,分布均匀。
(2)HashMap中存储数据的结构?
数组 + 链表 + 红黑树。每个数据单元是一个Node结点,有hash、key、value、next字段(形成链表使用)。默认初始长度16,懒加载机制,第一次put时创建。负载因子默认0.75,扩容阈值16乘以0.75=12。链表转化为红黑树?2个条件,链表长度达到8,数组长度达到64(否则会扩容一次)。hasu字段值是key的hashcode值吗?不是,二次加工得到的,加大散列程度。为什么引入红黑树?解决哈希冲突导致链化严重的问题,影响查询效率。特殊的二叉排序树。什么情况下会扩容?达到扩容阈值的时候,位移运算16、32、64。
(3)TreeMap对集合中的元素根据键排序,默认按照key升序
排序场景?
(1)list接口
ArrayList(基于 Object 数组)
LinkedList(基于双向链表)(插入和删除快、查询慢)
(2)set接口(不可重复)
Set接口是Collection的子接口,set接口没有提供额外的方法。
HashSet(底层基于 HashMap 实现、无序)
LinkedHashSet(继承HashSet,底层使用 LinkedHashMap存储元素)
(3)map接口
HashMap(数组+链表+红黑树、线程不安全)(使用频率最高)⭐️
Java8新特性
设计模式
1 单例模式
常见写法有懒汉模式(线程安全)、饿汉模式、静态内部类、双重校验锁。
(1)懒汉模式(线程安全)
public class LazySingleton {
// 私有的静态(属于类)对象,能够很好地保证单例对象的唯一性。
private static LazySingleton instance;
private LazySingleton() {
}
// 加锁的静态方法获取该对象,线程安全。
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
(2)饿汉模式
类加载器完成后该类的实例便已经存在于JVM中了。
实现如下:
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return instance;
}
}
(3)静态内部类
静态内部类在JVM中是唯一的。
public class Singleton {
// 静态内部类:对象实例的定义和初始化 放在内部类中进行
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
(4)双重校验锁(懒汉模式优化)
public class Lock2Singleton {
// 对象锁:保障初始化时对象的唯一性
private volatile static Lock2Singleton singleton;
public Lock2Singleton() {}
public static Lock2Singleton getInstance() {
if (singleton == null) {
// synchronized方法锁:保障操作的唯一性
synchronized (Lock2Singleton.class) {
if (singleton == null){
singleton = new Lock2Singleton();
}
}
}
return singleton;
}
}
Linux
熟悉 Linux 的基本使用
计算机基础
熟悉计算机网络的 OSI 和 TCP/IP 分层模型以及 HTTP 等基本网络通信协议
数据库
熟练掌握 Mysql 数据库的使用以及常见优化手段(比如索引、SQL优化、读写分离&分库分表)、熟悉Redis基本使用,了解Oracle数据库使用
事务
事务问题
幻读?
sql操作多个事务竞争存在三个问题:脏读(T1读取到T2事务未提交的数据,T2回滚则T1读取到最终不一定存在的数据)、幻读、不可重复读(2个事务同时执行,不同时刻读取同一行数据可能结果不同)。
- 数据库的事务的概念与特性(ACID, 然后说了下隔离级别)
- 为什么要有数据库事务?
隔离级别:为了解决多个并行事务竞争,导致数据安全问题的一种规范。
索引
- MySQL 的索引有什么优缺点?
优点:
- 高效查找(最主要原因)
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
- 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
缺点:
- 占用空间。降低表的更新速度。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
- 建索引对写性能有什么影响?
INSERT、UPDATE 和 DELETE 需要更多时间,因为不仅要更新数据,而且还要更新索引。
- 索引为什么要用 B+ 树?(树更矮,层数低)
- 相对于二叉树,层级更少,搜索效率高;
- 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;
- 相对Hash索引,B+tree支持范围匹配及排序操作;
- 如果用其他结构,B+ 或者 红黑树 的所有节点已经全部存内存里,那性能上还存在差距吗?
- 了解 MySQL 的 explain 命令吗?
获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。
EXPLAIN 执行计划各字段含义:(重点字段是type、possible_key、key、key_len)
- 自增 id 和唯一 id,在性能上有差距吗?
uuid有16个字节,比int(4 byte)和bigint(8 byte)占用更多存储空间
其实在innodb存储引擎下,自增长的id做主键性能已经达到了最佳。不论是存储和读取速度都是最快的,而且占的存储空间也是最小。
为了存储和查询性能应该使用自增长id做主键。
为了全局的唯一性,应该用uuid做索引关联其他表或做外键。
其它(优化等)
执行SQL响应比较慢,你有哪些排查思路?
-
慢查询日志定位sql,分析sql执行计划。优化sql语句走索引或者增加索引。
-
表切分。水平切分(千万级别)、垂直切分(按列切分)
-
读写分离
-
查询频繁的热点数据放到redis缓存中
框架
熟练掌握 Spring、Spring MVC、SpringBoot、MyBatis、Spring Security等主流开发框架
Spring
(1)Spring中的Bean是线程安全的吗? 有状态的单例bean才会存在线程安全问题。
(2)用到的设计模式?
分布式
-
熟悉 SpringCloud 常见组件的使用和部分原理
-
熟悉分布式下的常见理论 CAP、BASE
-
熟悉分布式事务(2PC、3PC、TCC)、配置中心(Nacos)、分布式 id (UUID、Snowflake)的使用及原理
-
了解RPC框架(Dubbo)
-
MQ(流量削峰、应用解耦、异步处理)
MQ
(1)什么是MQ? 分布式应用之间实现异步通信的方式。 三部分组成:
- 生产者(生产消息,消息的发起)
- 消息服务端(消息队列,存储消息)
- 消费者(消费消息,处理业务逻辑)
(2)应用场景
- 流量削峰(保证高可用。大流量缓冲,如订单系统流量高峰期)
- 应用解耦(订单、物流、库存、支付)(不同系统使用不同框架或编程语言,提高系统的灵活性)
- 异步处理(用户登陆发送验证码、支付成功的通知)(实时性要求不高)
(3)如何选择? Kafka:数据量大,吞吐量比较高。 RocketMQ:可靠性要求高。