2026-10-16 4算法+1面经

最小路径和

给定一个包含非负整数的 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

二维dp:表示从0.0 到i.j 的最小路径和

class Solution {

public int minPathSum(int\[\]\[\] grid) {

int m=grid.length;

int n=grid0.length;

int\[\]\[\] dp=new intmn;

dp00=grid00;

for(int i=0;i<m;i++)

for(int j=0;j<n;j++){

if(i==0&&j-1>=0)dpij=dpij-1+gridij;

else if(j==0&&i-1>=0)dpij=dpi-1j+gridij;

else

if(i-1>=0&&j-1>=0)dpij=Math.min(dpij-1,dpi-1j)+gridij;

}

return dpm-1n-1;

}

}

编辑距离

给两个单词 word1 和 word2, 返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

**输入:**word1 = "horse", word2 = "ros"

**输出:**3

解释:

horse -> rorse (将 'h' 替换为 'r')

rorse -> rose (删除 'r')

rose -> ros (删除 'e')

思路:二维dp

状态定义

设 dp[i][j] = 将 word1 的前 i 个字符 (即 word1[0..i-1])转换成 word2 的前 j 个字符 (word2[0..j-1])所需的最少操作数。

  • 答案:dp[m][n],其中 m = word1.length(),n = word2.length()

  • 空串也纳入状态:dp[0][j] 表示把空串变成 word2 前 j 个字符


状态转移方程(核心)

考虑 word1[i-1] 和 word2[j-1] 这两个字符:

情况 1:字符相同 word1[i-1] == word2[j-1]

无需操作,直接继承:

dpij = dpi-1j-1

情况 2:字符不同

三种操作对应三种「来源状态」,取最小:

操作 含义 来源 转移
删除 删掉 word1[i-1] dp[i-1][j] 已经匹配好 word1 前 i-1 和 word2 前 j
插入 在 word1 插入一个字符(等价于删 word2[j-1]) dp[i][j-1]
替换 把 word1[i-1] 换成 word2[j-1] dp[i-1][j-1]

text

dpij = 1 + min(dpi-1j, // 删除

dpij-1, // 插入

dpi-1j-1) // 替换

dpij = (word1i-1 == word2j-1)

? dpi-1j-1

: 1 + min(dpi-1j-1, dpi-1j, dpij-1)

class Solution {

public int minDistance(String word1, String word2) {

int m=word1.length();

int n=word2.length();

int\[\]\[\] dp=new intm+1n+1;

for(int i=0;i<=m;i++)dpi0=i;

for(int i=0;i<=n;i++)dp0i=i;

for(int i=1;i<=m;i++)

for(int j=1;j<=n;j++){

if(word1.charAt(i-1)==word2.charAt(j-1)){

dpij=dpi-1j-1;

}

else

dpij=Math.min(dpi-1j-1,Math.min(dpi-1j,dpij-1))+1;

}

return dpmn;

}

}

1.介绍一个做过的 Java 后端项目

项目中的第二个领趣生活就是一个传统的java后端项目,该项目是一个本地生活服务平台,支持商户的查询,评论等功能,我最主要参与的是里面的一个优惠券秒杀模块,主要从缓存优化,避免超卖和确保一人一单,接口限流等维度进行开发,比如,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户信息,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销

2.系统的核心架构是怎样的

系统基于传统的三层架构开发,包括controller,service,mapper

3.高并发下怎么保性能和稳定性

1.在核心接口秒杀优惠券入口引入限流注解,限制入口流量

2.搭建多级缓存架构,降低数据库访问压力

3.利用Redis+lua脚本避免分布式锁的开销

4.数据库在高压力下是否出现过瓶颈?怎么优化的?

我举一个场景,在高并发下,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户ID,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销

后续利用jemeter进行了简单的压测,发现在保证不超卖和一人一单的情况下,QPS确实有较大幅提升,证明方案取得了效果

5.在项目中具体负责了哪些模块?有没有遇到特别难的问题?

1.滑动窗口限流注解开发

2.引入MQ异步扣减库存

3.优化库存超卖和一人一单逻辑判断

问题:

1.缓存击穿两套方案的取舍

queryWithLogicalExpire 逻辑过期 返回旧值 + 异步重建,可用性最高但数据弱一致,重建期间所有线程拿到的都是过期数据;queryWithMutex互斥锁 强一致,代价是需要阻塞等待,可用性较弱

综合考虑,在当前业务场景下,本地生活服务平台对于热点数据的访问更加注重可用性,而不是像金融交易场景下的数据强一致性,所以综合考虑,当前项目采用了逻辑过期方案解决缓存击穿问题

