14. 字符串反转与回文:从基础到进阶
字符串处理,说白了就是跟字符数组打交道。我做了这么多年嵌入式开发,字符串反转和回文判断这两个操作,几乎每个项目都会碰到。今天咱们就把这块彻底讲透。
14.1 字符串原地反转
什么叫「原地反转」?就是不额外开数组,直接在原字符串上动手。我刚开始学C语言时,总喜欢新开一个数组来存结果,后来被老工程师骂了一顿——内存宝贵啊,兄弟。
核心思路很简单:两个指针,一个从头,一个从尾,交换字符,往中间靠拢。
#include <stdio.h>
#include <string.h>
void reverse_string(char *str) {
if (str == NULL) return;
int left = 0;
int right = strlen(str) - 1;
char temp;
while (left < right) {
// 交换左右字符
temp = str[left];
str[left] = str[right];
str[right] = temp;
left++;
right--;
}
}
int main() {
char text[] = "Hello, World!";
printf("原字符串: %s\n", text);
reverse_string(text);
printf("反转后: %s\n", text);
return 0;
}
小提示:注意我用的 char text[] 而不是 char *text。用指针指向字符串常量的话,你没法修改它——我曾经在这上面栽过跟头,程序直接崩溃。
14.2 判断回文字符串
回文,就是正着读反着读都一样。比如 "level"、"上海自来水来自海上"。判断回文其实跟反转有异曲同工之妙。
#include <stdio.h>
#include <string.h>
#include <ctype.h> // 提供 tolower() 函数
int is_palindrome(const char *str) {
if (str == NULL) return 0;
int left = 0;
int right = strlen(str) - 1;
while (left < right) {
// 跳过非字母数字字符(处理带标点的句子)
while (left < right && !isalnum(str[left])) left++;
while (left < right && !isalnum(str[right])) right--;
// 忽略大小写比较
if (tolower(str[left]) != tolower(str[right])) {
return 0; // 不是回文
}
left++;
right--;
}
return 1; // 是回文
}
int main() {
char *tests[] = {"level", "Hello", "A man, a plan, a canal: Panama", "race a car"};
for (int i = 0; i < 4; i++) {
printf("\"%s\" -> %s\n", tests[i],
is_palindrome(tests[i]) ? "是回文" : "不是回文");
}
return 0;
}
关键点:实际项目中,回文判断往往要考虑大小写和标点符号。比如 "A man, a plan, a canal: Panama" 去掉标点、忽略大小写后,确实是回文。我当年面试时就因为没考虑这个被扣分了。
14.3 最长回文子串
这个就有点意思了。给你一个字符串,找出最长的那个回文子串。比如 "babad" 中,"bab" 和 "aba" 都是答案。
我常用的方法是「中心扩展法」。说白了,就是遍历每个字符,以它为中心向两边扩展,看看能扩多远。
#include <stdio.h>
#include <string.h>
// 从中心向两边扩展,返回回文子串的起始和结束位置
void expand_around_center(const char *str, int left, int right,
int *start, int *max_len) {
int len = strlen(str);
while (left >= 0 && right < len && str[left] == str[right]) {
left--;
right++;
}
// 当前回文长度
int cur_len = right - left - 1;
if (cur_len > *max_len) {
*max_len = cur_len;
*start = left + 1;
}
}
void longest_palindrome(const char *str) {
if (str == NULL || strlen(str) < 1) return;
int len = strlen(str);
int start = 0;
int max_len = 1;
for (int i = 0; i < len; i++) {
// 奇数长度回文:中心是一个字符
expand_around_center(str, i, i, &start, &max_len);
// 偶数长度回文:中心是两个字符之间
expand_around_center(str, i, i + 1, &start, &max_len);
}
// 输出结果
printf("最长回文子串: ");
for (int i = start; i < start + max_len; i++) {
putchar(str[i]);
}
printf(" (长度: %d)\n", max_len);
}
int main() {
char *tests[] = {"babad", "cbbd", "a", "ac"};
for (int i = 0; i < 4; i++) {
printf("输入: \"%s\"\n", tests[i]);
longest_palindrome(tests[i]);
printf("\n");
}
return 0;
}
注意:中心扩展法的时间复杂度是 O(n²),空间复杂度 O(1)。对于长字符串(比如几千个字符),这个算法够用。但如果字符串有几百万字符,你得考虑 Manacher 算法——那个是 O(n) 的,不过实现起来复杂不少。
14.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了。我建议你多看几遍,理解每个知识点之间的联系。
14.5 避坑指南
我这些年踩过的坑,给你列出来,省得你再走一遍弯路。
- 字符串常量不能修改:用
char *p = "hello"然后试图反转?程序直接崩。记得用字符数组char arr[] = "hello"。 - 空字符串和单字符:空字符串反转还是空,单字符反转还是它自己。边界条件一定要处理。
- 中文字符串:C语言里一个中文字符占多个字节,直接用上面的方法会乱码。处理中文时得用宽字符
wchar_t。 - 性能陷阱:最长回文子串如果用暴力法(三重循环),O(n³) 的时间复杂度,字符串稍微长点就跑不动了。中心扩展法 O(n²) 是性价比最高的选择。
我的习惯:写字符串处理函数时,我总会先加一个 if (str == NULL) return; 的判断。别嫌啰嗦,线上环境里空指针导致的崩溃,我见过太多次了。
好了,字符串反转和回文这块就讲到这里。代码不多,但每个函数背后都有值得琢磨的细节。你写代码时多想想边界情况,多测试几种输入,慢慢就能写出健壮的程序了。
公众号:蓝海资料掘金营,微信deep3321