工作8年没涨工资,面试外企给涨了50%。为了给领导面子,我说内部涨薪30%我就留下,结果领导说:就你这水平,涨15%都多!
工作8年没涨工资,面试外企给涨了50%。为了给领导面子,我说内部涨薪30%我就留下,结果领导说:就你这水平,涨15%都多!
这话听着就离谱。你要是真觉得人不行,早干嘛去了?8年让人干着,关键时候又嫌人水平差,这不是评价能力,这是压价话术。
面子这东西,有些时候真没必要替别人顾。人家压你工资的时候,可没替你房租、生活费、未来想过多少。
能给你涨50%的地方都出现了,还在这儿听15%都多,属实没必要。
堆栈里抛出的递归超时,看着像经典的 Fibonacci 或者斐波那契型计算,但其实是我第一眼就不敢用纯递归。今天聊聊“记忆函数”——就是把递归结果缓存起来,少走重复路。
我碰到的例子是计算阶乘变体、或者一些动态规划的状态函数。典型场景是:同一个函数会被反复调用,参数重复率极高。如果不缓存,每次都走一遍递归树,时间复杂度直接指数级,线上就会卡死。
先上最原始的递归:
publicclassFib{
publicstaticlongfib(int n){
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
publicstaticvoidmain(String[] args){
System.out.println(fib(40)); // 等得心累
}
}
40一下就能看到 CPU 燃烧的感觉。原因是 fib(38) 被计算两次,fib(37) 被计算三次……递归树指数级膨胀。
这时候记忆函数(Memoization)就派上用场了,思路就是给函数加一层缓存。Java 实现里最常用是 Map 或者数组,按参数索引存储结果:
import java.util.HashMap;
import java.util.Map;
publicclassMemoFib{
private Map<Integer, Long> cache = new HashMap<>();
publiclongfib(int n){
if (n <= 1) return n;
if (cache.containsKey(n)) return cache.get(n);
long result = fib(n - 1) + fib(n - 2);
cache.put(n, result);
return result;
}
publicstaticvoidmain(String[] args){
MemoFib mf = new MemoFib();
System.out.println(mf.fib(100)); // 秒出结果
}
}
注意,我这里用 Map 而不是数组,因为有些函数的参数不是连续整数,或者是组合型参数,比如 (i, j, k) 三维状态,数组开多维比较麻烦。
比如我在做一个棋盘路径计数,函数形如 ways(x, y)。如果不缓存,每个 (x, y) 状态都重复计算很多次:
publicclassChessPaths{
private Map<String, Integer> memo = new HashMap<>();
publicintways(int x, int y){
if (x == 0 && y == 0) return1;
if (x < 0 || y < 0) return0;
String key = x + "," + y;
if (memo.containsKey(key)) return memo.get(key);
int count = ways(x - 1, y) + ways(x, y - 1);
memo.put(key, count);
return count;
}
publicstaticvoidmain(String[] args){
ChessPaths cp = new ChessPaths();
System.out.println(cp.ways(20, 20));
}
}
这里你会发现,关键在于“状态唯一标识”。像 x + "," + y 这种组合 key 最方便。实战里我碰到过用对象做 key 的场景,要重写 hashCode() 和 equals(),不然缓存全白搭。
还有一种简单数组缓存方案,适合参数是整数范围已知的情况:
publicclassMemoFibArray{
privatelong[] memo;
publicMemoFibArray(int n){
memo = newlong[n + 1];
for (int i = 0; i <= n; i++) memo[i] = -1;
}
publiclongfib(int n){
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
}
publicstaticvoidmain(String[] args){
MemoFibArray mf = new MemoFibArray(100);
System.out.println(mf.fib(100));
}
}
这类缓存基本就是把递归树扁平化成 DAG,减少重复调用。注意几点坑:
缓存初始化:数组的话初始化为一个不可能的值,如 -1,Map 则不需要。 参数唯一性:记忆函数的 key 一定要保证唯一,否则可能覆盖或错取。 空间开销:缓存是额外消耗,状态数量极大时要考虑 LRU或限制大小。线程安全:如果多线程调用,Map 就需要 ConcurrentHashMap,或者加锁。
总结一句话:记忆函数本质上是手动做动态规划,不用完全改写递归,只是在每个函数入口加个缓存。实战中你会发现,之前秒卡的递归题,一下就秒了,而且代码干净、逻辑直接。