一、力扣
1、旋转图像

java
class Solution {
public void rotate(int[][] matrix) {
// 设矩阵行列数为 n
int n = matrix.length;
// 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2
// 其中 '/' 为整数除法
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < (n + 1) / 2; j++) {
// 暂存 A 至 tmp
int tmp = matrix[i][j];
// 元素旋转操作 A <- D <- C <- B <- tmp
matrix[i][j] = matrix[n - 1 - j][i];
matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j];
matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i];
matrix[j][n - 1 - i] = tmp;
}
}
}
}
2、 用 Rand7() 实现 Rand10()


java
class Solution extends SolBase {
public int rand10() {
while (true) {
int ans = (rand7() - 1) * 7 + (rand7() - 1); // 进制转换
if (1 <= ans && ans <= 10) return ans;
}
}
}
3、最大正方形

解法一:统计[i,j]左侧连续的1个数,遍历[0,i-1]看是否为正方形全一。
java
class Solution {
public int maximalSquare(char[][] matrix) {
int m=matrix.length;
int n=matrix[0].length;
int[][] count=new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i][j]=='1'){
count[i][j]=1+(j>0?count[i][j-1]:0);
}
}
}
int res=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i][j]=='1'){
int start=count[i][j];
for(int k=i;k>=0;k--){
start=Math.min(start,count[k][j]);
if(start==i-k+1||(k==i&&start==1)) res=Math.max(res,start*(i-k+1));
}
}
}
}
return res;
}
}
解法二:动态规划,dp[i+1][j+1]=Math.min(dp[i][j+1],Math.min(dp[i][j],dp[i+1][j]))+1;
java
class Solution {
public int maximalSquare(char[][] matrix) {
int m=matrix.length,n=matrix[0].length;
int[][] dp=new int[m+1][n+1];
int res=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(matrix[i][j]=='1'){
dp[i+1][j+1]=Math.min(dp[i][j+1],Math.min(dp[i][j],dp[i+1][j]))+1;
}
res=Math.max(res,dp[i+1][j+1]);
}
}
return res*res;
}
}
4、全排列

java
class Solution {
List<List<Integer>> res;
List<Integer> path;
int[] nums;
public List<List<Integer>> permute(int[] nums) {
res=new ArrayList<>();
path=new ArrayList<>();
this.nums=nums;
int[] vis=new int[nums.length];
dfs(vis);
return res;
}
public void dfs(int[] vis){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(vis[i]==0){
vis[i]=1;
path.add(nums[i]);
dfs(vis);
vis[i]=0;
path.removeLast();
}
}
}
}
5、全排列 II

解法一:使用Set来进行树层去重,因为每一次递归都会重新创Set

java
class Solution {
List<List<Integer>> res;
List<Integer> path;
int[] nums;
public List<List<Integer>> permuteUnique(int[] nums) {
res=new ArrayList<>();
path=new ArrayList<>();
this.nums=nums;
int[] vis=new int[nums.length];
dfs(vis);
return res;
}
public void dfs(int[] vis){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
Set<Integer> set=new HashSet<>();
for(int i=0;i<nums.length;i++){
if(vis[i]==0&&!set.contains(nums[i])){
vis[i]=1;
set.add(nums[i]);
path.add(nums[i]);
dfs(vis);
vis[i]=0;
path.removeLast();
}
}
}
}
解法二:排序后使用used数组在树枝上去重

java
class Solution {
List<List<Integer>> res;
List<Integer> path;
int[] nums;
int[] used;
int n;
public List<List<Integer>> permuteUnique(int[] nums) {
res=new ArrayList<>();
path=new ArrayList<>();
Arrays.sort(nums);
this.nums=nums;
n=nums.length;
used=new int[n];
backtracking();
return res;
}
public void backtracking(){
if(path.size()==n){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<n;i++){
if(used[i]==1) continue;
if(i>0&&used[i-1]==1&&nums[i]==nums[i-1]) continue;
path.add(nums[i]);
used[i]=1;
backtracking();
used[i]=0;
path.removeLast();
}
}
}
6、重新安排行程