这让我明白方案的设计是跟随业务场景的变化而变化的,不同的方案适用于不同的场景,我们需要灵活的分析并应用

2.优化超卖和一人一单逻辑

在高并发下,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户ID,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销

后续利用jemeter进行了简单的压测,发现在保证不超卖和一人一单的情况下,QPS确实有较大幅提升,证明方案取得了效果

合并2个有序列表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

思路:建立一个哨兵节点,利用while循环,只有列表均不指向空,则每一次循环里判断2两个列表当前指向的值,如果表1小于或等于表2,则连接在哨兵节点后面,表1指针后移,哨兵指针也后移,反之,接入表2

如果表1表2两者长度不一致,在遍历完后,直接接入更长的后面的节点即可

/**

* Definition for singly-linked list.

* public class ListNode {

* int val;

* ListNode next;

* ListNode() {}

* ListNode(int val) { this.val = val; }

* ListNode(int val, ListNode next) { this.val = val; this.next = next; }

* }

*/

class Solution {

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {

ListNode dummy=new ListNode(-1);

ListNode cur=dummy;

while(list1!=null&&list2!=null){

if(list1.val<=list2.val){

cur.next=list1;

list1=list1.next;

}

else{

cur.next=list2;

list2=list2.next;

}

cur=cur.next;

}

cur.next=(list1!=null)?list1:list2;

return dummy.next;

}

}

删除链表的倒数第N个节点

思路

1.直接遍历得到长度,在遍历长度-n;

2.利用快慢指针和哨兵节点

哨兵节点指向head,初始时fast和slow指向dummy,注意不是指向head

让fast先走n+1步,然后同步走fast和slow,当fast==null的时候就停止,此时slow指针指向要删除节点的前驱节点,利用slow.next=slow.next.next直接删除,然后返回dummy.next即可

/**

* Definition for singly-linked list.

* public class ListNode {

* int val;

* ListNode next;

* ListNode() {}

* ListNode(int val) { this.val = val; }

* ListNode(int val, ListNode next) { this.val = val; this.next = next; }

* }

*/

class Solution {

public ListNode removeNthFromEnd(ListNode head, int n) {

ListNode dummy=new ListNode(-1);

dummy.next=head;

ListNode fast=dummy;

ListNode slow=dummy;

for(int i=0;i<=n;i++){

fast=fast.next;

}

while(fast!=null){

fast=fast.next;

slow=slow.next;

}

slow.next=slow.next.next;

return dummy.next;

}

}

1.-

Java 内存模型 JMM线程之间的可见性、有序性、原子性是怎么保证的?

volatile 和 synchronized 的区别?

底层实现原理?

JMM:围绕并发编程的三大特性 来设计的:原子性、可见性、有序性,定义了一套规则,规范了多线程如何通过主内存和工作内存来交互

  • 原子性:一个或多个操作要么全部执行,且不被中断,要么全部不执行。

    • 保证方式 :synchronized、Lock、java.util.concurrent 包下的原子类。单纯的 volatile 不保证原子性 (比如 i++)。
  • 可见性:一个线程修改了共享变量,其他线程能立刻看到。

    • 保证方式 :volatile(通过内存屏障强制刷新主内存并失效其他线程工作内存)。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

    • 保证方式 :volatile(禁止指令重排序)、synchronized(保证串行执行)。
volatile synchronized
作用范围 变量 方法、代码块
原子性 ❌ 不保证(i++ 会出问题) ✅ 保证
可见性 ✅ 保证 ✅ 保证
有序性 ✅ 禁止指令重排 ✅ 保证
阻塞 不会阻塞 会阻塞
性能 轻量级,性能高 重量级,性能相对低

底层实现原理

  • volatile 的底层:

    • 在汇编层面,写操作会加上 lock 前缀指令。

    • lock 指令会做两件事:① 将当前处理的数据写回主内存;② 这个写回操作会使其他 CPU 里缓存了该内存地址的数据失效(MESI 缓存一致性协议)。

    • 同时,JMM 会在 volatile 读写前后插入内存屏障(LoadLoad、StoreStore 等),禁止指令重排序。

  • synchronized 的底层:

    • 修饰代码块:通过 monitorenter 和 monitorexit 指令实现。

    • 修饰方法:通过方法访问标志 ACC_SYNCHRONIZED 实现。

    • 底层依赖 Monitor(管程) 对象,锁升级过程:无锁 → 偏向锁 → 轻量级锁(CAS 自旋) → 重量级锁(操作系统互斥量)。

  • 集合框架HashMap 的底层实现,JDK 8 中为什么引入红黑树?

