程序员老鬼

某字节员工爆料:我们运营负责人,44岁,手里管:着几十个项目。他定了个死规矩:每周四晚上8点必须离开

刚刷到这个有点绷不住。说某字节运营负责人,44岁,一个人管几十个项目,结果定了个规矩:每周四晚上8点必须离开公司

Image

明明是加班常态的地方,突然搞个“固定下班时间”,有点反直觉。你说是反内卷,还是单纯把节奏当管理手段,也不好说。

下面的人估计挺分裂,一边项目还在爆,一边领导先走了。打工人看了大概也就两种反应:要么羡慕,要么开始琢磨周四是不是才是最要命的一天。

今日面试题

一看到“把字符串里的数字替换成 number”,我第一反应不是上正则。

这种题看着小,最容易写出一段很舒服但不太稳的代码:

s = s.replaceAll("[0-9]", "number");

能跑,但我一般不这么写。算法题里没必要把正则引擎拉进来,而且这种写法把问题藏掉了:它到底扫了几遍?每次匹配怎么处理?字符串中间扩容多少次?这些东西面试官要是追一句,就容易虚。

这题的输入一般长这样:

a1b2c3

输出:

anumberbnumbercnumber

规则很简单:遇到字母,原样放进去;遇到数字,放入字符串 number。

所以我会直接按字符扫一遍。

不要在原字符串上改。Java 的 String 是不可变对象,你每拼一次,其实都是新对象。小数据看不出来,大一点就开始浪费。

关键代码就这么几行:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

publicclassMain{

publicstaticvoidmain(String[] args)throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String raw = reader.readLine();

        StringBuilder fixed = new StringBuilder(raw.length() * 2);

for (int i = 0; i < raw.length(); i++) {
char ch = raw.charAt(i);

if (ch >= '0' && ch <= '9') {
                fixed.append("number");
            } else {
                fixed.append(ch);
            }
        }

        System.out.println(fixed);
    }
}

这里有个小地方我会刻意这么写:

ch >= '0' && ch <= '9'

而不是:

Character.isDigit(ch)

不是说 Character.isDigit 错,而是这道题通常限定的是英文小写字母和数字,数字就是 ASCII 里的 0 到 9。用范围判断更直接,不会把 Unicode 里一些奇怪的数字字符也算进去。

StringBuilder 的初始容量这里写了:

raw.length() * 2

这不是精确值,只是少扩几次容。最精确的做法是先扫一遍统计数字个数,再算最终长度:

最终长度 = 原长度 + 数字个数 * 5

因为一个数字原来占 1 个字符,替换成 number 以后占 6 个字符,多出来 5 个。

但这题没必要搞两遍。除非输入特别大,或者你明确被要求原地扩容处理,否则一遍扫描已经够干净了。

时间复杂度就是 O(n),每个字符只看一次。

空间复杂度也是 O(n),因为结果字符串肯定要重新存一份。别在这里硬说 O(1),输出本身就变长了。

这题真正想考的不是“会不会替换”,而是你写代码时会不会下意识避开低效字符串拼接。

比如下面这种,我看到一般会皱一下眉:

String ans = "";
for (int i = 0; i < s.length(); i++) {
    ans += s.charAt(i);
}

这不是不能跑,是跑得没必要。每次 += 都在造新字符串,循环一长,成本就堆上来了。

所以这类字符串处理题,记住一个顺手的排查顺序:

先看是不是逐字符处理。

再看是不是需要扩容。

最后再决定用 StringBuilder,别上来就正则一把梭。

小题写顺手了,大字符串清洗、日志字段替换、接口参数脱敏,都是这套东西。