java
class Solution {
// 使用 HashMap 存储每个出发地点及其对应的目的地的优先队列
Map<String, PriorityQueue<String>> map = new HashMap<String, PriorityQueue<String>>();
// 存储最后的行程路径
List<String> itinerary = new LinkedList<String>();
// 主方法:寻找行程
public List<String> findItinerary(List<List<String>> tickets) {
// 遍历每张机票
for (List<String> ticket : tickets) {
String src = ticket.get(0); // 获取出发地
String dst = ticket.get(1); // 获取目的地
// 如果出发地不在map中,初始化一个优先队列
if (!map.containsKey(src)) {
map.put(src, new PriorityQueue<String>());
}
// 将目的地加入对应出发地的优先队列
map.get(src).offer(dst);
}
// 从起点 "JFK" 开始深度优先搜索构建行程
dfs("JFK");
// 反转行程顺序,因为是在回溯过程中添加的,最终结果需要逆序
Collections.reverse(itinerary);
return itinerary; // 返回最终的行程
}
// 深度优先搜索方法
public void dfs(String curr) {
// 当当前地点在 map 中并且存在可访问的目的地时
while (map.containsKey(curr) && map.get(curr).size() > 0) {
// 从当前地点的优先队列中取出最小的目的地(字典序最小)
String tmp = map.get(curr).poll();
// 递归访问这个目的地
dfs(tmp);
}
// 将当前地点加入行程列表,保证最终是从目的地到出发地的逆序
itinerary.add(curr);
}
}
7、 N 皇后

java
class Solution {
List<List<String>> res;
int n;
public List<List<String>> solveNQueens(int n) {
char[][] ans=new char[n][n];
this.n=n;
for(int i=0;i<n;i++){
Arrays.fill(ans[i],'.');
}
res=new ArrayList<>();
dfs(0,ans);
return res;
}
public void dfs(int point,char[][] ans){
if(point==n){
List<String> temp=new ArrayList<>();
StringBuilder zos=new StringBuilder();
for(int i=0;i<n;i++){
zos=new StringBuilder();
for(int j=0;j<n;j++){
zos.append(ans[i][j]);
}
temp.add(zos.toString());
}
res.add(temp);
return;
}
for(int i=0;i<n;i++){
if(check(ans,point,i)){
ans[point][i]='Q';
dfs(point+1,ans);
ans[point][i]='.';
}
}
}
public boolean check(char[][] ans,int x,int y){
for(int i=0;i<x;i++){
if(ans[i][y]=='Q') return false;
}
for(int i=0;i<y;i++){
if(ans[x][i]=='Q') return false;
}
for(int i=x-1,j=y-1;i>=0&&j>=0;i--,j--){
if(ans[i][j]=='Q') return false;
}
for(int i=x-1,j=y+1;i>=0&&j<n;i--,j++){
if(ans[i][j]=='Q') return false;
}
return true;
}
}
8、解数独