HashMap 底层实现

  • JDK 7:数组 + 链表。

  • JDK 8:数组 + 链表 + 红黑树。

  • 为什么引入红黑树?

    • 当哈希冲突严重时,链表会变得很长,查询效率退化为 O(n)。

    • 红黑树将查询复杂度优化到 O(log n)。

    • 触发条件:链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树。

  • ConcurrentHashMap 的分段锁机制和 JDK 1.8 之后的 CAS+红黑树实现。

  • JDK 7(分段锁):

    相当于把整个 Map 分成多个小 Map,不同 Segment 之间可以并发读写,锁粒度是 Segment。

  • JDK 8(CAS + synchronized + 红黑树):

    • 摒弃分段锁,改用 CAS + synchronized 锁定单个桶(数组的某个索引位置)。

    • 结构:Node 数组 + 链表 + 红黑树。

    • 当桶为空时,用 CAS 无锁插入;当桶不为空时,用 synchronized 锁住该桶的头节点,保证线程安全。这样锁的粒度更细,并发度更高。

  • 线程池参数的含义?

  • 如何选择合适的线程池?

  • submit() 和 execute() 的区别?

  • 线程池满了之后会发生什么?如何避免?

线程池参数含义

  • corePoolSize:核心线程数,即使空闲也不会被回收

  • maximumPoolSize:最大线程数。

  • keepAliveTime:非核心线程空闲存活时间。

  • unit:时间单位。

  • workQueue :任务队列,常用 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。

  • threadFactory:线程工厂,自定义线程名、优先级等。

  • handler:拒绝策略。

如何选择合适的线程池?

  • CPU 密集型:线程数 = CPU 核数 + 1。

  • IO 密集型:线程数 = CPU 核数 × 2 或 (1 + 平均等待时间/平均计算时间)。

  • 实际生产中,建议根据压测结果动态调整,而不是死记公式。

submit() 和 execute() 的区别

  • execute():只能提交 Runnable,无返回值,无法捕获异常(异常会直接抛到线程池的 UncaughtExceptionHandler)。

  • submit():可以提交 Runnable 或 Callable,返回 Future,可以通过 Future.get() 获取结果或捕获异常。

线程池满了之后会发生什么?如何避免?

  • 发生过程:

    1. 核心线程满 → 任务入队。

    2. 队列满 → 创建非核心线程。

    3. 线程数达到 maximumPoolSize 且队列也满 → 触发拒绝策略。

  • 四种拒绝策略:

    • AbortPolicy(默认):显示抛出 RejectedExecutionException。

    • CallerRunsPolicy:让提交任务的线程自己执行。

    • DiscardPolicy:静默丢弃。

    • DiscardOldestPolicy:丢弃队列最老的任务。

  • 如何避免:

    • 使用有界队列 ,避免 LinkedBlockingQueue 默认无界导致 OOM。

    • 根据业务量合理设置核心线程数和最大线程数。

    • 监控线程池状态(活跃线程数、队列大小),及时告警。

  • 线上 JVM 调优的经历:如何分析 OOM、频繁 Full GC 的问题?

如何分析 OOM?

  • 第一步:保留现场 。启动参数加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump。

  • 第二步:分析 dump 文件 。用 jvisualvm或 jhat 打开,查看:

    • 哪些对象占用了最多内存(Dominator Tree)。

    • 是否有内存泄漏(Leak Suspects 报告)。

  • 常见原因:

    • 线程池队列无界导致任务堆积。

    • 数据库连接、IO 流未关闭。

    • 大量动态代理类生成。

2. 如何分析频繁 Full GC?

  • 第一步:查看 GC 日志 。-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log。

  • 第二步:用工具分析 。GCViewer、GCEasy。

  • 常见原因:

    • 内存泄漏:老年代对象持续增长,每次 Full GC 后回收效果差。

    • 大对象直接进入老年代 :-XX:PretenureSizeThreshold 设置不当。

  • 解决:调整堆大小、新生代比例、选择合适的垃圾回收器(如 G1、ZGC)。

  • MySQL 索引的底层数据结构(B+ 树),为什么不用 B 树?

  • B+ 树特点:

    • 非叶子节点只存索引,不存数据,所以一个节点能存更多索引,树更矮胖,磁盘 IO 次数更少。

    • 所有数据都在叶子节点,且叶子节点用链表相连,范围查询效率极高。

  • 为什么不用 B 树:

    • B 树的非叶子节点也存数据,导致每个节点能存的索引数量少,树更高,IO 次数多。

    • B 树范围查询需要中序遍历,效率不如 B+ 树的链表遍历。

  • 索引失效的几种情况。

