
1. 项目缘起为什么我们需要一份Java刷题“兵器谱”如果你正在准备技术面试或者想系统性地提升自己的算法和编程能力LeetCode几乎是绕不开的平台。但很多朋友尤其是Java开发者在刷题时常常会遇到一个尴尬的局面题目思路想明白了代码逻辑也清晰了但就是写不快、写不优雅甚至在一些边界条件处理上栽跟头。问题出在哪很多时候不是算法本身而是对Java这门语言的标准库API和常用数据结构不够熟悉。我自己在带新人、面试候选人以及日常刷题时发现一个普遍现象大家花大量时间研究动态规划的状态转移方程琢磨回溯的剪枝策略这当然没错。但与此同时却对Arrays.sort()如何自定义排序、PriorityQueue的初始容量和比较器、StringBuilder和StringBuffer在并发场景下的细微差别、Map的computeIfAbsent方法如何简化代码等“基本功”掌握得模棱两可。结果就是一个简单的哈希表去重操作可能要写五六行代码而熟练的开发者一两行就能搞定这中间的效率差在笔试或面试的紧张环境下会被无限放大。这份总结就是我想为你整理的Java刷题“兵器谱”。它不教你具体的算法思想那是算法导论和各类教程的任务。它的核心目标只有一个当你确定了解题思路后能让你用Java语言最快、最稳、最专业地把代码写出来。我会把LeetCode刷题中最高频、最实用的API和数据结构用法结合具体的题目场景掰开揉碎了讲清楚并附上我踩过的坑和总结的技巧。这份文档是“活”的我会根据大家的反馈和新的题目类型持续更新。2. 集合框架你的算法“弹药库”深度解析Java集合框架是刷题时使用频率最高的部分没有之一。但会用ArrayList和HashMap只是入门理解其内部机制和特性才能让你在关键时刻做出最优选择。2.1 List家族ArrayList与LinkedList的抉择几乎所有需要动态数组的场景ArrayList都是首选。它的底层是数组支持O(1)时间的随机访问这是巨大优势。但在刷题中有几点必须注意初始化与容量很多人在刷题时习惯ListInteger list new ArrayList();就完事了。如果提前知道数据规模一定要指定初始容量。例如你知道最终要存放大约1000个元素那么new ArrayList(1000)。这能避免多次扩容带来的性能损耗和数据拷贝。虽然对于单次运行影响不大但在追求极致性能的解法或处理大数据量时这是一个好习惯。遍历与修改这是经典的坑。在遍历ArrayList并尝试删除元素时直接使用for循环配合索引删除会导致后续元素索引错乱。正确做法是使用Iterator的remove方法或者从后往前遍历删除。更现代和简洁的做法是使用removeIf方法ListInteger list new ArrayList(Arrays.asList(1, 2, 3, 4, 5)); list.removeIf(num - num % 2 0); // 删除所有偶数 System.out.println(list); // 输出: [1, 3, 5]LinkedList的应用场景在LeetCode中LinkedList作为双向链表其用武之地相对特定。当你需要频繁在列表头部或尾部进行插入和删除操作并且不需要随机访问时它就是最佳选择。典型的题目是实现LRU缓存淘汰算法的双向链表部分或者某些BFS中用于队列但通常更推荐ArrayDeque。记住LinkedList的get(int index)方法是O(n)的切忌把它当数组用。2.2 Map家族HashMap、TreeMap与LinkedHashMapHashMap是哈希表题的绝对主力O(1)时间复杂度的查找、插入是其核心价值。自定义对象作为Key这是面试常考点。如果你自定义的类比如一个点的坐标Point要作为HashMap的键必须重写hashCode()和equals()方法。hashCode决定了对象被放入哪个桶equals用于在哈希冲突时比较桶内的对象是否真正相等。IDE通常可以自动生成这两个方法但你需要理解其必要性。computeIfAbsent与merge让代码更简洁这两个方法是Java 8之后提升代码表达力的神器。看一个统计词频的例子// 传统写法 MapString, Integer countMap new HashMap(); for (String word : words) { if (countMap.containsKey(word)) { countMap.put(word, countMap.get(word) 1); } else { countMap.put(word, 1); } } // 使用 merge 方法 (更简洁) MapString, Integer countMap2 new HashMap(); for (String word : words) { countMap2.merge(word, 1, Integer::sum); // 如果key存在将旧值和1相加不存在则放入1 } // 使用 computeIfAbsent 和 put (在某些复杂value时好用) MapString, ListString groupMap new HashMap(); for (String item : items) { String key getKey(item); groupMap.computeIfAbsent(key, k - new ArrayList()).add(item); }computeIfAbsent特别适合value是集合的场景它实现了“如果key不存在则创建一个新集合放入如果存在则直接返回该集合”的原子操作。TreeMap需要有序Key时使用当题目要求你按照键的自然顺序或自定义顺序进行遍历时TreeMap就派上用场了。它的增删查改操作都是O(log n)。例如LeetCode上“数据流的中位数”、“我的日程安排表”等题目利用TreeMap的ceilingKey返回大于等于给定键的最小键、floorKey等方法可以优雅求解。记住它的有序性是靠红黑树实现的。LinkedHashMap记住插入顺序或访问顺序它继承自HashMap但额外维护了一个双向链表来记录条目顺序。默认是插入顺序也可以构造为访问顺序最近访问的放在最后。这使其成为实现LRU缓存的绝佳选择无需自己从头构建双向链表。2.3 Set家族HashSet、TreeSet与去重艺术Set用于去重和快速存在性检查。HashSet基于HashMapTreeSet基于TreeMap所以它们的特性与对应的Map一致。去重的陷阱对于自定义对象放入HashSet同样需要正确重写hashCode和equals。TreeSet则需要对象实现Comparable接口或者在构造时传入Comparator。TreeSet的导航方法和TreeMap类似TreeSet提供了ceiling(e),floor(e),higher(e),lower(e)等方法在需要找到集合中某个元素“附近”的元素时非常有用例如在“包含重复元素的有序集合中找上下界”这类题目中。2.4 Queue与Deque算法中的“流水线”队列在BFS广度优先搜索中是核心数据结构。LinkedList实现了Deque接口可以作为队列使用但通常更推荐ArrayDeque。为什么推荐ArrayDeque作为队列ArrayDeque底层是循环数组它在大多数操作上的性能都优于LinkedList因为避免了链表节点的内存开销。无论是作为普通队列FIFO还是栈LIFOArrayDeque都是更好的选择。除非你需要频繁在中间插入删除或者需要用到LinkedList的特定API。// BFS 模板中使用 ArrayDeque DequeTreeNode queue new ArrayDeque(); queue.offer(root); // 入队推荐使用 offer 而不是 add后者在容量限制队列中会抛异常 while (!queue.isEmpty()) { int levelSize queue.size(); for (int i 0; i levelSize; i) { TreeNode node queue.poll(); // 出队 // ... 处理当前节点 ... if (node.left ! null) queue.offer(node.left); if (node.right ! null) queue.offer(node.right); } }PriorityQueue堆的实现这是解决Top K问题、求中位数、Dijkstra算法等问题的利器。关键在于构造时传入正确的比较器Comparator。// 最小堆默认 PriorityQueueInteger minHeap new PriorityQueue(); // 最大堆 PriorityQueueInteger maxHeap new PriorityQueue((a, b) - b - a); // 或者 Comparator.reverseOrder() // 自定义对象堆按某个属性排序 PriorityQueuePerson pq new PriorityQueue(Comparator.comparingInt(p - p.age));注意PriorityQueue的iterator()遍历不保证顺序。只有通过poll()或remove()方法取出的元素才是有序的。3. 数组与字符串基础中的战斗机虽然基础但相关的API和技巧往往能决定代码的简洁度和效率。3.1 数组工具类java.util.ArraysArrays类充满了宝藏方法。排序与二分查找Arrays.sort()可以对数组排序对于对象数组可以传入Comparator。Arrays.binarySearch()在有序数组上进行二分查找返回索引找到或插入点未找到为-插入点- 1。切记必须在调用binarySearch前确保数组已排序否则结果未定义。数组填充、复制与比较Arrays.fill(arr, value)快速填充数组。Arrays.copyOf(arr, newLength)复制数组可以扩容或缩容。Arrays.equals(arr1, arr2)比较两个数组内容是否相等。对于多维数组使用Arrays.deepEquals。Arrays.toString(arr)/Arrays.deepToString(multiArr)调试神器快速打印数组内容。将数组转为ListArrays.asList(T... a)。这里有一个大坑它返回的List是一个固定大小的视图不支持add或remove操作会抛出UnsupportedOperationException。如果需要一个可变的ArrayList应该new ArrayList(Arrays.asList(...))。3.2 字符串String、StringBuilder与StringBuffer不可变性与性能String的不可变性是Java的基础设计。这意味着任何对String的修改拼接、替换都会产生新的对象。在循环中进行字符串拼接是性能杀手。// 错误示范在循环中拼接字符串 String result ; for (String str : stringList) { result str; // 每次循环都创建新的StringBuilder和String对象 } // 正确示范使用 StringBuilder StringBuilder sb new StringBuilder(); for (String str : stringList) { sb.append(str); } String result sb.toString();StringBuildervsStringBuffer两者API几乎一样。StringBuffer是线程安全的方法加了synchronized关键字但因此有性能损耗。在LeetCode刷题这种单线程场景下永远使用StringBuilder。常用API点睛charAt(int index),length()基础访问。substring(int beginIndex, int endIndex)取子串。注意参数是前闭后开[begin, end)。indexOf(String str),lastIndexOf(String str)查找子串位置。split(String regex)按正则分割注意正则元字符如.、|需要转义。toCharArray()转为字符数组便于修改和遍历。String.format(String format, Object... args)格式化字符串在构造复杂输出时比拼接更清晰。字符串转换数字与字符串Integer.parseInt(str),String.valueOf(num)。字符数组与字符串new String(charArray),str.toCharArray()。4. 数学、随机与位运算隐藏的利刃这类工具不常用但一旦用到就是解决问题的关键。4.1Math类与Random类Math类提供了基本的数学运算常量和方法Math.PI,Math.E,abs,max,min,pow,sqrt,log,sin,cos等。在图形、几何或需要数学计算的题目中会用到。Random类用于生成伪随机数。刷题中常用于打乱数组洗牌算法、随机选择等。注意可以指定种子seed以保证可重复性这在调试时很有用。Random rand new Random(12345); // 固定种子 int randomNum rand.nextInt(100); // 生成 [0, 100) 的随机整数 // 洗牌算法 (Fisher-Yates Shuffle) for (int i arr.length - 1; i 0; i--) { int j rand.nextInt(i 1); // 生成 [0, i] 的随机整数 // 交换 arr[i] 和 arr[j] int temp arr[i]; arr[i] arr[j]; arr[j] temp; }Java中更现代的洗牌可以使用Collections.shuffle(List? list)。4.2 位运算高效与技巧并存位运算在部分算法题中能极大提升性能也是面试高频考点。基本操作(与)同1为1。常用场景判断奇偶n 1取特定位。|(或)有1为1。常用场景将特定位设为1。^(异或)相同为0不同为1。重要性质任何数与自身异或为0a ^ a 0与0异或为自身a ^ 0 a。这是解决“只出现一次的数字”系列题目的核心。~(取反)0变11变0。(左移)相当于乘以2的n次方。低位补0。(右移)相当于除以2的n次方向下取整。高位补符号位算术右移。(无符号右移)高位补0。常用技巧判断奇偶(n 1) 1为奇数。交换两个数a ^ b; b ^ a; a ^ b;无需临时变量但可读性差谨慎使用。取最低位的1lowbit n (-n)。这是树状数组Binary Indexed Tree的核心操作。消去最低位的1n n (n - 1)。常用于统计二进制中1的个数Brian Kernighan算法。判断是否是2的幂n 0 (n (n - 1)) 0。对2的幂取模n % (2^k)等价于n ((1 k) - 1)。BigInteger与BigDecimal当题目涉及远超long范围的大整数运算比如某些数学题、高精度计算时就需要用到BigInteger。它提供了任意精度的整数运算。BigDecimal用于高精度的浮点数运算避免double的精度损失。使用时注意它们的对象是不可变的运算方法返回新对象。5. 输入输出与调试刷题的“后勤保障”LeetCode虽然帮我们处理了输入输出但了解Java的标准I/O对于理解代码、本地调试和应对其他OJ平台至关重要。5.1 快速输入输出Scanner与BufferedReader对于数据量较大的输入Scanner虽然方便但比较慢。BufferedReaderStringTokenizer或String.split()是更高效的选择。// 高效读入示例 (适用于本地测试或某些OJ) import java.io.*; import java.util.*; public class FastIO { public static void main(String[] args) throws IOException { BufferedReader br new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st new StringTokenizer(br.readLine()); int n Integer.parseInt(st.nextToken()); int m Integer.parseInt(st.nextToken()); // 或者使用 split String[] parts br.readLine().split( ); int a Integer.parseInt(parts[0]); int b Integer.parseInt(parts[1]); // 快速输出 BufferedWriter bw new BufferedWriter(new OutputStreamWriter(System.out)); bw.write(结果: (a b)); bw.newLine(); bw.flush(); // 记得刷新缓冲区 } }5.2 调试与格式化输出System.out.println是最常用的调试输出但在循环中大量调用会影响性能。在需要输出大量数据时可以考虑使用StringBuilder拼接后一次性输出。格式化输出除了String.formatSystem.out.printf也支持类似C语言的格式化输出对于控制输出格式非常方便。double value 3.1415926; System.out.printf(Value: %.2f, Integer: %d%n, value, 100); // 输出: Value: 3.14, Integer: 1006. 实战场景串联API与数据结构的组合拳理论知识需要结合题目才能融会贯通。我们来看几个经典场景看看如何运用这些“兵器”。6.1 场景一统计频率与Top K问题题目特征需要统计元素出现次数并可能根据频率进行排序或选取。核心武器HashMapPriorityQueue或Bucket Sort桶排序。示例前K个高频元素使用HashMap统计每个数字出现的频率。使用PriorityQueue最小堆维护频率最高的K个元素。堆内按频率排序堆顶是频率最小的元素。遍历HashMap的条目集entrySet若堆大小小于K直接加入否则比较当前元素频率与堆顶频率若更大则弹出堆顶加入当前元素。最后堆中剩下的就是频率最高的K个元素。public int[] topKFrequent(int[] nums, int k) { // 1. 统计频率 MapInteger, Integer frequencyMap new HashMap(); for (int num : nums) { frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) 1); } // 2. 使用最小堆按频率排序 PriorityQueueMap.EntryInteger, Integer minHeap new PriorityQueue( Comparator.comparingInt(Map.Entry::getValue) ); // 3. 维护大小为K的堆 for (Map.EntryInteger, Integer entry : frequencyMap.entrySet()) { minHeap.offer(entry); if (minHeap.size() k) { minHeap.poll(); // 弹出频率最小的 } } // 4. 提取结果 int[] result new int[k]; for (int i k - 1; i 0; i--) { result[i] minHeap.poll().getKey(); // 注意堆顶是频率最小的所以倒序放入 } return result; }技巧这里使用Map.Entry直接放入堆中避免了创建额外类。Comparator.comparingInt(Map.Entry::getValue)是Java 8的函数式写法非常简洁。6.2 场景二区间合并与日程安排题目特征给出一组区间需要合并重叠区间、插入新区间或查找空闲时间。核心武器排序 线性扫描或TreeMap。示例合并区间将所有区间按照起始时间排序Arrays.sort(intervals, (a, b) - a[0] - b[0])。初始化一个结果列表放入第一个区间。从第二个区间开始遍历比较当前区间与结果列表中最后一个区间如果当前区间起始时间 最后一个区间的结束时间说明重叠则更新最后一个区间的结束时间为两者结束时间的最大值last[1] Math.max(last[1], current[1])。否则不重叠将当前区间加入结果列表。public int[][] merge(int[][] intervals) { if (intervals.length 1) return intervals; // 按起始时间排序 Arrays.sort(intervals, (a, b) - Integer.compare(a[0], b[0])); Listint[] merged new ArrayList(); merged.add(intervals[0]); for (int i 1; i intervals.length; i) { int[] last merged.get(merged.size() - 1); int[] current intervals[i]; if (current[0] last[1]) { // 重叠 last[1] Math.max(last[1], current[1]); // 合并取最大的结束时间 } else { merged.add(current); // 不重叠直接加入 } } return merged.toArray(new int[merged.size()][]); }技巧对于更复杂的日程安排问题如LeetCode 729, 731TreeMap键为时间点值为状态或TreeSet存储已安排的区间起点能提供floorKey/ceilingKey等高效查询是更优解。6.3 场景三字符串的排列、子串与滑动窗口题目特征在字符串中寻找满足某些条件的子串或排列。核心武器滑动窗口 HashMap或数组记录字符频率。示例找到字符串中所有字母异位词使用一个固定长度的滑动窗口长度等于目标词p的长度。用两个int[26]数组或HashMap分别记录窗口内字符计数和目标词p的字符计数。初始时统计p的计数并初始化第一个窗口的计数。滑动窗口每次右移一位移除左边出去的字符加入右边新进入的字符并比较当前窗口计数是否与目标计数相等。public ListInteger findAnagrams(String s, String p) { ListInteger result new ArrayList(); if (s.length() p.length()) return result; int[] pCount new int[26]; int[] windowCount new int[26]; // 初始化p的计数和第一个窗口的计数 for (int i 0; i p.length(); i) { pCount[p.charAt(i) - a]; windowCount[s.charAt(i) - a]; } if (Arrays.equals(pCount, windowCount)) { result.add(0); } // 滑动窗口 for (int i p.length(); i s.length(); i) { // 移除窗口左端字符 windowCount[s.charAt(i - p.length()) - a]--; // 加入窗口右端新字符 windowCount[s.charAt(i) - a]; // 比较计数数组 if (Arrays.equals(pCount, windowCount)) { result.add(i - p.length() 1); } } return result; }技巧使用长度为26的数组代替HashMap来统计小写字母频率效率更高。Arrays.equals用于快速比较两个数组内容是否一致。7. 性能优化与避坑指南知道API怎么用只是第一步用得好、用得对才是关键。这里总结一些实战中容易忽略的性能陷阱和最佳实践。7.1 集合初始化与容量预估容量如前所述为ArrayList、HashMap、HashSet等预估并设置初始容量initialCapacity和负载因子loadFactor对于HashMap能有效避免扩容带来的开销。对于HashMap如果你知道大概有100个元素设置new HashMap(128)找一个大于100的2的幂比默认的16要好。谨慎使用LinkedList除非确需频繁的中间插入删除否则优先使用ArrayList或ArrayDeque。遍历选择遍历ArrayList用索引或forEach遍历LinkedList用Iterator或forEach避免用get(index)。7.2 字符串操作无脑用StringBuilder在循环内或复杂逻辑中拼接字符串永远首选StringBuilder。警惕substring的内存持有在旧版本Java中substring会共享原字符串的char[]可能导致内存泄漏如果原字符串很大截取很小一段却无法被GC。Java 7以后通常已优化但了解这个历史问题有益无害。在极端性能敏感场景可以考虑new String(str.substring(...))来强制拷贝。split的极限情况String.split()的参数是正则表达式。像split(.)、split(|)会出错因为.和|是正则元字符需要转义split(\\.)、split(\\|)。7.3 自动装箱与拆箱集合类如ListInteger,MapInteger, ...存储的是对象Integer而我们的操作常常是int。这中间涉及自动装箱int-Integer和拆箱Integer-int。在循环中频繁操作可能会产生大量临时对象影响性能。在性能瓶颈处可以考虑使用原始类型数组如int[]代替ListInteger。7.4 比较器与相等性Comparator的链式调用Java 8引入了Comparator.comparing、thenComparing等方法可以轻松构建复杂的比较逻辑。// 按年龄升序年龄相同按姓名降序 people.sort(Comparator.comparingInt(Person::getAge) .thenComparing(Comparator.comparing(Person::getName).reversed()));重写equals必须重写hashCode这是HashMap、HashSet正确工作的铁律。两个equals的对象必须有相同的hashCode但hashCode相同的对象不一定equals。7.5 并发修改异常在使用Iterator遍历集合如ArrayList,HashMap时如果直接调用集合自身的add或remove方法修改集合会抛出ConcurrentModificationException。正确的做法是使用Iterator自身的remove方法或者使用Java 8的Collection.removeIf方法。8. 总结与持续更新这份“兵器谱”的整理源于我多年刷题、面试和开发中的切身体会。Java标准库非常强大但只有深入理解其设计意图、性能特性和使用场景才能在算法竞技和工程实践中游刃有余。记住工具是死的人是活的。最好的学习方式是在理解原理的基础上多动手实践把每个API在具体的题目中用上几遍自然就熟了。我会持续关注LeetCode的新题型和社区讨论将更多实用的技巧和易错点补充进来。如果你有特别想了解的API场景或者发现了文中的任何错误与不足也欢迎随时交流。刷题之路道阻且长但装备精良总能走得更稳、更快。