java
class Solution {
// 用于标记每行、每列和每个3x3宫内已出现的数字
private boolean[][] line = new boolean[9][9]; // 行标记
private boolean[][] column = new boolean[9][9]; // 列标记
private boolean[][][] block = new boolean[3][3][9]; // 3x3宫标记
private boolean valid = false; // 用于标记是否找到有效的解
private List<int[]> spaces = new ArrayList<int[]>(); // 存储所有空白格的位置
// 主函数,用于解决数独问题
public void solveSudoku(char[][] board) {
// 遍历整个数独板,初始化标记和空白格
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
// 如果是空白格,添加到spaces列表中
spaces.add(new int[]{i, j});
} else {
// 如果是非空白格,更新行、列和宫的标记
int digit = board[i][j] - '0' - 1;
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
}
}
}
// 使用深度优先搜索(DFS)来填充空白格
dfs(board, 0);
}
// 深度优先搜索辅助函数,用于递归填充数独板
public void dfs(char[][] board, int pos) {
// 如果pos等于spaces的大小,说明所有空白格都已填充,标记为有效解
if (pos == spaces.size()) {
valid = true;
return;
}
// 获取当前空白格的位置
int[] space = spaces.get(pos);
int i = space[0], j = space[1];
// 尝试填充当前空白格为1到9中的每一个数字
for (int digit = 0; digit < 9 && !valid; ++digit) {
// 如果当前数字在行、列和宫中都没有出现过
if (!line[i][digit] && !column[j][digit] && !block[i / 3][j / 3][digit]) {
// 更新行、列和宫的标记
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
// 填充当前空白格
board[i][j] = (char) (digit + '0' + 1);
// 递归填充下一个空白格
dfs(board, pos + 1);
// 如果当前填充的数字不是有效解,回溯并清除标记
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = false;
}
}
}
}
二、面试
1、项目经验相关
- 第一个项目(仿掘金抽奖系统)的整体流程是怎样的?在项目中承担了哪些工作?遇到了哪些技术难点?如何解决的?
- 第二个项目(DB Router路由中间件)是基于什么技术实现的?有什么特点?
- 抽奖系统中使用REDIS预扣减和锁兜底的方案,为什么选择这种方式?有没有其他替代方案?
2、Java基础知识相关
- 面向对象编程的三大特性是什么?请分别解释。
- 封装、继承和多态的具体理解是什么?
- 多肽中的重载和重写有什么区别?
- 抽象类和普通类有什么区别?抽象类和接口有什么区别?
- 接口中可以包含构造函数吗?
- 什么是深拷贝和浅拷贝?它们的区别是什么?
- 泛型一般在什么时候使用?
- 请解释一下Java的反射机制?动态代理是如何实现反射的?
- 在项目开发中用到了哪些Java集合?CONCURRENT哈希map和哈希map有什么区别?
- 请解释一下线程池的工作原理?常用的核心参数有哪些?
1. 问题答案
- 面向对象编程的三大特性是什么?请分别解释。
封装、继承、多态。
- 封装:将数据与操作数据的方法绑定,隐藏内部实现细节,通过公共接口访问,提升安全性和可维护性。
- 继承:子类继承父类的属性和方法,实现代码复用,支持层次化类设计。
- 多态:同一方法在不同对象中表现不同行为,通过重写(运行时多态)和重载(编译时多态)实现,增强灵活性。
- 封装、继承和多态的具体理解是什么?
- 封装:通过访问修饰符(如
private
)限制直接访问,提供getter/setter
控制数据操作,降低耦合。 - 继承:子类复用父类代码,支持
is-a
关系,如Dog
继承Animal
,可扩展或重写方法。 - 多态:同一方法签名在不同上下文表现不同,如父类引用指向子类对象时,调用方法按实际类型执行。
- 多态中的重载和重写有什么区别?
- 重载(Overload):同一类中方法名相同、参数列表不同(类型/数量/顺序不同),返回类型可不同,属于编译时多态。
- 重写(Override):子类覆盖父类方法,方法名、参数列表、返回类型必须一致,访问权限不能更严格,属于运行时多态。
- 抽象类和普通类有什么区别?抽象类和接口有什么区别?
- 抽象类 vs 普通类:抽象类不能实例化,可含抽象方法(无实现)和具体方法;普通类可实例化,方法全为具体实现。
- 抽象类 vs 接口:
- 抽象类支持构造方法、成员变量和具体方法,用于代码复用;接口仅定义方法(Java 8+ 可有默认方法)、常量,强制实现契约。
- 类单继承抽象类,但可多实现接口。
- 接口中可以包含构造函数吗?
不可以。接口不能被实例化,无需构造函数。其方法默认由实现类实现(Java 8+ 允许默认方法,但无构造函数)。
- 什么是深拷贝和浅拷贝?它们的区别是什么?
- 浅拷贝:复制对象时,引用类型字段指向原对象的内存地址,修改会影响原对象。
- 深拷贝:完全复制对象及其引用字段指向的新对象,与原对象独立。
区别:浅拷贝共享引用,深拷贝完全独立。例如,浅拷贝后的数组元素修改会影响原对象,深拷贝不会。
- 泛型一般在什么时候使用?
类型安全与代码复用。泛型用于集合类(如 List<T>
)避免强制类型转换,或在通用算法中处理多种类型,保证编译时类型检查。例如,HashMap<K, V>
键值对类型明确,减少运行时错误。
- 请解释一下Java的反射机制?动态代理是如何实现反射的?
- 反射:在运行时动态获取类信息(方法、字段、构造函数),并调用或修改其行为,如
Class.forName()
、method.invoke()
。 - 动态代理:通过
Proxy.newProxyInstance()
生成代理对象,在运行时实现接口,利用反射拦截方法调用并增强逻辑(如日志、事务),核心依赖InvocationHandler
。
- 在项目开发中用到了哪些Java集合?CONCURRENT哈希map和哈希map有什么区别?
- 常用集合:
ArrayList
(列表)、HashMap
(键值对)、HashSet
(集合)、ConcurrentHashMap
(线程安全映射)。 - 区别:
•HashMap
非线程安全,多线程并发可能导致死循环或数据丢失;
•ConcurrentHashMap
通过分段锁(Java 7)或 CAS(Java 8+)实现高并发安全,性能更高。
- 请解释一下线程池的工作原理?常用的核心参数有哪些?
- 工作原理:复用已创建的线程处理任务,减少频繁创建销毁的开销。流程:提交任务→核心线程执行→队列等待→创建新线程→队列满且达最大线程数→拒绝策略。
- 核心参数:
•corePoolSize
(核心线程数)、maximumPoolSize
(最大线程数)、keepAliveTime
(空闲线程存活时间)、workQueue
(任务队列)、threadFactory
(线程工厂)、RejectedExecutionHandler
(拒绝策略)。
- 一个新任务到线程池后的过程
当一个新任务提交到线程池时,线程池会首先尝试通过空闲的核心线程执行任务;若核心线程均忙碌且任务队列未满,则将任务放入队列等待;若队列已满,则创建新的非核心线程(不超过最大线程数限制)执行任务;若线程数已达上限且队列仍满,则触发拒绝策略(如抛出异常或丢弃任务)。任务执行完成后,线程会从队列中获取下一个任务,空闲线程在超时时间内无任务则销毁,实现资源复用与动态扩缩容。
3、并发编程相关
- 常用的锁有哪些?synchronize和ReentrantLock的工作原理和区别是什么?
1. 问题答案
常用的锁包括synchronized
(内置锁)、ReentrantLock
(可重入锁)、ReentrantReadWriteLock
(读写锁)、StampedLock
(乐观读锁)等。synchronized是JVM层面的锁,基于监视器(monitor)实现,自动获取和释放锁,支持可重入但仅非公平;ReentrantLock是java.util.concurrent.locks
包下的API,需手动加锁/释放(通过try-finally
确保),支持公平/非公平模式,提供条件变量(Condition
)和可中断特性。两者的核心区别在于:1) 实现层面,前者依赖JVM,后者基于AQS(AbstractQueuedSynchronizer);2) 功能,后者支持公平锁、多条件队列及可中断加锁;3) 性能,早期版本中ReentrantLock在高竞争下更优,但Java 6后synchronized通过偏向锁、轻量级锁等优化大幅缩小差距。适用场景:简单同步优先选synchronized
(简洁性),复杂需求(如公平性、精细控制)则用ReentrantLock。
4、JVM相关
- JVM的内存模型分为哪几部分?线程私有和线程共享的区域分别有哪些?
- 请解释一下JVM的垃圾回收机制?常用的垃圾回收器有哪些?垃圾回收算法有哪些?
1. 问题答案
- JVM的内存模型分为哪几部分?线程私有和线程共享的区域分别有哪些?
JVM内存模型主要分为以下部分:
- 程序计数器(Program Counter Register):线程私有,记录当前线程执行的字节码指令地址(或行号),用于线程切换后恢复执行位置。
- Java虚拟机栈(Java Virtual Machine Stacks):线程私有,存储方法调用的栈帧(局部变量表、操作数栈、动态链接、方法出口)。
- 本地方法栈(Native Method Stack):线程私有,为Native方法(非Java代码)服务。
- 堆(Heap):线程共享,存放对象实例和数组,是垃圾回收的主要区域。
- 方法区(Method Area):线程共享,存储类信息、常量、静态变量等,JDK8后由元空间(Metaspace)实现,使用本地内存。
线程私有区域:程序计数器、虚拟机栈、本地方法栈。
线程共享区域:堆、方法区。
- JVM的垃圾回收机制?常用垃圾回收器?垃圾回收算法?
垃圾回收机制:
JVM通过自动内存管理回收不再使用的对象(即"垃圾"),核心是判断对象可达性(通过GC Roots引用链)。当对象不可达时,会被标记为可回收,由垃圾回收器(GC)清理内存,避免内存泄漏。
常用垃圾回收器:
• Serial:单线程串行回收,适用于客户端模式(小型应用)。
• ParNew:多线程版Serial,常与CMS配合使用。
• Parallel Scavenge:关注吞吐量的多线程新生代回收器(高吞吐量=运行时间/(运行时间+GC时间))。
• CMS(Concurrent Mark-Sweep):并发标记清除,低延迟但可能产生内存碎片,适用于老年代。
• G1(Garbage-First):面向服务端应用的垃圾回收器,分代收集,可预测停顿时间,优先回收高价值区域。
• ZGC/Shenandoah:低延迟回收器(亚毫秒级停顿),支持超大堆。
垃圾回收算法:
- 标记-清除(Mark-Sweep):标记不可达对象后清除,效率低且产生内存碎片。
- 复制(Copying):将存活对象复制到空闲区域(如新生代Serial/ParNew),简单高效但浪费一半内存。
- 标记-整理(Mark-Compact):标记后整理存活对象至一端(如老年代CMS最终阶段),解决碎片问题。
- 分代收集(Generational):基于对象存活周期划分堆(新生代+老年代),新生代用复制算法,老年代用标记-清除/整理。
5、MySQL相关
- 一条SQL语句的执行过程是怎样的?
- 请解释一下MySQL的索引?分为哪几类?非聚簇索引一定会回表吗?
- 索引背后的数据结构是什么?为什么使用B+树?
- 请解释一下MySQL的事务?四大特性是什么?常见的并发问题有哪些?解决措施是什么?
- 事务的隔离级别有哪些?分别解决了哪些并发问题?
1. 问题答案
-
一条SQL语句的执行过程是怎样的?
SQL语句的执行过程通常分为多个阶段:首先,解析器进行语法和语义分析,生成解析树;优化器根据统计信息选择最优执行计划;执行引擎调用存储引擎接口访问数据,若涉及索引则通过B+树定位数据页,读取数据到内存并处理;最后返回结果。若涉及事务,还会通过日志模块(如redo log、undo log)保证持久性和原子性。
-
请解释一下MySQL的索引?分为哪几类?非聚簇索引一定会回表吗?
索引是加速数据检索的数据结构,MySQL中主要分为聚簇索引(数据与索引存储在一起,如InnoDB的主键索引)和二级索引(非聚簇索引,叶子节点存储主键值)。此外还有唯一索引、全文索引等。非聚簇索引不一定会回表:若查询字段全部包含在索引中(覆盖索引),可直接返回数据,无需回表;若包含非索引字段,则需通过主键值二次查找聚簇索引(回表)。
-
索引背后的数据结构是什么?为什么使用B+树?
索引常用B+树结构。相比B树,B+树的非叶子节点仅存储键值(不存数据),可容纳更多键,减少树高,降低磁盘I/O次数;叶子节点通过链表连接,支持高效范围查询;且查询稳定性更高(所有查询到叶子节点)。B+树更适合数据库的随机查找与范围扫描场景。
-
请解释一下MySQL的事务?四大特性是什么?常见的并发问题有哪些?解决措施是什么?
事务是数据库操作的逻辑单元,具备ACID特性:原子性(操作不可分割,通过undo log实现回滚);一致性(事务前后数据合法,依赖原子性+隔离性+持久性);隔离性(事务互不干扰,通过锁或MVCC实现);持久性(提交后数据永久保存,依赖redo log)。
并发问题包括:脏读(读未提交数据)、不可重复读(同一事务内多次读取结果不一致)、幻读(同一查询条件返回不同行数)。解决措施:通过锁(悲观锁、乐观锁)、MVCC(多版本并发控制)实现不同隔离级别下的数据一致性。
-
事务的隔离级别有哪些?分别解决了哪些并发问题?
• 读未提交(Read Uncommitted):最低级别,可能脏读、不可重复读、幻读。
• 读已提交(Read Committed):仅读取已提交数据,解决脏读,但存在不可重复读和幻读。
• 可重复读(Repeatable Read,默认InnoDB):事务内多次读取结果一致,解决脏读和不可重复读,但可能幻读(通过MVCC和间隙锁优化,实际可避免幻读)。
• 串行化(Serializable):完全隔离,通过锁强制顺序执行,解决所有并发问题,但性能最低。
6、HTTP相关
- HTTP报文的结构是怎样的?
- 常见的HTTP状态码有哪些?
- 请解释一下TCP的三次握手过程?为什么需要序列号?
1. 问题回答
- HTTP报文的结构是怎样的?
HTTP报文分为请求报文和响应报文。请求报文包含:
• 请求行(方法、URL、HTTP版本,如 GET /index.html HTTP/1.1
);
• 请求头(键值对,如 Host: example.com
、User-Agent: Chrome
);
• 空行(分隔头部与正文);
• 请求体(可选,如POST请求的数据)。
响应报文包含:
• 状态行(HTTP版本、状态码、原因短语,如 HTTP/1.1 200 OK
);
• 响应头(如 Content-Type: text/html
、Content-Length
);
• 空行;
• 响应体(实际返回内容,如HTML网页)。
- 常见的HTTP状态码有哪些?
• 2xx(成功):200 OK
(请求成功)、201 Created
(资源创建成功)。
• 3xx(重定向):301 Moved Permanently
(永久跳转)、302 Found
(临时跳转)。
• 4xx(客户端错误):400 Bad Request
(请求格式错误)、404 Not Found
(资源不存在)、403 Forbidden
(无权限)。
• 5xx(服务器错误):500 Internal Server Error
(服务器内部错误)、503 Service Unavailable
(服务暂时不可用)。
- 请解释一下TCP的三次握手过程?为什么需要序列号?
三次握手过程:
- 客户端发送SYN包(初始序列号
seq=x
),请求建立连接; - 服务端回复SYN+ACK包(确认号
ack=x+1
,自身序列号seq=y
); - 客户端再发ACK包(确认号
ack=y+1
),连接建立。
序列号的作用:
• 保证有序传输:接收方按序列号重组数据包;
• 去重:通过唯一序号识别重复数据包;
• 流量控制:通过确认机制实现可靠传输。
三次握手防止已失效的旧连接请求(如网络滞留包)被误认为新连接,确保双方同步初始序列号,建立可靠通信。
7、算法
1. 买卖股票的最佳时机 II

