topcode【随机算法题】【2026.5.11打卡-java版本】
32.最长有效括号【hard】
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号 子串 的长度。
左右括号匹配,即每个左括号都有对应的右括号将其闭合的字符串是格式正确的,比如 "(()())"。
要点:1.栈 2.push下标 3.哨兵(初始push(-1))
class Solution {
public int longestValidParentheses(String s) {
//栈
//要保留栈的前一个索引
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1);
int ans = 0;
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(c == '('){
stack.push(i);
}else{
stack.pop();
if(stack.isEmpty()){
stack.push(i);
}else{
ans = Math.max(ans, i - stack.peek());
}
}
}
return ans;
}
}
69.x的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
要点:1.二分查找 2.记得转换long
class Solution {
public int mySqrt(int x) {
if(x== 0 || x == 1){
return x;
}
int left = 0;
int right = x;
int ans = 0;
while(left <= right){
int mid = left + (right - left)/2;
//记得转换
long bijiao = (long) mid * mid;
//
if(bijiao == x){
return mid;
}else if(bijiao < x){
ans = mid;
left = mid +1;
}else{
right = mid -1;
}
}
return ans;
}
}
148.排序链表
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
要点:1.分治算法【sort,merge】 2.mid【取中间点,注意pre断连结】 3.merger两个链表
/**
* 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 sortList(ListNode head) {
//分治
if(head == null || head.next ==null){
return head;
}
ListNode head2 = middle(head);
head = sortList(head);
head2 = sortList(head2);
return merge(head, head2);
}
public ListNode middle(ListNode head){
//断开前一个
ListNode pre = head;
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
pre = slow;
fast = fast.next.next;
slow = slow.next;
}
pre.next = null;
return slow;
}
public ListNode merge(ListNode list1, ListNode list2){
ListNode dummy = new ListNode();
ListNode curr = dummy;
while(list1 != null && list2 != null){
if(list1.val < list2.val){
curr.next = list1;
list1 = list1.next;
}else{
curr.next = list2;
list2 = list2.next;
}
curr = curr.next;
}
curr.next = (list1 == null ) ? list2 : list1;
return dummy.next;
}
}
随机刷新的面经整理
说明:小红书上的博主更新的我只是整理
4.10美团后端一面 模块一:项目一(高并发优惠券系统) 1. 异常流推演(数据库): 抢完之后,如果后面数据库操作失败了怎么办? 2. 异常流推演(MQ发送): 你说的 Try-Catch 捕获异常,是在发送 MQ 的时候做的吗? 3. 异常流推演(MQ消费): 发送 MQ 失败存数据库,那有没有可能是消费的时候出现异常呢? 4. 死信队列追问: 你说的消费异常重复 16 次,是因为有库存但是消费逻辑有问题导致没扣减成功,还是什么原因? 5. 极端兜底场景: 有没有可能扣减就扣减不到呢?比如模板被删了,给它重复消费 4 个小时,最终用户那边收到抢券失败吗? 模块二:高并发场景题与数据库设计 1. 超发控制机制: MySQL 扣减库存是用“库存大于0”这个逻辑来判断的,这个逻辑你觉得有没有什么问题? 2. 深挖 ABA(技术向): 你用“库存大于0”判断,会不会有 ABA 的问题? 3. 缓存与数据库一致性: 数据库和缓存同时有数据,更新时是先更新缓存还是先更新数据库?如果是你,你选择哪种方案? 4. 一致性追问(失败场景): 如果你选择先操作缓存,再操作数据库,那如果操作数据库失败了怎么办?5. 一致性追问(并发场景): 高并发场景下,直接操作缓存再去操作数据库,会不会有一些并发安全的问题?模块三:Java 基础(并发与线程池) 1. 线程池运行原理: 线程池里面的源码逻辑有了解过吗?它大概是什么样的? 2. 线程回收机制: 已经有一些临时线程了,它什么时候会被回收? 3. 核心线程追问: 核心线程为什么不会被回收? 模块四:项目二(Agent 智能助手与 RAG) 1. 架构阐述: Agent 项目重点说一到两个觉得比较重点的内容。 2. 认知考察: 你对 Agent 是怎么理解的?你觉得它是一个什么样的东西? 3. 细节追问(动态获取): 你刚才在对比 Prompt 时提到的“动态获取”是什么意思? 4. Function Call 落地: 你这里面是一个独立的 Agent 负责知识库检索,有具体实现什么 Function Call 吗? 模块五:Java 集合底层 1. HashMap 并发缺陷: HashMap 有线程安全的问题吗?举一个具体的并发情况的例子,什么操作会导致线程安全问题? 2. 线程一改成3,线程二覆盖,是覆盖它的 value 吗?如果顺序执行 put 3 再 put 2 最后是 2,那并发情况下最后也是置为 2,这为什么叫线程安全问题? 3. 头插法为什么会有循环的问题?是怎么产生的? 4. 尾插法并发时,丢失的是 value 还是什么? 手撕代码 :反转链表,每个题目给出面试官满意的回答
好的,这是一个非常典型且高质量的后端面试题集合,涵盖了高并发系统设计、数据库、缓存、Java并发、集合源码等多个核心领域。用户希望获得每个题目的“面试官满意的回答”。
我需要为每个问题提供一个深入、准确且结构清晰的回答。回答要体现原理理解、实战经验和问题解决思路。
整体思路:
-
模块一(优惠券系统):聚焦异常流处理,从本地事务、MQ可靠性、死信队列机制到极端兜底,层层递进。
-
模块二(高并发与数据库):重点分析
库存>0判断在ABA、超发、一致性方面的缺陷,并讨论缓存与数据库的更新顺序及并发问题。 -
模块三(Java并发与线程池):讲解线程池核心参数、执行流程、线程回收机制(
keepAliveTime和allowCoreThreadTimeOut)。 -
模块四(Agent与RAG):阐述Agent的核心能力(规划、记忆、工具使用),以及Function Call的具体实现和动态Prompt设计。
-
模块五(HashMap):重点说明并发put导致死循环(头插法)或数据覆盖(尾插法)的原理。
-
手撕代码:给出递归和迭代两种反转链表的解法,并分析时空复杂度。
回答风格要专业、自信、有深度,多用“首先...其次...另外...”等逻辑词串联,避免过于口语化。现在开始组织每个问题的详细回答。
这是一份非常经典且全面的后端面试复盘。针对你记录的每一个问题,我整理了面试官期望听到的、具备深度和实战经验的回答思路。
模块一:项目一(高并发优惠券系统)
1. 异常流推演(数据库): 抢完之后,如果后面数据库操作失败了怎么办?
-
核心点:本地事务 + 最终一致性。
-
回答思路: “我会使用数据库本地事务来保证‘扣减库存’和‘创建订单’(或记录领券记录)的原子性。如果扣减库存成功后,后续的插入操作失败,整个事务会回滚,库存也会随之恢复。但如果是网络超时等异常,事务会回滚,此时需要通过日志(如binlog)或定时任务扫描未完成状态的记录进行重试或补偿。核心原则是:先扣库存,后生成凭证,保证资源不超卖。”
2. 异常流推演(MQ发送): 你说的 Try-Catch 捕获异常,是在发送 MQ 的时候做的吗?
-
核心点:事务消息 vs 本地消息表。
-
回答思路: “单纯的Try-Catch在发送MQ时存在漏洞(本地事务提交成功但MQ发送失败)。我的做法是:先扣库存并写入本地消息表(同事务),然后由独立的后台线程轮询消息表发送MQ。或者使用RocketMQ的事务消息:发送半消息 → 执行本地事务 → 提交/回滚消息。这是为了保证本地事务和消息发送的原子性。”
3. 异常流推演(MQ消费): 发送 MQ 失败存数据库,那有没有可能是消费的时候出现异常呢?
-
核心点:消费幂等性 + 重试机制。
-
回答思路: “会的,消费异常很常见(如依赖服务宕机、数据冲突)。解决方案:1. 重试机制:Spring Kafka/RabbitMQ默认会重试,我们可以配置重试次数(如3次)和间隔。2. 死信队列:超过重试次数后,消息进入死信队列,人工介入或定时补偿。3. 消费幂等:这是核心前提,下游服务必须能处理重复消息(通过唯一ID+状态机判断),避免重复发券。”
4. 死信队列追问: 你说的消费异常重复 16 次,是因为有库存但是消费逻辑有问题导致没扣减成功,还是什么原因?
-
核心点:区分业务异常与系统异常。
-
回答思路: “这取决于异常类型。如果是下游服务超时、网络抖动等临时性问题,重试16次很可能成功。但如果是业务逻辑错误(如参数校验失败、模板状态为‘已删除’),重试一万次也不会成功。因此,死信队列的设计应该区分:业务异常直接进死信;系统异常先重试,仍失败再进死信。实际操作中,我们还会监控死信队列,及时告警人工处理。”
5. 极端兜底场景: 有没有可能扣减就扣减不到呢?比如模板被删了,给它重复消费 4 个小时,最终用户那边收到抢券失败吗?
-
核心点:逻辑删除 vs 物理删除,最终一致性。
-
回答思路: “会的。如果模板是逻辑删除(deleted=1),消费时检查不通过,应该直接失败并进死信,同时更新本地消息表状态为‘失败’,并通知用户‘抢券失败,活动已结束’。如果模板是物理删除(数据都没了),这属于数据灾难,需要从备份恢复。兜底方案是:消费端最后会记录一条明确的失败日志,通过后台管理界面,运营人员可以主动给用户补发优惠券作为补偿。”
模块二:高并发场景题与数据库设计
1. 超发控制机制: MySQL 扣减库存是用“库存大于0”这个逻辑来判断的,这个逻辑你觉得有没有什么问题?
-
核心点:ABA问题、性能问题。
-
回答思路: “单纯用
UPDATE table SET stock = stock - 1 WHERE id = ? AND stock > 0能防止超卖,但有局限:1. 性能瓶颈:所有请求都落到MySQL,扛不住非常高并发(如万级QPS)。2. ABA不是主要问题:在高并发扣减场景下,ABA(A线程读库存=5,B线程扣到4,A线程基于旧值5判断)不会造成超卖,因为SQL的stock > 0在数据库行锁下判断的是当前最新值。3. 真正的风险:如果业务是先SELECT stock,代码里判断if(stock>0),再UPDATE,就会有超卖风险。必须用一条原子SQL。”
2. 深挖 ABA(技术向): 你用“库存大于0”判断,会不会有 ABA 的问题?
-
核心点:理解ABA发生的条件。
-
回答思路: “如果只是判断库存大于0,没有ABA问题。ABA通常发生在‘比较并交换’时值先变A再变回A,但中间业务状态发生了变化。在库存扣减中,如果库存从5减到4再回滚到5,另一个线程在回滚期间扣减,即使看到库存=5,它扣减后库存=4,依然正确,不会超卖。真正需要注意ABA的是无锁数据结构或版本号乐观锁场景。库存扣减用SQL的
stock > 0配合行锁是安全的。”
3. 缓存与数据库一致性: 数据库和缓存同时有数据,更新时是先更新缓存还是先更新数据库?如果是你,你选择哪种方案?
-
核心点:权衡一致性与性能。
-
回答思路: “我倾向先更新数据库,再删除缓存(Cache Aside Pattern)。原因:1. 先更新缓存在极端情况下(更新缓存成功但DB失败)会造成永久脏数据。2. 先更新DB再删缓存,即使删缓存失败,也只是短暂不一致,下次读会从DB加载。而先删缓存再更新DB,在并发读时容易把旧数据重新加载进缓存。所以标准方案是:更新DB → 删除缓存。如果对一致性要求极高,可以加延迟双删或订阅binlog异步删除。”
4. 一致性追问(失败场景): 如果你选择先操作缓存,再操作数据库,那如果操作数据库失败了怎么办?
-
核心点:补偿机制。
-
回答思路: “如果我先操作缓存成功了,但DB操作失败了,缓存就成了脏数据。我需要在DB操作失败的catch块中,立即回滚缓存(即再次删除缓存)。同时记录日志,必要时触发告警。这种做法复杂且容易出错,这也是为什么业界更推荐‘先DB后缓存’的原因。”
5. 一致性追问(并发场景): 高并发场景下,直接操作缓存再去操作数据库,会不会有一些并发安全的问题?
-
核心点:并发导致的数据不一致。
-
回答思路: “会的。比如:线程A先删缓存,准备更新DB;此时线程B读缓存miss,从DB加载旧数据到缓存;然后线程A更新DB。最终缓存中是旧数据。这就是‘先删缓存后更新DB’的经典并发问题。解决方案:1. 延迟双删:更新DB后,sleep几十毫秒再删一次缓存。2. 串行化:对同一个key的写操作加分布式锁。3. 最终一致性:接受短暂不一致,利用binlog异步更新缓存。”
模块三:Java 基础(并发与线程池)
1. 线程池运行原理: 线程池里面的源码逻辑有了解过吗?它大概是什么样的?
-
核心点:corePoolSize, workQueue, maximumPoolSize的执行流程。
-
回答思路: “核心逻辑在
execute方法中(以JDK ThreadPoolExecutor为例):1. 如果当前工作线程数 < corePoolSize,直接创建新核心线程执行任务。2. 如果 >= corePoolSize,尝试将任务放入阻塞队列。3. 如果队列满了,且线程数 < maximumPoolSize,创建非核心线程执行任务。4. 如果线程数已达maximumPoolSize,执行拒绝策略。注意:非核心线程只在队列满后被创建,如果队列是无界的,永远不会创建非核心线程。”
2. 线程回收机制: 已经有一些临时线程了,它什么时候会被回收?
-
核心点:keepAliveTime + 阻塞队列的poll超时。
-
回答思路: “临时线程(非核心线程)在空闲时间超过
keepAliveTime后会被回收。源码中,线程执行完任务后,会从工作队列中poll(keepAliveTime, TimeUnit)获取新任务。如果超时还没获取到,且当前线程数 > corePoolSize,这个线程就会结束并被回收。核心线程如果设置了allowCoreThreadTimeOut(true),也会被回收。”
3. 核心线程追问: 核心线程为什么不会被回收?
-
核心点:对比poll和take。
-
回答思路: “因为核心线程从队列中取任务时,使用的是
take()方法,它会一直阻塞直到拿到任务,不会像poll(timeout)那样超时返回null。除非通过setAllowCoreThreadTimeOut(true)改变了这个行为。所以默认情况下,核心线程会一直存活,避免了频繁创建销毁线程的开销。”
模块四:项目二(Agent 智能助手与 RAG)
1. 架构阐述: Agent 项目重点说一到两个觉得比较重点的内容。
-
回答思路: “我重点说一下动态检索路由和多轮对话记忆管理。1. 动态路由:根据用户问题的意图分类,自动选择走常规RAG还是调用API工具(如查天气、订机票)。2. 记忆槽位:我们维护了一个滑动窗口的对话历史,并利用LLM做关键信息摘要压缩,避免长期对话token溢出,同时保留上下文。”
2. 认知考察: 你对 Agent 是怎么理解的?你觉得它是一个什么样的东西?
-
回答思路: “我认为Agent是大语言模型+规划能力+工具使用+记忆的集合体。它不仅仅是一个聊天机器人,而是能够自主理解目标、拆解步骤(如ReAct模式)、调用外部工具(API、数据库、代码解释器)来完成任务,并在过程中根据反馈调整计划的智能体。比如让它‘帮我订下周去北京的机票并安排会议’,它会先查日历,再调机票API,再调日历创建日程。”
3. 细节追问(动态获取): 你刚才在对比 Prompt 时提到的“动态获取”是什么意思?
-
回答思路: “动态获取指的是,我们不在Prompt中硬编码固定的系统消息,而是在每次请求时,根据用户当前问题、用户上下文(如用户ID、历史行为)、以及从知识库检索到的相关信息,动态拼接生成最终的Prompt。这样能保证LLM获知最相关、最新的信息,而不是过时或无关的通用指令。”
4. Function Call 落地: 你这里面是一个独立的 Agent 负责知识库检索,有具体实现什么 Function Call 吗?
-
回答思路: “有实现。我们定义了几个Function:
retrieve_knowledge(query)、query_database(sql)、send_email(to, subject, body)等。LLM会根据用户意图,返回一个结构化的JSON来调用对应Function。比如用户问‘查一下上季度的销售数据’,LLM会返回{function: ‘query_database’, arguments: {sql: ‘SELECT …’}},我们执行后将结果返回给LLM生成最终回答。”
模块五:Java 集合底层
1. HashMap 并发缺陷: HashMap 有线程安全的问题吗?举一个具体的并发情况的例子,什么操作会导致线程安全问题?
-
回答思路: “有。最经典的是JDK 1.7及以前,并发put时可能出现死循环(链表成环)和数据丢失。JDK 1.8改用尾插法和红黑树后,死循环问题基本解决,但仍会出现数据覆盖(两个线程同时执行put,后一个覆盖前一个的value,或size计数不准)和扩容时数据错乱。”
2. 线程一改成3,线程二覆盖,是覆盖它的 value 吗?如果顺序执行 put 3 再 put 2 最后是 2,那并发情况下最后也是置为 2,这为什么叫线程安全问题?
-
核心点:可见性 + 丢失更新。
-
回答思路: “因为并发场景下,最终结果不一定是2。可能线程一put(3)后,线程二put(2)同时发生,它们都读取到了同一个空的table槽位。线程一计算出hash位置,写入节点;线程二也基于旧状态(空)计算出同一个位置,直接覆盖写入。最终结果是2,但线程一put的3丢失了,这就是数据覆盖。更重要的是,size++不是原子操作,可能只加了1次size却是2,导致size与实际entry数量不符。”
3. 头插法为什么会有循环的问题?是怎么产生的?
-
核心点:并发扩容 + 头插法导致链表反向。
-
回答思路: “JDK 1.7扩容时,遍历旧链表的每个节点,用头插法插入新数组。如果两个线程(T1、T2)同时触发扩容,T1被挂起;T2完成扩容后,原链表顺序被反转(如A->B变成B->A)。T1恢复后,继续用头插法处理自己持有的旧头节点,由于链表已经被T2改变指向,T1在新数组中插入时可能形成循环引用(A.next = B, B.next = A),导致
get时死循环。”
4. 尾插法并发时,丢失的是 value 还是什么?
-
核心点:尾插法避免死循环,但丢失数据。
-
回答思路: “尾插法主要丢失值(value)。并发put时,两个线程计算出的索引位置相同,后put的那个会覆盖前一个的value,造成前一个put的值丢失。另外,
modCount(快速失败机制)和size的更新都不是原子的,导致size与实际元素个数不一致。尾插法解决了死循环,但没有解决数据可见性、原子性、覆盖等并发问题。”
碎碎念:后续会更新每天学习的八股和算法题,暑假实习找不到了,开始准备秋招的第一天。努力连续更新100天!
更多推荐

所有评论(0)