程序员老鬼

在公司干了3年的技术骨干,负责核心业务半夜还要on-call处理线上问题,月薪30K,刚带的应届生,28K的offer进来了。

刚看到个贴子,说有网友在吐槽:公司干了三年的老员工,负责核心业务、半夜还要on-call,月薪30K;结果刚来的应届生,啥都还在学,拿着28K的起薪。这对比确实刺眼。

Image

我觉得这事吧,很多网友骂“不公平”没错,但也得承认,市场定价从来不看“辛苦程度”,看的是“稀缺性”。校招生贵,是因为他们代表公司未来的潜力;老员工便宜,是因为被贴上了“稳定”“可替代”的标签。听着扎心,但现实就是这么冷。

不过话说回来,如果真觉得不值,就别一边委屈一边硬撑。要么换赛道,要么提升溢价。抱怨有用的话,老板早加薪了。

总的来说,努力值得被尊重,但职场只尊重结果和价值。与其生气,不如想办法让自己值更多的钱。【备注:文末可领最新资料】

算法题:字符串转换

咱先约个具体的题目,不然“字符串转换”太宽了。

问题可以这样描述:

给你两个字符串 word1 和 word2,你可以对 word1 做三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 把一个字符替换成别的字符

问:最少需要几步操作,才能把 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 的串,只能一直“插入”,所以是 j
  • dp[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

最后给大家分享一份不错的副业资料,点击下方公众号,回复关键字: 副业 领取,也可以链接我领取,微信:hls404