java
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
int[][] dp=new int[n][2];
dp[0][0]=-prices[0];
int res=0;
for(int i=1;i<n;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
res=Math.max(res,dp[i][1]);
}
return res;
}
}
2. 买卖股票的最佳时机 III

java
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
int[][] dp=new int[n][5];
dp[0][1]=-prices[0];
dp[0][3]=-prices[0];
for(int i=1;i<n;i++){
dp[i][0]=dp[i-1][0];
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
}
return dp[n-1][4];
}
}
3. 买卖股票的最佳时机 IV

java
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
// dp[i][j]表示第i天处于第j个状态时的最大利润
// 状态j的范围是0~2k,共2k+1个状态:
// 0: 无操作(初始状态)
// 1: 第一次买入,3: 第二次买入,..., 2k-1: 第k次买入
// 2: 第一次卖出,4: 第二次卖出,..., 2k: 第k次卖出
int[][] dp = new int[n][2 * k + 1];
// 初始化:所有可能的买入状态在第0天的情况
// 奇数位(1,3,...,2k-1)表示买入操作,初始化为-price[0]
for (int i = 1; i < k * 2 + 1; i += 2) {
dp[0][i] = -prices[0];
}
// 遍历每个交易日
for (int i = 1; i < n; i++) {
// 遍历所有可能的状态
for (int j = 1; j < 2 * k + 1; j++) {
if (j % 2 == 0) {
// 偶数状态:卖出状态,可以选择保持或卖出
// 卖出操作需从前一天的买入状态转移而来(j-1为奇数)
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
} else {
// 奇数状态:买入状态,可以选择保持或买入
// 买入操作需从前一天的卖出状态转移而来(j-1为偶数)
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
}
}
}
// 返回最后一天完成k次卖出后的最大利润
return dp[n - 1][2 * k];
}
}