在索引列上使用函数 或运算

使用 LIKE 以 % 开头(如 %abc)

隐式类型转换

联合索引不满足最左前缀原则

OR 连接的条件中有一个没有索引

  • 事务的四大特性

  • 原子性(Atomicity) :事务要么全部成功,要么全部失败。由 undo log 实现。

  • 一致性(Consistency):事务执行前后,数据完整性约束不被破坏。

  • 隔离性(Isolation) :并发事务之间互不干扰。由 锁 和 MVCC 实现。

  • 持久性(Durability) :事务提交后,数据永久保存。由 redo log 实现。

  1. 你对 JVM 了解吗?

JVM 是 Java 程序运行的基础,它屏蔽了底层操作系统的差异,实现了"一次编写,到处运行"。我主要从三个维度理解 JVM:

1. 运行时数据区(内存结构)

  • 线程私有:

    • 程序计数器:记录当前线程执行字节码的行号,是唯一不会 OOM 的区域。

    • 虚拟机栈 :每个方法执行时创建栈帧,存储局部变量表、操作数栈、动态链接等。栈深度超限会抛 StackOverflowError。

    • 本地方法栈:为 Native 方法服务。

  • 线程共享:

    • 堆:存放对象实例,是 GC 的主战场。分新生代(Eden + Survivor)和老年代。

    • 方法区:JDK 8 之后由**元空间(MetaSpace)**实现,使用本地内存,存放类元信息、常量、静态变量等。

2. 垃圾回收(GC)

  • 核心是判断对象是否存活(可达性分析),然后由不同的垃圾回收器进行回收。

  • 常见回收器:Serial、Parallel、CMS、G1、ZGC。

3. 类加载机制

  • 包括加载、验证、准备、解析、初始化五个阶段。

  • 核心是双亲委派模型,保证类的唯一性和安全性。

4. 执行引擎

  • 解释器逐行执行字节码,JIT 编译器将热点代码编译为机器码,提升性能。
  1. 垃圾回收器,你知道哪些呢?
  • CMS(Concurrent Mark Sweep) :以最短停顿时间为目标,采用标记-清除算法。缺点:产生内存碎片、并发阶段占用 CPU。
  • G1(Garbage First) :JDK 9 之后的默认回收器。将堆划分为多个 Region,采用标记-整理算法,可预测停顿时间。适合大堆(6GB 以上)。
  1. 双亲委派模型你知道吗?

双亲委派模型是类加载机制的核心,它规定了类加载器之间的协作关系。

1. 类加载器层次

  • Bootstrap ClassLoader :启动类加载器,加载 JAVA_HOME/lib 下的核心类(如 rt.jar),由 C++ 实现,Java 中获取为 null。

  • Extension ClassLoader :扩展类加载器,加载 JAVA_HOME/lib/ext 下的类。

  • Application ClassLoader :应用类加载器,加载 classpath 下的类,也就是我们写的代码。

  • 自定义 ClassLoader :用户继承 ClassLoader 实现的加载器。

2. 工作流程

  • 当一个类加载器收到加载请求时,它不会自己先加载 ,而是委派给父加载器。

  • 父加载器再委派给它的父加载器,直到 Bootstrap。

  • 如果父加载器无法完成加载(在自己负责的路径下找不到),子加载器才会尝试自己加载。

3. 好处

  • 保证类的唯一性:同一个类在 JVM 中只会被加载一次,避免重复加载。

  • 保证安全性 :防止用户自定义的类(如 java.lang.String)覆盖核心类库。

  1. 双亲委派模型它可以被打破吗?
相关推荐
小poop1 小时前
基础排序算法详解——插入、选择与交换排序
算法·排序算法
青蓝智慧马老师2 小时前
想学数据分析但零基础,正确顺序不是先学 Python
大数据·人工智能·算法
jeo陽2 小时前
05-数组与冒泡排序:第一个数据结构与经典算法
c语言·数据结构·算法
鸽鸽程序猿2 小时前
【刷题册】hot21-25
算法
Keven_112 小时前
算法札记:记忆化搜索中面对多组数据的初始化
算法
happyprince2 小时前
02-具体观 · JevAny算法与实现剖析
人工智能·算法
Nil2082 小时前
leetcode 75颜色分类
数据结构·算法·leetcode
Liaiyang662 小时前
空圈容错视角 · 多维切面篇:我们生活的世界,不止三维
人工智能·算法·系统架构·前沿技术·哲学·认知科学
闻缺陷则喜何志丹2 小时前
【环】P6709 [CCC 2020] Swapping Seats 环|普及+
算法·洛谷·环