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(); } }
说明清楚调用方的传值