在 PHP 相关岗位的技术面试中,字符频率统计是一类出现频率很高的算法题。题目要求通常很简单:给定一个字符串,请写出程序统计其中每个字符出现的次数。虽然逻辑直观,但面试官往往通过这道题考察候选人对语言特性的掌握、对边界情况的处理以及对时间和空间复杂度的理解。

一、最基础的数组遍历统计法
PHP 的关联数组天生适合做频率统计,因为它本质上就是一张哈希表。我们只需要遍历字符串中的每一个字符,以其作为键,在数组中对应的值加一即可。这种方法代码量少,而且时间复杂度是线性的。
下面的示例展示了最基础的写法。这里使用 str_split 将字符串转为字符数组,然后循环处理。需要注意的是,如果字符串中包含多字节字符(如中文),str_split 会按字节拆分,可能导致统计错误,这一点我们在后面专门说明。
<?php
function countChars($str) {
$freq = array();
$chars = str_split($str);
foreach ($chars as $ch) {
if (!isset($freq[$ch])) {
$freq[$ch] = 0;
}
$freq[$ch]++;
}
return $freq;
}
$text = "abracadabra";
$result = countChars($text);
print_r($result);
// 输出: Array ( [a] => 5 [b] => 2 [r] => 2 [c] => 1 [d] => 1 )
?>
这段代码的时间复杂度为 O(n),其中 n 是字符串长度;空间复杂度为 O(k),k 是不同字符的数量。在面试中,主动说出复杂度会让面试官觉得你具备基本的算法素养。
另外,PHP 中可以用 isset 判断键是否存在,也可以用 array_key_exists,但在频繁读写场景下 isset 性能更好,因为它不关心值是否为 null。这个小细节也是面试官喜欢追问的点。
二、处理多字节字符与中文场景
上面代码在纯英文或单字节编码下没有问题,但一旦字符串包含 UTF-8 中文,str_split 就会把每个汉字拆成三个字节,统计出来的结果毫无意义。面试中如果题目没说字符串只包含 ASCII,你就应该主动提出多字节问题。
解决方式是使用 mb_str_split 函数,它可以按字符而非字节拆分字符串。下面的例子演示了如何安全地统计含中文字符的频率。
<?php
function countMbChars($str, $encoding = 'UTF-8') {
$freq = array();
$chars = mb_str_split($str, 1, $encoding);
foreach ($chars as $ch) {
$key = $ch;
if (!isset($freq[$key])) {
$freq[$key] = 0;
}
$freq[$key]++;
}
return $freq;
}
$text = "你好世界你";
$result = countMbChars($text);
print_r($result);
// 输出: Array ( [你] => 2 [好] => 1 [世] => 1 [界] => 1 )
?>
使用 mb_str_split 要求 PHP 安装了 mbstring 扩展,这在现代 PHP 环境中基本是默认开启的。如果运行环境不支持,也可以用 mb_substr 配合循环逐个截取。
在面试里,能写出兼容多字节的版本,通常说明候选人有实际处理过国际化文本的经验,而不只是背题。此时可以进一步说明:若只统计字母,可配合 preg_match 或 ctype_alpha 做过滤。
三、按频率排序与常见追问
面试官常在基础统计之后追加要求,比如“按出现次数从多到少输出”或者“只统计英文字母并忽略大小写”。这时候不能重写一遍逻辑,而应该用 PHP 内置数组函数组合解决。
对于排序,可以使用 arsort 对关联数组按值降序排列;对于过滤,可以用 array_filter 结合条件回调。下面示例展示忽略大小写、只留字母并排序的完整做法。
<?php
function countLettersSorted($str) {
$str = strtolower($str);
$freq = array();
$chars = str_split($str);
foreach ($chars as $ch) {
if (ctype_alpha($ch)) {
if (!isset($freq[$ch])) {
$freq[$ch] = 0;
}
$freq[$ch]++;
}
}
arsort($freq);
return $freq;
}
$text = "Hello World";
$result = countLettersSorted($text);
print_r($result);
// 输出类似: Array ( [l] => 3 [o] => 2 [h] => 1 [e] => 1 [w] => 1 [r] => 1 [d] => 1 )
?>
这种写法把统计、过滤、排序三步清晰分开,便于维护和讲解。在白板或线上编码时,分步骤写比塞在一个循环里更容易让面试官跟上思路。
如果面试官继续问“前 k 个高频字符怎么做”,你可以回答在排序后取前 k 项,或者提到用堆结构优化到 O(n log k),展示算法知识的广度。
四、大文本与内存优化思路
当字符串不是几十个字符,而是几百 MB 的日志文件时,一次性 str_split 或 file_get_contents 读入内存会直接撑爆 PHP 内存限制。面试中高级岗位常考这一点。
正确思路是用流式读取,比如按行读取文件,边读边更新频率表。下面示例用 fopen 和 fgets 演示分块统计,避免内存峰值过高。
<?php
function countFromFile($path) {
$freq = array();
$handle = fopen($path, 'r');
if (!$handle) {
return $freq;
}
while (($line = fgets($handle)) !== false) {
$chars = str_split($line);
foreach ($chars as $ch) {
if (!isset($freq[$ch])) {
$freq[$ch] = 0;
}
$freq[$ch]++;
}
}
fclose($handle);
return $freq;
}
// 假设有个大文件 input.txt 在 127.0.0.1 可访问的本地路径
$result = countFromFile('/tmp/input.txt');
print_r($result);
?>
这种方式内存占用只和单行长度以及字符种类有关,与文件总大小解耦。若字符集极大,还可考虑用数据库或外部存储做聚合,但面试现场通常说到流式读取就够了。
总结来说,PHP 统计字符频率表面简单,实则覆盖了数组操作、多字节处理、排序过滤和内存控制多个层面。面试时先写基础版,再主动补充边界与优化,通过率会明显提高。