
反转字符串面试必问,这5个坑让你代码跑不通
刚把网上抄的代码丢进 IDE,回车一按,直接报 IndexError。你盯着屏幕,心里直骂街:这代码明明看着对啊,怎么就炸了?
别慌,这不是你的问题,是这题本身“坑”多。反转字符串是面试必问的基础题,简单到让你放松警惕,复杂到能把你绕晕。很多人以为 str[::-1] 就是万能钥匙,结果换个语言、换个场景,代码全废。
今天咱们不整虚的,直接拆解那些让你“复制代码跑不通”的真实场景。从 Python 的切片陷阱,到 Java 的字符数组坑,再到 JS 的 Unicode 崩溃,一个个扒开来看。
坑的现象:为什么你的代码在本地能跑,面试官电脑上炸了
先说个惨案。我带过的应届生里,有一半人在面试时栽在同一个地方:他们写了 reverse_string(s: str) - str: return s[::-1],自信满满地说“这就是最优解”。面试官点点头,然后问:“如果输入是 '\ud83d\ude00'(一个笑脸 emoji),你的代码返回什么?”
候选人愣住了。因为他测试时只用了 'hello' 这种纯 ASCII 字符串。
这就是第一个坑:字符集边界。
在 Python 3 中,字符串是 Unicode 序列。s[::-1] 是按**码点(code point)**反转的。对于 BMP(基本多文种平面)内的字符,比如中文、英文,没问题。但对于增补平面(Supplementary Plane)的字符,比如 emoji,它们由两个 UTF-16 码元组成。Python 的 str 对象内部虽然按码点存储,但在某些底层操作或跨语言传输时,容易混淆“码点”和“码元”。
更常见的坑是:在 Java 或 C# 中直接反转 String 或 char[]。
错误现象:
输入 Hello \ud83d\ude00,期望输出 😀 olleH,实际输出 ☺ olleH 或者乱码。
根本原因:
Java 的 String 基于 UTF-16。一个 emoji 占两个 char。当你反转 char[] 时,你把两个码元拆开了,变成了两个非法的单独码元,导致显示为乱码或替换符。
复现代码(Java):
public static String reverseWrong(String s) {
char[] chars = s.toCharArray();
for (int i = 0, j = chars.length - 1; i j; i++, j--) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}
// 调用: reverseWrong(Hi \ud83d\ude00)
// 输出: ☺ iH (乱码)
正确写法(Java,使用 Code Point 反转):
public static String reverseCorrect(String s) {
int[] codePoints = s.codePoints().toArray();
for (int i = 0, j = codePoints.length - 1; i j; i++, j--) {
int temp = codePoints[i];
codePoints[i] = codePoints[j];
codePoints[j] = temp;
}
return new String(codePoints, 0, codePoints.length);
}
// 调用: reverseCorrect(Hi \ud83d\ude00)
// 输出: 😀 iH
注意这里用的是 codePoints(),这是 JDK 7+ 引入的方法。很多老代码还在用 toCharArray(),这就是坑。查阅 Java 官方开发者文档,String#codePoints() 的描述明确说明:该方法将字符串视为代码点序列,而不是 UTF-16 码元序列。这就是为什么它在处理 emoji 时是正确的。
根本原因:语言底层存储机制的差异
为什么不同语言坑不一样?因为字符串的底层存储方式不同。
Python 3:str 是 Unicode 码点序列。s[::-1] 反转的是码点。简单、高效,但要注意内存开销。
Java:String 是 UTF-16 码元序列。直接反转 char 会破坏增补平面的字符。
JavaScript:String 是 UTF-16 码元序列。split('').reverse().join('') 会踩中同样的坑。
Go:string 是字节序列。直接反转字节会破坏多字节 UTF-8 字符。
核心误区:
很多教程教你“反转字符串就是交换首尾字符”,这句话在单字节字符集(如 ASCII)中是对的,在定长字符集(如 UTF-16,且不考虑增补平面)中勉强能用,但在变长编码(如 UTF-8)或包含增补平面的 Unicode 中,直接反转“单位”(字节、码元)是错误的。
正确思路:
反转的“单位”必须是用户可见的字符(即 Unicode 码点,或者更精确地说是“字素簇” Grapheme Cluster,但面试中通常只需处理码点)。
正确写法对比:各语言的最佳实践
Python:简单,但要懂原理
Python 是最友好的,s[::-1] 就是标准答案。但为了面试加分,你得知道它的时间复杂度是 O(n),空间复杂度也是 O(n)(因为切片会创建新字符串)。
进阶写法:原地反转(如果允许修改列表)
如果题目要求“原地反转”(比如输入是 List[str]),你不能直接对 str 操作,因为 Python 字符串是不可变的。
错误写法:
def reverse_inplace_wrong(s: list) - list:
for i in range(len(s) // 2):
s[i], s[-i-1] = s[-i-1], s[i]
return s
# 如果 s 是 [a, b, c],没问题。
# 但如果 s 是 abc,报错:'str' object does not support item assignment
正确写法:
def reverse_inplace_correct(s: list) - list:
left, right = 0, len(s) - 1
while left right:
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
return s
JavaScript:别用 split('').reverse().join('')
这是最经典的 JS 坑。
错误写法:
function reverseWrong(s) {
return s.split('').reverse().join('');
}
// reverseWrong(Hi \ud83d\ude00) - ☺ iH
正确写法(使用 Array.from 或 Spread 运算符处理 Unicode):
function reverseCorrect(s) {
return [...s].reverse().join('');
}
// 或者
function reverseCorrect2(s) {
return Array.from(s).reverse().join('');
}
// 两者都能正确处理 emoji
原理:[...s] 或 Array.from(s) 会将字符串分解为码点数组,而不是码元数组。这是 ES6 之后处理 Unicode 字符串的标准方式。
Go:字节 vs rune
Go 的 string 是字节切片。直接反转字节是灾难。
错误写法:
func reverseWrong(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
// 对 Hi \ud83d\ude00 会输出乱码
正确写法(使用 rune):
func reverseCorrect(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
rune 是 Go 中 32 位无符号整数,用于表示 Unicode 码点。[]rune(s) 会将字节序列转换为码点序列,反转后再转回字节。
复现与修复:一个完整的调试案例
假设你在做 LeetCode 151 或类似题目,要求反转字符串中的单词。你写了个函数:
def reverse_words(s: str) - str:
return ' '.join(reversed(s.split()))
这个写法在大多数测试用例下都过。但有一个隐藏测试用例:s = the sky is blue (注意前后有空格)。
你的代码输出:blue is sky the。
期望输出:blue is sky the(有些题目要求去除首尾空格,有些要求保留)。
如果题目要求保留首尾空格,你的 split() 会自动忽略空字符串,导致空格丢失。
修复方案:
def reverse_words_preserve_spaces(s: str) - str:
# 使用 re 模块分割,保留分隔符
import re
words = re.split('(\s+)', s)
words.reverse()
return ''.join(words)
或者,更手动的方式:
def reverse_words_manual(s: str) - str:
# 1. 反转整个字符串
reversed_s = s[::-1]
# 2. 反转每个单词
result = []
current_word = []
for char in reversed_s:
if char == ' ':
if current_word:
result.append(''.join(current_word))
current_word = []
result.append(' ')
else:
current_word.append(char)
if current_word:
result.append(''.join(current_word))
return ''.join(result)
注意,第二种方法更高效,因为它只遍历一次字符串,而 re.split 可能会有正则引擎的开销。
规避建议:面试前必做的 3 件事
明确“反转”的单位:
在面试时,先问清楚:“反转是指反转字符,还是反转单词?是否包含 Unicode 特殊字符?” 这一步能体现你的严谨性,也是避坑的关键。
不要背代码,要懂底层:
知道为什么 split('').reverse().join('') 在 JS 中会炸,知道为什么 Go 中要转 rune。当你能解释“因为 JS 字符串是 UTF-16,emoji 占两个码元”时,面试官会对你刮目相看。
测试边界用例:
在本地测试时,永远加入这些用例:
空字符串
单字符 a
纯 ASCII hello
中文 你好
Emoji Hi \ud83d\ude00
混合 a\ud83d\ude00b
最后,给你一个记忆口诀:
Python:切片 [::-1] 最省心,但别对 str 原地改。
Java:char 是坑,codePoints 是正主。
JS:split('') 是雷,[...s] 才安全。
Go:byte 别乱转,rune 才是道。
反转字符串看似简单,实则是考察你对语言底层和 Unicode 理解的试金石。别再复制粘贴了,动手跑一遍,看看你的代码在 emoji 面前是否还站得住脚。
你更常用哪种写法?评论区交流,看看谁的代码最“健壮”。