2025年4、5月大厂java岗算法开放式面试题
最近辅导了几位字节后端求职同学,有些offer入职了,有些过了笔试却死在了面谈中。这些同学的情况我都清楚,明明准备的不错、属于拿的出手那种,为何区别这么大呢?复盘中发现有几位都是:经不住反问。同一道题笔试过了,却在面谈中,摆下了阵来!
以2025年5月字节java岗位的面试真题为例
请描述下如何在缓存中实现:在O(1)时间复杂度内,查询到树形结构的子节点。
审题要点:
- O(1)时间内完成。不能是列表或数组结构。
- 高并发情况下的缓存,容量是有限的、要有淘汰机制。最近不用的内容删除。
- 查询子节点,1个或多个,那么查询条件就是父节点id
经过复盘
1、当面试官反问,一个线程安全的分布式key-value map不就实现了O(1)时间查询吗,为何要选择更繁琐的LRU缓存?
这部分答不好的同学都是,审题中的第2个要点没做好。高并发的情况,要确保内存安全就必须要加入内存淘汰策略,LRU算法是性价比最高的简单方案。
2、当面试官反问,你的实现中为何不单向链表?操作不是更简单一些吗。
这部分答不好的同学,审题中的2个要点没做好。要删除最近不用的,单向链表是没有办法让节点前后之间建立链接的啊。
最近一个月的复盘也发现了其它几家大厂的面试题,也越来越场景化了。**********************。
案例题的面谈要点
1、LRU算法,最少的语言说出最重要的内容:哈希表 + 双向链表
- 哈希表:O(1) 查找
- 双向链表:O(1) 调整顺序
- 头部:最近使用
- 尾部:最久未使用
操作:
- 访问:移到头部
- 插入:放到头部,满了删尾部
- 时间复杂度:全部 O(1)
回答的每一点,要经的起面试官反问!
2、key是父结点id,value是子节点列表
案例题的笔试参考
package net.dreampo.java_share.lru_algo;
import net.dreampo.java_share.structure_algo.DoublyLinkedList;
import java.util.HashMap;
/**
* LRU缓存实现
* 使用双向链表维护访问顺序,HashMap提供O(1)查找
* @param <K> 键类型
* @param <V> 值类型
*/
public class LRUCache<K, V> {
private HashMap<K, DoublyLinkedList<CacheEntry>.Node> cache; // 哈希表存储键到节点的映射
private DoublyLinkedList<CacheEntry> linkedList; // 双向链表维护访问顺序
private int capacity; // 缓存容量
// 缓存项,封装键值对
private class CacheEntry {
K key;
V value;
public CacheEntry(K key, V value) {
this.key = key;
this.value = value;
}
}
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.linkedList = new DoublyLinkedList<>();
}
/**
* 获取缓存值
* 如果存在,将其移动到链表头部(表示最近使用)
*/
public V get(K key) {
DoublyLinkedList<CacheEntry>.Node node = cache.get(key);
if (node == null) return null;
// 将访问节点移动到链表头部(表示最近使用)
linkedList.moveToHead(node);
return node.getData().value;
}
/**
* 设置缓存值
* 如果已存在则更新,否则添加新节点
* 超出容量时删除最久未使用的节点
*/
public void put(K key, V value) {
DoublyLinkedList<CacheEntry>.Node node = cache.get(key);
if (node == null) { // 新节点
CacheEntry entry = new CacheEntry(key, value);
node = linkedList.createNode(entry);
cache.put(key, node);
linkedList.addToHead(node); // 添加到链表头部
if (linkedList.size() > capacity) { // 容量超出,删除尾部节点
DoublyLinkedList<CacheEntry>.Node removed
= linkedList.removeTail();
if (removed != null) {
cache.remove(removed.getData().key);
}
}
} else { // 已存在节点
node.getData().value = value;
linkedList.moveToHead(node); // 更新访问时间
}
}
/**
* 删除指定键的缓存
*/
public V remove(K key) {
DoublyLinkedList<CacheEntry>.Node node = cache.get(key);
if (node == null) return null;
V value = node.getData().value;
linkedList.removeNode(node);
cache.remove(key);
return value;
}
/**
* 检查是否包含指定键
*/
public boolean containsKey(K key) {
return cache.containsKey(key);
}
/**
* 获取当前缓存大小
*/
public int size() {
return linkedList.size();
}
/**
* 获取缓存容量
*/
public int capacity() {
return capacity;
}
/**
* 清空缓存
*/
public void clear() {
cache.clear();
linkedList.clear();
}
/**
* 判断缓存是否为空
*/
public boolean isEmpty() {
return linkedList.isEmpty();
}
}
说明清楚调用方的传值

