1. 整体思路:工程代码和竞赛代码,到底差在哪
1.1 为什么工程开发者看竞赛题解会觉得别扭
我一直有个感受:做过几年Java后端再回头刷算法题,第一反应不是"这题不会做",而是"同样的功能,为什么竞赛选手的写法让我不认识了"。
举个很常见的例子。你在业务代码里往HashMap里塞一个List,惯用写法一定是先判断key存不存在:
Map<String, List<Integer>> map = new HashMap<>(); if (!map.containsKey(key)) { map.put(key, new ArrayList<>()); } map.get(key).add(value);这套逻辑写起来顺手,读起来也清楚。但竞赛选手一行就搞定了:
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);函数式接口、lambda、默认方法,全都怼在一起。不是说它装,而是竞赛场景里每一行代码都在为"代码量和可读性之间找平衡",整个编码习惯和工程开发完全不是一套思维模式。
好在这篇文章不是教你打比赛,而是站在工程开发者的视角,把竞赛里最常用的Java数据结构和操作整理成一份"助记版"笔记。你不需要为了刷题去改变自己多年的工程习惯,只需要建立一张映射表:这个常见需求在竞*赛代码里通常怎么写,背后用到了哪个类的哪个方法,边界条件在哪。理解了这个对应关系,再去看题解代码,障碍就消掉了一大半。
1.2 Java在算法竞赛里的优劣势
Java在算法竞赛里其实是"能打但不占便宜"的语言。优势在于容器类库非常完善,HashMap、TreeMap、PriorityQueue都是开箱即用,比C++的STL某些时候还直观;劣势在于常数比较大,同样一个O(NlogN)的排序,C++可能跑0.5秒,Java跑到1秒出头很正常,再加上JVM启动时间,线上比赛的体验确实不占优。
但换个角度看,Java刷算法题有一个工程开发者无法拒绝的理由:面试要考。现在大厂的技术面试圈子里,白板写题的主流语言就是Java,用Java刷题等于把面试语言、工程语言、刷题语言统一成了一种,切换成本最低。而且Java的容器类在工程代码里也普遍使用,多熟悉一层竞赛场景的用法,对写业务代码里的复杂逻辑也有帮助。
1.3 助记的核心原则
整理这份笔记时,我给自己定了三条原则:
- 按"操作需求"分类,而不是按"类名"硬记。比如"需要有序的键值对"对应TreeMap,"需要自动排序的队列"对应PriorityQueue,先有需求再找工具,而不是反过来背API。
- 工程写法和竞赛写法做对照。每个操作都给出两种写法,你只要曾经写过工程代码,就能从对照中快速理解竞赛写法的来源。
- 把时间复杂度放在最显眼的位置。竞赛代码的核心是"在规定时间内跑完",任何一个数据结构的选择都跟复杂度强相关,这个思维必须建立起来。
下文所以内容都用Java 8+的语法,这也是目前线上算法题环境最通用的版本。
2. 容器类:竞赛里高频操作的速记手册
2.1 ArrayList:不只是"自动扩容的数组"
ArrayList在工程开发里是List接口的默认实现,大多数人拿它当"可变的数组"用。在竞赛场景里,它的角色其实更微妙:因为竞赛题绝大多数是静态数据读入后就不再增删,理论上用原生数组是最快的,但原生数组不方便扩容、不方便传参、没有丰富的方法,所以ArrayList反而成了高频妥协方案。
竞赛中几个容易忽视的ArrayList操作:
// 排序 List<Integer> list = new ArrayList<>(); Collections.sort(list); // 升序 Collections.sort(list, Collections.reverseOrder()); // 降序 // 列表间批量添加 list.addAll(otherList); // 转为数组(注意参数是new Integer[0]不是new Integer[n]) Integer[] arr = list.toArray(new Integer[0]); // 需要注意:list.toArray()返回的是Object[],直接强转会报错这里有个真正值得记住的点:toArray(new Integer[0])这个写法,不少工程开发者会写成list.toArray(new Integer[list.size()]),性能上是前者更好。原因在JVM的优化机制——new Integer[0]只需要一个空数组做类型标记,JDK内部判断后直接新建正确大小的数组返回,逻辑更清爽。
还有ensureCapacity这个方法,平时几乎没人用。但如果你提前知道最终容量,比如读数据时已经知道行数,调用list.ensureCapacity(n)可以避免中间多次扩容的数组拷贝。数据量上了百万以后,这个微优化在竞赛场景里能省下几十毫秒,值得养成习惯。
ArrayList和LinkedList的选择也是竞赛新手最容易纠结的。我把结论说透:绝大多数情况下选ArrayList。LinkedList的随机访问是O(N),在需要按下标操作的题里完全没法用;而ArrayList哪怕是做头部插入,如果数据量小也看不出差别,数据量大时LinkedList的节点对象本身又吃内存。真正需要使用Deque(双端队列)语义时,用ArrayDeque,不要用LinkedList。
2.2 HashMap:默认方法才是竞赛的灵魂
HashMap在工程开发里最常用的就是put、get、containsKey、size这几个。但在竞赛代码里,几个Java 8引入的默认方法才是真正拉高效率的地方,它们能把三行样板代码缩成一行,而且语义清晰。
我列一下刷题中出镜率最高的三个:
computeIfAbsent(key, mappingFunction):key不存在时才执行函数并放入,返回当前key对应的值。最常用于分组、建邻接表。
// 需求:把每个节点的邻接节点塞进列表 Map<Integer, List<Integer>> graph = new HashMap<>(); for (int[] edge : edges) { graph.computeIfAbsent(edge[0], k -> new ArrayList<>()).add(edge[1]); graph.computeIfAbsent(edge[1], k -> new ArrayList<>()).add(edge[0]); }merge(key, value, remappingFunction):key不存在时放入value,存在时用函数合并。最经典的是计数。
// 需求:统计每个字符出现次数 Map<Character, Integer> count = new HashMap<>(); for (char c : s.toCharArray()) { count.merge(c, 1, Integer::sum); }getOrDefault(key, defaultValue):有值取值,无值取默认值。这句其实在JDK 8之前就有,但竞赛里很多选手依然习惯先判断再取,其实一行能搞定。
还有一个细节很多人踩坑:HashMap的遍历顺序是"无序"的。如果你需要按插入顺序或访问顺序遍历,要用LinkedHashMap;如果确信数据量很小(比如不超过100个key),HashMap和LinkedHashMap的性能没有本质差别,但LinkedHashMap能帮你省掉调试时"为什么顺序不对"的烦恼。
在竞赛里,HashMap最常见的场景是"去重"和"计数"。去重完全可以用HashSet,但计数需要HashMap。这里有个工程开发者也容易忽略的特性:HashMap的key如果是自定义对象,必须同时重写hashCode和equals,否则查不到也是正常的。竞赛中为了避开这个坑,绝大多数人会把key设计成String、Integer这些基础包装类——这也是为什么你看题解代码时,他们总是在"绕着弯子"把复杂对象转成字符串再塞进Map。
2.3 TreeMap和TreeSet:有序性的威力
TreeMap和TreeSet的核心能力是"键有序",底层红黑树,所有操作O(logN)。竞赛题中只要出现"找比某个数大的最小值"或"找比某个数小的最大值"这类最近邻查询,这几个类就是标准答案。
TreeMap的常用方法:
TreeMap<Integer, String> map = new TreeMap<>(); map.firstKey(); // 最小的key map.lastKey(); // 最大的key map.ceilingKey(5); // 大于等于5的最小key,没有返回null map.floorKey(5); // 小于等于5的最大key,没有返回null map.higherKey(5); // 严格大于5的最小key map.lowerKey(5); // 严格小于5的最大key记住ceiling是"向上取",取的是"不小于给定值";floor是"向下取",取的是"不大于给定值"。中文翻译成"天花板"和"地板"就很好记了。这一组方法在处理区间合并、滑动窗口、日程冲突检查(区间是否重叠)时特别好用。
TreeSet和TreeMap用法对称,只是没有value。比如说"维护一个有序集合,随时取出最大/最小元素并删除",TreeSet可以做到:
TreeSet<Integer> set = new TreeSet<>(); set.add(10); set.add(3); set.add(7); int max = set.last(); // 10 set.remove(set.last()); // 移除最大 int min = set.first(); // 3和PriorityQueue不一样的是,TreeSet不止能取最值,还能查询"在集合中哪个范围内"的元素,这是堆做不到的。代价是插入和删除的常数比堆略大一些。
2.4 PriorityQueue:默认是小顶堆,这一点别记反
PriorityQueue这个类名特别容易被工程开发者想当然:Priority不是"优先级"吗,那不应该是大的先出?不对,Java的PriorityQueue默认是小顶堆,也就是值最小的元素在队头。这个点我见过太多人搞反,一写就错。
自定义顺序有两种方式。一种是用Collections.reverseOrder():
// 大顶堆 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());另一种是自己实现Comparator:
// 小顶堆(默认) PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 自定义(比如按绝对值大小) PriorityQueue<Integer> absHeap = new PriorityQueue<>((a, b) -> Math.abs(a) - Math.abs(b));堆在竞赛里的出场频率极高,TopK问题、合并K个有序链表、Dijkstra、Prim、任务调度等等,全都是堆的经典应用。核心操作就三个:offer(入堆)、poll(出堆头)、peek(看堆头),全部O(logN)。
工程开发者和竞赛选手在堆的思维上一个很大的差异是:工程开发者倾向"完整实现一个类",竞赛选手习惯"只关心数据进出顺序"。
举一个高频的TopK写法:
// 求数组里最大的K个数,维护一个大小为K的小顶堆 PriorityQueue<Integer> minHeap = new PriorityQueue<>(k); for (int num : nums) { if (minHeap.size() < k) { minHeap.offer(num); } else if (num > minHeap.peek()) { minHeap.poll(); minHeap.offer(num); } } // 堆里剩下的就是最大的K个数注意堆里的元素数量如果一直没超过K,不需要poll;只有超过或者peek值比当前数小才更新。这个题用大顶堆也能做但复杂度高,因为得把全部数据都放进堆再来K次poll;用小顶堆每次只淘汰堆内最小的,最终堆里留着最大的K个,两者相比差距就出来了。
3. 字符串、数组与类型转换:竞赛里最常见的暗坑
3.1 StringBuilder:字符串操作的第一选择
Java里的String是不可变的,所以循环里直接拼字符串,等于每拼一次都创建一个新对象,O(N^2)的时间逃不掉。这个话我在工程代码评审里讲过无数遍,在竞赛场景里更是致命的——数据量一大,TLE(超时)就来了。
竞赛里StringBuilder的最高频场景有三个:
第一是拼接:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { sb.append(arr[i]); } String result = sb.toString();第二是反转。String本身没有reverse方法,StringBuilder有:
String reversed = new StringBuilder(s).reverse().toString();注意reverse()是反转整个字符序列,不是反转单词顺序,两者别搞混。
第三是修改某个位置的字符。String这个操作几乎做不了——要转字符数组再改再转回String,而StringBuilder直接用setCharAt:
sb.setCharAt(i, 'x');这里我再多提醒一句:StringBuilder的初始容量默认是16,如果提前知道要累积大量内容,构造时传入初始容量,比如new StringBuilder(totalLength),能省掉扩容时内部数组拷贝的开销。这个和ArrayList的ensureCapacity是同一个思路。
3.2 字符串与数值转换的几种写法
竞赛题里,把"数字字符串"转成int,或者把int变成字符串,这种操作太频繁了,但写法上有几个易错的细节。
字符串转数字:
String s = "12345"; int a = Integer.parseInt(s); // 推荐 long b = Long.parseLong(s); // 用long接更大范围 // 二进制解析 int bin = Integer.parseInt("1010", 2); // 10数字转字符串:
int num = 123; String s1 = Integer.toString(num); String s2 = String.valueOf(num); String s3 = num + ""; // 能用,但不优雅,而且会在循环里产生额外对象字符串转字符数组,以及字符转数字,这两个操作也极其高频:
char[] chars = s.toCharArray(); // 遍历时要注意:char是不能直接参与算术的,要想清楚要不要-'0' int digit = s.charAt(i) - '0';这里有一个竞赛新手特别容易忘记的点:char类型本质上是无符号整数,'9' - '0'才是数字9,直接用Integer.parseInt(String.valueOf(s.charAt(i)))效率极低,还容易出错。同理,把一个小写字母转成它在字母表中的序号时,c - 'a'起步是0,转大写是c - 'A'。
3.3 字符数组与字符串互转
单纯是字符串层面解决不了问题时,工程思维马上就会想到"转成可变结构"——在Java里这个可变结构通常是char数组。
String s = "acbd"; char[] arr = s.toCharArray(); Arrays.sort(arr); // 原地排序,O(NlogN) String sorted = new String(arr); // 排序后的字符串这个"字符串转字符数组、排序、再转回字符串"的组合是判断两个字符串是否由相同字符组成(字母异位词)的经典解法之一,另一套是用HashMap计数,两者各有适用场景。字符数组的优势在于它不产生额外存储对象的开销,排序后直接得到一个规整的String。
我多次用到字符数组后总结出一个习惯:只要题目需要操作字符串中的"单个字符",且不止一次修改——就先转成char[],处理完再用new String(arr)转回来。工程代码里写这个会被人吐槽风格奇怪,但竞赛场景里这是最直观的写法了。
3.4 BigInteger:工程开发者容易忽略的大数解法
Java的BigInteger在竞赛题里是"保底方案"。它最纯粹的价值在于:不管整数多大,都能精确表示,不会溢出。但代价是慢,而且不慢一点点,是比原生long慢几个数量级。
适合用BigInteger的场景有两类:
第一类是题目明确给的数值范围超出了long(约9.2×10^18),比如求高精度幂、超大数的加减乘除。
第二类是涉及超大范围的素性判断或求最大公约数。BigInteger内置了isProbablePrime和gcd,用起来比手写Miller-Rabin踏实得多。
BigInteger a = new BigInteger("123456789012345678901234567890"); BigInteger b = new BigInteger("987654321098765432109876543210"); BigInteger sum = a.add(b); BigInteger prod = a.multiply(b); BigInteger g = a.gcd(b); boolean prime = a.isProbablePrime(100); // 100是确定性参数,越大越精确,但耗时也越高BigInteger的构造函数也值得注意:new BigInteger(String)是十进制,new BigInteger(String, 2)可以读二进制。还有一种常用的构造是BigInteger.valueOf(long),可以直接传入long。
我踩过的坑是:用BigInteger做循环时,顺手写了一堆new BigInteger("1")然后加,结果慢到我怀疑人生。大数据场景建议直接预先把常量的BigInteger存成静态变量复用:
private static final BigInteger ONE = BigInteger.ONE; private static final BigInteger ZERO = BigInteger.ZERO;4. 竞赛常用算法操作与实用模板
4.1 排序与自定义比较器
排序是整个算法竞赛里最基础的"基础设施"。Java给两种排序:数组用Arrays.sort,List用Collections.sort,底层都是TimSort,性能稳定。
但有一个Java独有的坑,必须高度警惕:Arrays.sort对基本类型数组和对象数组的处理逻辑完全不同。基本类型数组(int[]、long[]等)的sort用的是快速排序算法,只能升序,不接受Comparator参数;对象数组(比如Integer[])的sort用的是归并排序的变体,可以传Comparator。
这就导致了一个让无数人困扰的现象:
// 这会编译报错!int[] 无法搭配lambda Arrays.sort(arr, (a, b) -> b - a);正确做法是转成Integer[]:
Integer[] arr = {3, 1, 4, 1, 5}; Arrays.sort(arr, (a, b) -> b - a); // 降序或者使用Arrays.stream的装箱再排序,但那样开销更大。竞赛中如果不想装箱,有一个实用技巧——先升序排,再手动反转前一半,但通常多此一举。个人建议:数据量不超过10^5时直接转包装类排序完全够用。
自定义对象排序在竞赛里多为二维数组按某个维度排序。最常见的写法:
int[][] intervals = new int[n][2]; Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // 按第一维升序 // 注意:不要写成 a[0] - b[0],如果差值超过int范围会溢出这个Integer.compare比直接减法的好处就是防止溢出。很多人在大数排序时莫名其妙出错,最后定位到就是这里。
4.2 二分查找:会用Arrays.binarySearch,更要会手写
Java的Arrays.binarySearch和Collections.binarySearch是现成的二分查找工具,但工程思维容易忽略一个关键点:这个方法在找不到目标时返回的不是-1,而是-(插入点) - 1。
举个具体例子:
int[] arr = {1, 3, 5, 7}; int idx = Arrays.binarySearch(arr, 4); // 返回 -3,因为它应该插在下标2的位置,结果是 -(2) - 1 = -3很多题解里会用到这个返回值来定位"插入点",但把它当普通查找用就会出错。如果你只想判断"是否存在",那>= 0检查一下就行。
竞赛里更常见的需求其实是"找左边界"和"找右边界",也就是lowerBound和upperBound。Arrays.binarySearch没法直接解决这类问题,所以很多选手会选择手写:
static int lowerBound(int[] arr, int target) { int l = 0, r = arr.length - 1; while (l < r) { int mid = l + (r - l) / 2; // 防溢出写法 if (arr[mid] >= target) { r = mid; } else { l = mid + 1; } } return arr[l] >= target ? l : arr.length; }这个模板的记忆方式很简单:lowerBound找的是"第一个>=target的位置",upperBound找的是"第一个>target的位置",只需要把arr[mid] >= target改成arr[mid] > target即可。
手写时我还想额外提醒:mid用l + (r - l) / 2而不是(l + r) / 2,因为后者在l和r都特别大时可能溢出。这个坑虽然竞赛题给的数据范围不一定能触发,但养成习惯没有坏处。
4.3 位运算助记
位运算在竞赛里像暗器一样,平时不用,用到就要命一样好使。工程开发者可能一年写不了几处位运算,但刷题时必须知道几个高频模板。
判断奇偶:
if ((n & 1) == 1) { // 奇数 }乘2除2:
int x = n << 1; // n*2,注意溢出风险 int y = n >> 1; // n/2(向下取整,负数会出问题)提取最右边的1(lowbit):
int lowbit = n & (-n);这句话看起来简单,它背后是"取反加一"两步操作把符号位利用起来的效果。lowbit在树状数组里是核心操作,只要涉及区间和查询,这一行就能派上大用场。
异或运算的性质:a ^ a = 0,a ^ 0 = a。最经典的题就是"数组里只有一个数出现一次,其余都出现两次",用这个性质一遍循环就能找出来:
int result = 0; for (int num : nums) { result ^= num; } return result;位运算还有一个工程开发者容易忽略的优势:它就是为底层性能而生的,比加减乘除都快。
4.4 快读快写模板
Java在竞赛里最大的劣势就是I/O慢。Scanner读1万行数据没问题,但读100万行就会明显拖慢整个程序。如果你决定用Java打比赛或应对面试里的OJ环境,强烈建议直接用快读模板。
我用过的最顺手的快读方案是BufferedReader+StringTokenizer+StringBuilder的组合:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(br.readLine()); int n = Integer.parseInt(st.nextToken()); // 循环读大量数据时,用StringBuilder收集输出 StringBuilder sb = new StringBuilder(); while (st.hasMoreTokens()) { int a = Integer.parseInt(st.nextToken()); sb.append(a).append('\n'); } System.out.print(sb);这里有几个优化的细节:
- 不要用System.out.println直接逐行输出,它在每次调用时都有一次IO操作。积少成多,数据量大时比StringBuilder攒着统一输出慢一个数量级。
- StringTokenizer比String.split()快,后者基于正则,开销大得多。
- 如果题目数据量特别大,甚至可以直接用
BufferedReader逐字节读入再手动解析,这个就属于进阶玩法,正常快读模板已经够用。
也有人会封装一个FastScanner类,把nextInt、nextLong、nextDouble都实现进去。这个class在竞赛圈基本人手一个,核心思想就是维护一个buffered char数组,用下标扫描。我一般比赛时用一个稍微精简的版本来回复用。
5. 常见问题与排查技巧实录
5.1 超时的几个隐蔽根源
Java提交后显示TLE(Time Limit Exceeded),大多数问题不在算法复杂度,而在IO或常数上。这是我多次实战后总结的几个高频超时原因,建议按顺序排查。
第一,Scanner读入。这个最快暴露,数据量超过10^6时Scanner比BufferedReader慢5到10倍,是最大的常数瓶颈。第二,System.out.println逐行输出。这个比Scanner更隐蔽,因为输出量少时感受不到,一旦大量输出就立刻卡住。第三,无意识的装箱拆箱。比如把int放进List 时反复自动装箱,在小数据量没事,大数据量的循环里会产生大量对象。第四,String拼接。循环里用"+"拼接就是灾难。
排查时有个经验法则:先检查IO,再检查是否有高频创建对象,最后再看算法复杂度是不是真的错了。我自己就有过"算法是对的却反复TLE"的经历,最后定位在Scanner上,换成快读后直接压线通过。
5.2 容器拷贝与内存陷阱
工程开发里,List的拷贝大家都习惯用new ArrayList<>(original),或者Map用putAll,这都没问题。但竞赛里有一个特别致命的陷阱:这种拷贝是浅拷贝。
如果原List里装的是自定义对象,那么新List里的元素和旧List指向的是同一个对象引用。修改其中一个会影响另一个。如果你需要"复制一个列表然后改一版再对比",必须想清楚深层拷贝要不要做。
另外还有一个很隐蔽的内存陷阱:Arrays.asList生成的List是固定大小,调用add或remove会抛UnsupportedOperationException,但很多人会拿它当普通List使,报错了才反应过来。竞赛中如果你只是想快速初始化一个List,用这个可以,但别修改它。
内存溢出场景最典型的还是数组开太大。有些人在方法内部声明int[][] dp = new int[100000][100000],这种在Java里肯定OutOfMemoryError,因为二维数组每个一维数组都是一个对象,对象头还有额外开销。处理办法是把二维"拍扁"成一维索引,用i * cols + j定位。
5.3 比较器相关的三个坑
比较器这块是Java竞赛代码里翻车率最高的,我逐一记录一下。
坑一:基本类型数组传不了Comparator。前面提过了,Arrays.sort(int[], (a,b)->...)编译不过,这是Java的设计限制。尽量用包装类数组或者提前转类型。
坑二:Comparator的返回值含义容易被记反。a.compareTo(b)返回负数是a在前?注意是很绕的:Comparator的compare(a, b)返回负数表示a排在b前面。这个可以这么助记:返回负数就往"前"排,返回正数就往"后"排,返回0就俩一样。如果你排出来顺序反了,就把a和b对调一下即可。
坑三:return a - b有溢出风险。一个极其极端的例子:Integer.MIN_VALUE - Integer.MAX_VALUE会溢出成一个很大的数。推荐永远用Integer.compare(a, b)或Long.compare(a, b),这两个方法内部用的是不溢出的比较逻辑,而且代码也简洁清晰。
5.4 数据结构操作速查助记表
最后放一张我实际刷题时用得很频繁的速查表,你可以存下来,想不起来的时候翻一眼:
| 需求 | 推荐结构 | 核心操作 | 时间复杂度 |
|---|---|---|---|
| 动态数组/随机访问 | ArrayList | add, get, set, sort | O(1)按索引 |
| 频繁头尾增删 | ArrayDeque | offerFirst, pollLast | O(1) |
| 键值对统计 | HashMap | merge, getOrDefault | 平均O(1) |
| 需要有序的键值对 | TreeMap | ceilingKey, floorKey, firstKey | O(logN) |
| 去重 | HashSet | add, contains, size | 平均O(1) |
| 有序去重集合 | TreeSet | first, last, ceiling | O(logN) |
| 取最小/最大值 | PriorityQueue | offer, poll, peek | O(logN) |
| 字符串拼接/反转 | StringBuilder | append, reverse | 均摊O(1) |
| 大数运算 | BigInteger | add, multiply, gcd | 位数相关 |
这张表的记忆逻辑其实很简单:需要单点随机访问,就选ArrayList;需要按大小顺序访问,就选Tree系列;需要最值快速进出,就选堆;需要一键去重计数,就选Hash系列。数据结构选型这件事,本质就是把"操作需求"翻译成"对应结构的时间复杂度"。
写在最后的一点经验
我整理这份助记版的初衷,其实来源于刷题群里经常看到的一句话:"代码我都认识,合在一起就不知道什么意思。"Java的容器类API非常多,但竞赛真正高频的就那么十几个方法。与其继续记API手册,不如做减法,把二十多个最常用的方法固化成肌肉记忆。
实际操作中最想强调的是:不要试图把所有方法背下来再去刷题,而是用几道经典题带出自己的盲区。我自己的路径就是先从HashMap和PriorityQueue入手,把TopK、分组、滑动窗口这类基础题跑通,然后再逐步引入TreeMap和高级位运算。每遇到一个新需求,就查一次表,查完用一次,两三次之后自然就记住了。
如果这篇文章能帮你在面对竞赛题解时少一点陌生感,那这个助记版就算达到目的了。最后再补一句题外话:工程思维和竞赛思维完全可以共存,理解两者差异本身就是一种复合能力,这个能力在写复杂业务逻辑时,反而会转化成你的优势。