在公司干了3年的技术骨干,负责核心业务半夜还要on-call处理线上问题,月薪30K,刚带的应届生,28K的offer进来了。
刚看到个贴子,说有网友在吐槽:公司干了三年的老员工,负责核心业务、半夜还要on-call,月薪30K;结果刚来的应届生,啥都还在学,拿着28K的起薪。这对比确实刺眼。
我觉得这事吧,很多网友骂“不公平”没错,但也得承认,市场定价从来不看“辛苦程度”,看的是“稀缺性”。校招生贵,是因为他们代表公司未来的潜力;老员工便宜,是因为被贴上了“稳定”“可替代”的标签。听着扎心,但现实就是这么冷。
不过话说回来,如果真觉得不值,就别一边委屈一边硬撑。要么换赛道,要么提升溢价。抱怨有用的话,老板早加薪了。
总的来说,努力值得被尊重,但职场只尊重结果和价值。与其生气,不如想办法让自己值更多的钱。【备注:文末可领最新资料】
算法题:字符串转换
咱先约个具体的题目,不然“字符串转换”太宽了。
问题可以这样描述:
给你两个字符串 word1 和 word2,你可以对 word1 做三种操作:
插入一个字符 删除一个字符 把一个字符替换成别的字符
问:最少需要几步操作,才能把 word1 变成 word2?
比如:word1 = "intention",word2 = "execution",最少操作次数是 5。 这就是经典的“编辑距离(Edit Distance)”问题。
听着有点抽象,其实你可以把它理解成:两个字符串有多“不一样”,这个数字就是“不一样程度”的量化。
2. 暴力想法为什么不行?
直觉里的想法一般是这样的:
我从左往右看两个字符串:
字符一样就一起往后走 不一样就试着插入、删除、替换,看看哪种更省
问题是: 每一处都可能有三种决策,而且会影响后面怎么走,如果你用暴力回溯去试,分支会爆炸式增长,复杂度非常恐怖,根本跑不动。
所以这个题,基本就是在招手:用动态规划(DP)吧。
3. 动态规划怎么建模?
我们先想一个问题:
如果我只考虑前一部分字符串呢?比如:
word1的前i个字符:word1[0..i-1]word2的前j个字符:word2[0..j-1]
如果我能知道: 把 word1[0..i-1] 转成 word2[0..j-1] 的最小操作数, 那我是不是可以一点点“长大”,直到整个字符串?
于是状态就出来了:
dp[i][j] 表示:
word1 的前 i 个字符
变成 word2 的前 j 个字符
需要的最少操作数
注意:
i表示有几个字符,不是下标所以 word1第i个字符其实是word1.charAt(i - 1)
4. 状态转移:三种操作怎么融进去?
分几种情况看。
1)如果当前两个字符相同
也就是:word1.charAt(i - 1) == word2.charAt(j - 1)
那这两个位置不用操作,答案就等于前面那一段的结果:
dp[i][j] = dp[i - 1][j - 1]
2)如果当前两个字符不同
那我们有三种选择(对应三种操作):
在
word1里 插入 一个字符希望插完之后, word1[0..i]能对齐word2[0..j-1]转移来源: dp[i][j - 1] + 1在
word1里 删除 一个字符删除 word1[i - 1],让word1[0..i-2]去对齐word2[0..j-1]转移来源: dp[i - 1][j] + 1在
word1里 替换 当前字符把 word1[i - 1]改成word2[j - 1],然后前面i-1和j-1对齐转移来源: dp[i - 1][j - 1] + 1
所以当字符不同时:
dp[i][j] = 1 + min(
dp[i - 1][j], // 删除
dp[i][j - 1], // 插入
dp[i - 1][j - 1] // 替换
)
3)边界情况(很容易被忽略)
dp[0][j]:把空串变成长度为j的串,只能一直“插入”,所以是jdp[i][0]:把长度为i的串变成空串,只能一直“删除”,所以是i
5. Java 实现代码
直接上一个完整方法,用二维 DP 表,思路清晰,便于理解:
publicclassEditDistance{
publicstaticintminDistance(String word1, String word2){
int m = word1.length();
int n = word2.length();
// dp[i][j]:word1 前 i 个字符 -> word2 前 j 个字符 的最小操作数
int[][] dp = newint[m + 1][n + 1];
// 初始化第一行第一列
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // 全删
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // 全插
}
// 状态转移
for (int i = 1; i <= m; i++) {
char c1 = word1.charAt(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = word2.charAt(j - 1);
if (c1 == c2) {
// 当前字符相同,不需要操作
dp[i][j] = dp[i - 1][j - 1];
} else {
int insert = dp[i][j - 1] + 1;
int delete = dp[i - 1][j] + 1;
int replace = dp[i - 1][j - 1] + 1;
dp[i][j] = Math.min(insert, Math.min(delete, replace));
}
}
}
return dp[m][n];
}
publicstaticvoidmain(String[] args){
System.out.println(minDistance("intention", "execution")); // 输出 5
System.out.println(minDistance("horse", "ros")); // 输出 3
}
}
这个版本时间复杂度是 O(m * n),空间复杂度也是 O(m * n),在大多数面试题和业务代码里都能接受。
6. 常见坑和一些小优化
随便提几个容易踩的坑:
下标经常搞混: dp用的是长度,字符串访问用i-1/j-1初始化忘记写: dp[0][j] = j、dp[i][0] = i必须有把三种操作的含义搞反:建议自己在纸上画一个小例子推一遍
如果你对空间比较敏感,其实可以把二维数组压成两行(滚动数组),因为 dp[i][j] 只依赖上一行和当前行左边的元素,这个就算进阶版本了,先把二维版真正吃透,再去压缩会更自然。
7. 收个尾:什么时候会用到这个算法?
这个东西不只是“面试题”,在一些实际场景里也会用到,比如:
搜索框的“拼写纠错”:你输 applw,它猜你想搜apple推荐系统里做“相似度”计算:两个标题、两段话相差多少 文本去重、模糊匹配:找出“差不多”的字符串
只要你脑子里有个印象:我可以用“编辑距离”来量化两个字符串的差异,这篇文章的目的就到了。剩下的就是多写几次,自己敲一遍代码、打个断点看着 dp 表怎么填,基本就牢固了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html