程序员老鬼

某大厂员工吐槽:跟父母交谈说今年钱不好挣,我妈说多的不说了,打工一年最少要挣个45十万...

某大厂员工跟父母感慨一句,今年行情差,钱没以前好挣。结果他妈轻飘飘来一句:别的先不说,出去打工一年最少也得挣个四五十万吧。你看,这话一出来,空气都得安静两秒。大厂牛马听完估计都想看看自己工资条,是不是少印了一位数。

Image

评论区也挺真实。有人说,父母那代对“大厂”三个字有滤镜,默认进去就是高管待遇,工牌一挂,年薪自动起飞。

我看这事最魔幻的,不是父母要求高,是他们对职场的认知还停在“找个好单位就能翻身”那套。现在别说四五十万,很多人能稳住不裁员、年终奖别打骨折,都得去庙里谢谢菩萨了。

面试题:统计词频

这个题真不大,但线上一跑,最容易写出一坨“能用但不敢放大流量”的代码。

统计词频,很多人第一反应就是 split + HashMap。本地测几个字符串当然没问题。可一旦文本稍微大一点,标点、大小写、空格、换行、制表符一混进来,结果就开始飘。再狠一点,要求你把高频词排个序,代码味道基本就出来了。

这类题我一般先盯两件事:第一,词到底怎么算一个词;第二,结果要不要稳定。前者决定你怎么切,后者决定你最后怎么排。

先看一个常规写法,逻辑不复杂,但别写成教材味太重的那种。

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

publicclassWordFrequencyCounter{

privatestaticfinal Pattern WORD_PATTERN = Pattern.compile("[a-zA-Z]+");

publicstatic Map<String, Integer> countWords(String text){
        Map<String, Integer> freq = new HashMap<>();
if (text == null || text.isEmpty()) {
return freq;
        }

        Matcher matcher = WORD_PATTERN.matcher(text.toLowerCase());
while (matcher.find()) {
            String word = matcher.group();
            freq.put(word, freq.getOrDefault(word, 0) + 1);
        }
return freq;
    }

publicstaticvoidmain(String[] args){
        String text = "Java is great, java is practical. Java! Really great.";
        Map<String, Integer> result = countWords(text);

for (Map.Entry<String, Integer> entry : result.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

这里我没直接拿空格去切,而是用正则去抓单词。原因很简单,"Java,java" 这种东西你如果直接 split(" "),逗号会粘在词上,最后 java 和 java, 会被算成两个词,这种结果我第一眼就不太信。

再往前走一步,很多题目不会只满足于“统计出来”,还会让你输出“出现次数最高的前 N 个单词”。这时候别一边统计一边排序,没必要。先计数,后排序,思路更干净。

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

publicclassTopWordFrequency{

privatestaticfinal Pattern WORD_PATTERN = Pattern.compile("[a-zA-Z]+");

publicstatic List<Map.Entry<String, Integer>> topNWords(String text, int n) {
        Map<String, Integer> freq = new HashMap<>();
        Matcher matcher = WORD_PATTERN.matcher(text.toLowerCase());

while (matcher.find()) {
            String word = matcher.group();
            freq.merge(word, 1, Integer::sum);
        }

        List<Map.Entry<String, Integer>> list = new ArrayList<>(freq.entrySet());
        list.sort((a, b) -> {
int cmp = Integer.compare(b.getValue(), a.getValue());
if (cmp != 0) {
return cmp;
            }
return a.getKey().compareTo(b.getKey());
        });

return list.subList(0, Math.min(n, list.size()));
    }

publicstaticvoidmain(String[] args){
        String text = "Tom likes Java. Jack likes Java too. Tom also likes coding in Java.";
        List<Map.Entry<String, Integer>> topWords = topNWords(text, 3);

for (Map.Entry<String, Integer> entry : topWords) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

这里多做了一步:当词频相同时,按字典序排。这个不是什么高深优化,就是为了结果稳定。要不然同样的输入,换个 JDK 版本或者底层实现细节,输出顺序可能都不一样。面试里这种细节不一定扣你分,但在实际代码里,这种“偶发不一致”很烦。

再说复杂度。统计阶段,每个单词进一次 HashMap,时间复杂度基本是 O(n);排序阶段如果有 k 个不同单词,就是 O(k log k)。绝大部分场景够用了。要是文本非常大,只关心前 10 个高频词,那就可以再上小顶堆,把排序成本压下来。不过算法题叫“统计词频”,通常写到这里已经够交卷了,别一上来把题做成日志平台。

还有几个坑,顺手提一下。

一个是大小写。Java 和 java 到底是不是一个词,题目不写,你最好自己统一,不然后面没法解释。

另一个是数字要不要算词。比如 GPT4、2026 这种,和业务有关。上面的代码只统计纯字母单词,这是我故意收紧的。如果题目要求字母数字都算,那正则改成 [a-zA-Z0-9]+ 就行。

还有中文。这个题如果突然换成中文文章统计词频,那就不是这套写法了,得先分词,再计数。不要拿英文单词这套硬抠中文,最后看起来像写了,实际没解决问题。

所以这题的关键根本不在 HashMap,那只是工具。真正该想清楚的是:你准备怎么定义“词”,怎么规避脏数据,怎么让输出结果稳定。把这三件事想明白,代码自然就顺了。