字符串的应用——真题归纳与解读|算法篇
字符串在算法面试中,单独考察的机会并不多,同样倾向于和一些经典算法(后面会讲的)结合来体现区分度。步子不能跨太大,不然容易扯着x。本节我们照样是先解决只需要数据结构知识做基础就可以解决的字符串问题。
在讲题之前,我首先要给大家点拨两个字符串相关的“基本算法技能”。这两个技能偶尔也会单独命题,但整体来看在综合性题目中的考察频率较高,需要大家着重熟悉、反复练习和记忆,确保真正做题时万无一失。
# 基本算法技能
30 秒速记
- JavaScript 字符串不可变,所谓原地交换通常要先转字符数组,完成后再
join('') - 字符串题先确认比较单位是 UTF-16 code unit、Unicode code point 还是用户看到的字素簇,emoji 可能占多个 code unit
- 反转、回文常用左右指针把额外空间降为
O(1)(对字符数组而言),并把边界写成left < right
做字符串算法前,先要明确题目中的“字符”和“相等”按什么口径计算,再选择双指针、计数或滑动窗口。 只处理英文小写字母时按索引访问通常够用,但遇到 emoji、组合音标或多语言文本,就要区分 UTF-16 code unit、Unicode 码点和字素簇。反转或回文判断常用左右指针;由于 JavaScript 字符串不可变,需要修改时通常先转成字符数组,处理后再 join('')。空串、单字符、重复字符和超长输入也都应该覆盖。
字符串算法的第一步不是立刻写循环,而是定义“字符”和“相等”。只处理英文小写字母时,按索引读取通常足够;一旦输入包含 emoji、组合音标或多语言文本,就要区分 UTF-16 code unit、Unicode code point 与用户看到的字素簇。这个口径会直接决定长度、切分、反转和回文判断是否正确。
确定字符语义后,再根据访问模式选工具:两端向中间收缩用双指针,统计出现次数用 Map,连续区间约束用滑动窗口,复杂模式匹配则考虑状态机或动态规划。每个方案都要补空串、单字符、重复字符和超长输入,不能只验证 ASCII 样例。
面试官追问
追问 1用户资料页限制昵称最多 10 个“字符”,但 '😀'.length 得到 2;产品、前端和后端在评审时必须先统一什么口径?
必须先明确“字符”指 UTF-16 code unit、Unicode code point,还是用户看到的字素簇。JavaScript 的 length 按 code unit 计数,Array.from() 按 code point 拆分;面向视觉字符的限制通常还需 Intl.Segmenter。
追问 2搜索框只处理英文小写字母,候选人仍为每次输入建立复杂分词对象;你会如何权衡正确性与实现成本?
若输入契约确实限定为英文小写字母,按索引读取已经能满足字符语义,没有必要引入字素簇分段。关键是把限制写进校验和测试;一旦允许 emoji、组合音标或多语言文本,就必须重新选择切分口径。
追问 3文本审核功能从“判断是否回文”扩展为“找出最长的不重复连续片段”,原来的两端双指针还能直接复用吗?
两端收缩适合比较首尾关系,不能直接维护任意连续区间内的重复约束。应改用滑动窗口,并借助 Map 记录字符位置或计数;窗口中的字符单位仍须与产品定义保持一致,否则 Unicode 输入会产生偏差。
追问 4线上只有带组合重音的姓名出现长度校验和回文判断不一致,ASCII 与普通 emoji 测试都通过;你会怎样定位?
先构造由基础字符和组合标记组成的最小样例,分别打印 length、Array.from() 结果与字素簇分段结果。若两个功能采用了不同字符口径,应统一到同一分段层;规范化是否需要引入,则要由“相等”的业务定义决定。
追问 5日志分析要统计超长文本中每种符号的出现次数,团队在普通对象与 Map 之间选型;你会提醒哪些边界?
Map 能直接以分段后的字符为键,并避免普通对象原型键带来的额外处理,适合表达频次表。真正影响结果的仍是分段单位和输入规模;完整统计需要随不同字符数增长的空间,无法仅靠换容器消除。
# 反转字符串
30 秒速记
- JavaScript 字符串不可变,反转会创建新结果;最常见实现是拆分字符、原地交换、再拼接
split('')按 UTF-16 code unit 拆分,会破坏由代理对组成的 emoji;Array.from()或[...text]至少按 code point 拆分- code point 仍不等于用户看到的字素簇,组合音标、家庭 emoji 等要用
Intl.Segmenter - 双指针交换时间
O(n)、额外空间O(n),因为字符串不可变且需要字符数组 - 面试前先确认题目处理 ASCII、小写字母、Unicode code point,还是用户可见字符
反转字符串通常先转成字符数组,再用左右指针交换,最后通过 join('') 拼回字符串。 因为 JavaScript 字符串不可变,这个过程的时间和额外空间都是 O(n)。纯英文可以用 split(''),但它会拆坏 emoji 的代理对;处理 Unicode code point 时应改用 Array.from()。如果要求按用户看到的字符反转,组合音标和家庭 emoji 还要通过 Intl.Segmenter 按字素簇分段。
原文的 split('').reverse() 对纯英文成立,但会把 emoji 的代理对拆开。若题目只要求按 Unicode code point 反转,可以这样写:
function reverseText(text) {
const chars = Array.from(text)
let left = 0
let right = chars.length - 1
while (left < right) {
;[chars[left], chars[right]] = [chars[right], chars[left]]
left += 1
right -= 1
}
return chars.join('')
}
console.log(reverseText('A😀B')) // B😀A
Array.from() 能保住单个 emoji,却不能保证组合字素。例如 e 加组合重音可能仍被拆开;面向用户文本时用 new Intl.Segmenter(locale, { granularity: 'grapheme' }) 分段。算法时间 O(n),字符数组和结果占 O(n) 空间。
在 JS 中,反转字符串我们直接调相关 API 即可,相信不少同学都能手到擒来:
// 定义被反转的字符串
const str = 'juejin'
// 定义反转后的字符串
const res = str.split('').reverse().join('')
console.log(res) // nijeuj
面试官追问
追问 1昵称编辑页把 A😀B 用 split('').reverse().join('') 处理后出现乱码,而纯英文测试正常;你会怎样解释这段代码的失败点?
split('') 按 UTF-16 code unit 拆分,部分 emoji 由代理对组成,反转后代理对顺序被破坏。若需求是按 Unicode code point 反转,可先用 Array.from(text);这仍不保证组合字素完整。
追问 2前端需要实现按 code point 反转,代码评审要求避免不断执行 result = char + result;你会给出什么实现?
先用 Array.from(text) 得到字符数组,再以 left、right 双指针原地交换,最后执行一次 join('')。算法遍历为 O(n),数组与结果占 O(n) 空间;JavaScript 字符串不可变,不能承诺真正的零额外输出空间。
追问 3产品把“反转昵称”改成按用户看到的字符反转,输入可能包含 e 加组合重音;仅把 split('') 换成 Array.from() 足够吗?
不够,Array.from() 保护的是 Unicode code point,而组合重音可能与基础字符属于同一个字素簇。应使用 Intl.Segmenter(locale, { granularity: 'grapheme' }) 分段后再反转;具体 locale 需要与产品文本规则一致。
追问 4线上故障只发生在少量多语言昵称,排查时你发现存储值、页面显示和反转结果的长度各不相同;应该先收集什么证据?
先保留原始字符串,并分别查看 code unit、code point 和字素簇三种切分结果,避免只依据页面视觉判断。再核对校验、存储和反转是否使用同一字符口径;若上游还改变了文本表示,需要单独确认相等规则。
追问 5面试官要求“O(1) 额外空间反转字符串”,候选人直接在 JavaScript 字符串上交换索引;你会如何纠正这个约束冲突?
JavaScript 字符串不可变,索引位置不能被原地交换,生成反转字符串必然需要新的结果存储。若题目把输入改为可变字符数组,双指针交换可做到算法辅助空间 O(1);但字符数组和最终输出本身仍占空间。
(这段代码需要你非常熟悉,一些公司一面为了试水,有时会单独考这个操作)。
# 判断一个字符串是否是回文字符串
30 秒速记
- 回文判断使用左右指针比较对称位置,首次不等即可返回
false,无需真的构造完整反转串 - 时间
O(n)、辅助空间按可索引字符表示而定;ASCII 字符串可直接索引,Unicode 文本先明确字符口径 - “忽略大小写、空格和标点”不是回文定义自带条件,必须由题目契约决定并在比较前规范化
- 规范化可能改变字符数量与语义,例如
é有预组合和组合形式,可按需求使用normalize('NFC') - 空串和单字符通常视为回文,但应在函数契约或测试中明确
判断回文字符串,我一般用左右指针比较对称位置,遇到第一处不同就直接返回 false。 这样不需要构造反转字符串,时间复杂度是 O(n);ASCII 字符串可以直接按下标访问,Unicode 文本则要先明确比较口径。忽略大小写、空格和标点并不是回文判断的默认规则,只有题目明确要求时才做预处理。像 é 还可能存在不同编码形式,可按需求先调用 normalize('NFC'),空串和单字符通常视为回文。
