ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言面试题:原地反转字符串中的单词

C语言面试题:原地反转字符串中的单词 在C语言面试和笔试里“反转字符串中的单词”是一道几乎绕不开的经典题。题目本身不复杂但能把边界情况、内存操作和原地修改讲清楚的人不多。尤其当你面对的是“只能使用 O(1) 额外空间”这种限制时很多自以为熟练的同学会当场翻车。今天我就把这道题从头到尾拆开聊聊几种常见解法的优劣、手写时的坑以及我在刷题和面试里积累的一些实操体会。这道题有很多变体比如 LeetCode 151、剑指 Offer 58、PTA 上的字符串逆序题核心都是同一个给定一个字符串把里面的单词顺序反转同时去掉多余空格。比如the sky is blue要变成blue is sky the hello world 要变成world hello。C语言里没有现成的split函数没有StringBuilder一切都要靠指针、字符数组手动操作所以它特别适合考察基本功。下面我按从“思路选型”到“踩坑实录”的顺序把这题讲的透透的。1. 项目概述与整体拆解1.1 这道题到底在考什么先别急着写代码我们得先搞清楚出题人的意图。表面上看它考的是“反转字符串”但真正核心的考点有三个字符数组的原地修改、边界条件的处理、以及对C语言字符串内存模型的理解。我见过不少同学在面试时第一反应是“我可以用strtok分割然后倒着放回去。”这句话一出来面试官基本就知道你对C语言字符串的底层机制理解不够深。strtok确实能分割但它会修改原字符串把分隔符替换成\0而且它内部有静态状态不可重入。更关键的是strtok遇到连续多个分隔符时会自动跳过这在某些场景里是好事但如果你要精确控制空格数反而会带来困扰。真正有区分度的做法是在不借助额外大数组的前提下原地完成反转。这意味着你要手动管理字符的搬移对指针和数组下标的把握要非常准。这道题放在C语言场景里本质上是在考你有没有形成“指针思维”和“边界意识”。所以我的建议是拿到题目先不要急着炫技先问清楚三个问题——能不能修改原字符串输出对空格的要求是什么额外空间复杂度能不能是 O(n)大多数面试场景下最优解是 O(1) 额外空间的原地算法。1.2 三种主流解法我推荐哪一种我梳理了一下常见的解法大概有三类各有优缺我直接做成了对比表解法空间复杂度时间复杂度代码量面试评价二维数组逐词存储倒序拼接O(n)O(n)少能跑通但不够亮眼用栈存储单词再出栈拼接O(n)O(n)中思路清晰但空间不够优原地三段反转整体反转局部反转O(1)O(n)中最优解加分明显二维数组法适合给初学者建立思路。思路很简单遍历原字符串把每个单词复制到一个char words[count][max_len]里然后从后往前拼回去。优点是逻辑直白、不容易写错缺点是需要额外开一个二维数组空间浪费大而且如果单词数量不固定还要动态处理。面试的时候能用但属于“及格线”答案。栈解法的想法很自然单词从左到右读但输出要从右到左这不就是先进后出吗于是用一个栈把所有单词压进去再边弹出边拼接。这比二维数组优雅一点但空间复杂度依然是 O(n)。在要求原地修改的题目里这个解法直接不满足条件。原地三段反转是我最推荐的也是我平时在代码里最常用的方案。核心思想就三步先清理多余空格再把整个字符串反转最后把每个单词单独反转。这三步做完单词顺序就反过来了而且单词内部字符顺序也恢复正常。整个过程只用了一个临时变量做交换额外空间是 O(1)。虽然代码量稍多一点但每一行都有存在的理由面试时讲起来也显得功底扎实。2. 核心细节解析与实操要点2.1 C语言字符串的内存本质想把这个题目写对先得把C语言字符串的内存模型刻在脑子里。C语言里没有真正的“字符串类型”字符串本质就是一个以\0结尾的char数组。你在函数里拿到一个char *s实际上拿到的是字符数组首元素的地址你并不知道数组有多长只能靠strlen(s)去数或者靠遍历到\0来判断终点。这道题里有一个很容易踩的坑sizeof(s)和strlen(s)的区别。如果s是函数参数sizeof(s)返回的是指针大小在 64 位机器上通常是 8而不是字符串长度。用sizeof去控制循环边界必挂。正确的做法是先用strlen(s)拿到长度或者每次遍历时都判断s[i] ! \0。另外C语言的字符串字面量是存储在只读数据段的如果你写出char *s hello world然后试图修改s[0]程序会直接崩溃。正确的姿势是声明成可修改的字符数组char s[] hello world。我见过不少人在本地用字符串字面量测试没问题一提交就 Segmentation fault多半就是这个原因。面试或刷题时一定要保证传入的是可写缓冲区。2.2 边界条件才是真正的考点字符串反转题目里最容易挂的地方不是主逻辑而是边界。我第一次写这题的时候就是被边界条件折磨到怀疑人生。下面这几种情况我建议你在写代码前先在草稿纸上列出来空字符串应该输出空串不能越界。纯空格字符串 清理后应该变成空串。前导空格 hello world输出不能带多余空格。尾随空格hello world 同理。多个连续空格hello world中间只能保留一个空格。只有一个单词hello反转后还是hello。单词内部含标点hello, world逗号应跟在单词上即world hello,。面试的时候我把这些用例写在白板边上然后对着代码一行行走一遍基本能避免 80% 的隐藏 bug。面试官看到你主动列边界用例印象分会明显提升因为这代表你经历过真实的项目测试而不是只会背题。这里还要提一个细节题目里的“单词”通常指的是由非空格字符组成的连续序列所以空格是唯一的分隔符。如果需要处理换行符、制表符等可以用isspace函数如果题目明确只说空格那直接用s[i] 判断就够了别画蛇添足。2.3 手写时最容易翻车的三个细节第一个细节反转函数的边界到底是left right还是left right。这属于代码洁癖问题但很能体现一个人的细致程度。用left right就好因为当left right时交换同一个元素没有意义纯属浪费一次操作。当然用也不会错只是不够优雅。我习惯统一用left right逻辑更干净。第二个细节清理空格后字符串长度变了要及时更新。很多人处理完多余空格之后还用原来的len做整体反转结果把\0也反转了或者把清理后残留的无效字符带进去输出直接乱掉。正确做法是清理完空格后重新计算长度或者直接用一个变量保存新的len这个新长度就是后续操作的唯一依据。第三个细节处理最后一个单词时循环结束条件必须包含\0。因为最后一个单词后面没有空格如果你只在遇到空格时反转单词最后一个单词就会漏掉。我常用的技巧是让循环遍历到i newLen当s[i]是空格或\0时都触发反转逻辑。这个细节我在初学时踩过好多次现在形成了肌肉记忆。3. 实操过程与核心环节实现3.1 先写一个通用的区间反转函数我写这类题的习惯是先把最底层的操作函数抽出来后面代码会清晰很多。这里需要一个能反转char数组中任意区间的函数。区间定义采用闭区间[left, right]也就是两个端点都要反转。void reverse(char *s, int left, int right) { while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } }这个函数非常通用后面三步全在用它。注意我已经把left和right定义为int虽然strlen返回size_t但在这道题里用int更顺手只要长度不超过INT_MAX就没问题。如果你追求严谨可以用size_t但要注意left--时不能下溢反而麻烦。交换字符时用临时变量即可不需要什么异或技巧。老老实实写三行编译器优化后会生成很高效的代码别为了炫技写s[left] ^ s[right]这种容易出错可读性也差。3.2 核心步骤一原地清理多余空格现在进入正题。第一步是最关键也最容易写错的把字符串里多余的空格清掉让每个单词之间只保留一个空格同时去掉首尾空格。我的思路是用双指针i负责遍历原字符串j负责记录新字符串的写入位置。大致逻辑是跳过连续的空格。如果当前已经写入了内容j 0就在写入下一个单词前补一个空格。复制一个完整的单词。循环结束后在j位置写\0完成清理。int i 0, j 0; int len strlen(s); while (i len) { while (i len s[i] ) { i; } if (i len) { break; } if (j 0) { s[j] ; } while (i len s[i] ! ) { s[j] s[i]; } } s[j] \0;我来逐步演算一下。假设输入是 hello world 初始i 0j 0。外层第一次循环跳过前导空格i走到2指向h。此时j 0不需要补空格。内层复制单词s[0]hs[1]es[2]ls[3]ls[4]o之后i指向5的空格内层结束。此时j 5。外层第二次循环跳过连续空格i走到8指向w。因为j 0所以先写入一个空格s[5] j变成6。然后复制单词worldj变成11。复制完i指向末尾的空格。外层第三次循环跳过空格i走到leni lenbreak。最后s[11] \0得到hello world。首尾空格没了中间多个空格也变成了单空格。这个过程是典型的原地压缩空间 O(1)时间 O(n)。3.3 核心步骤二整体反转再反转单词清理完空格后字符串变成了hello world下一步就是三段反转的核心操作。第一步把整个字符串反转。reverse(s, 0, newLen - 1)hello world变成dlrow olleh。这里newLen是清理后字符串的长度我用int newLen strlen(s)重新获取。第二步遍历字符串以空格为界把每个单词单独反转。dlrow反转成worldolleh反转成hello最终得到world hello。你可能会问为什么要先整体反转我举个例子你就明白了。假设原字符串是A B C整体反转后变成C B A。你看三个单词的相对顺序已经反过来了只是每个单词内部的字母也反了。这时候再把每个单词各自反转一次字母顺序就恢复了而单词顺序保持倒序。这个“两次反转抵消”的思路非常经典在旋转数组、链表反转里也经常用到。代码实现如下int newLen strlen(s); reverse(s, 0, newLen - 1); int start 0; for (int k 0; k newLen; k) { if (s[k] || s[k] \0) { reverse(s, start, k - 1); start k 1; } }这里k newLen是关键它保证s[k]能遍历到结尾的\0。当k newLen时s[k]就是\0触发最后一个单词的反转k - 1正好指向最后一个字符。如果写成k newLen最后一个单词就永远不会被反转。3.4 完整可用代码与测试结果把上面的代码拼起来加上必要的头文件和main函数就是一个完整的可运行程序。我习惯把逻辑封装成一个函数返回处理后的字符串指针这样在 LeetCode 和 PTA 上都能直接提交。#include stdio.h #include string.h void reverse(char *s, int left, int right) { while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } } char *reverseWords(char *s) { int len strlen(s); int i 0, j 0; // 第一步清理多余空格 while (i len) { while (i len s[i] ) { i; } if (i len) { break; } if (j 0) { s[j] ; } while (i len s[i] ! ) { s[j] s[i]; } } s[j] \0; // 第二步整体反转 int newLen strlen(s); reverse(s, 0, newLen - 1); // 第三步反转每个单词 int start 0; for (int k 0; k newLen; k) { if (s[k] || s[k] \0) { reverse(s, start, k - 1); start k 1; } } return s; } int main() { char s1[] the sky is blue; printf(\%s\\n, reverseWords(s1)); char s2[] hello world ; printf(\%s\\n, reverseWords(s2)); char s3[] a; printf(\%s\\n, reverseWords(s3)); char s4[] ; printf(\%s\\n, reverseWords(s4)); return 0; }我实际跑过这段代码输出如下blue is sky the world hello a 这四个用例覆盖了普通句子、前后空格加中间多个空格、单字符、纯空格结果都符合预期。整体时间复杂度 O(n)额外空间 O(1)达到了最优解的标准。4. 常见问题排查与经验技巧4.1 高频报错与问题速查表我在给同事review代码和自己在刷题平台提交时总结了一些高频问题整理成一张速查表遇到类似问题可以直接对照排查现象可能原因解决方案Segmentation fault用char *s ...定义字符串字面量并尝试修改改用char s[] ...输出首尾有多余空格清理空格时没处理好首尾检查清理逻辑中j 0的条件单词中间连续空格没去掉只做了反转没有先清理必须严格先清理空格再做反转最后一个单词丢失反转单词的循环没处理结尾\0循环条件改成k newLen字符串长度不对使用了sizeof(s)作为长度用strlen且注意清理后要重新取长度输出乱码清理后没有在j位置写\0清理循环结束后执行s[j] \0死循环内层while忘记移动i检查i是否写对位置如果你在网页平台上提交时遇到“wrong answer”别急着怀疑题库有问题先把自己的代码拿到本地跑一遍上面的测试用例。八成是边界没处理干净。4.2 面试和比赛里的追问与扩展这道题看起来简单但面试官很喜欢在写完代码后追加几个追问用来探测你的深度。我遇到过几个高频追问这里整理一下我的回答思路。追问一如果输入是只读的不能修改原字符串怎么办那就不能原地反转了只能从后往前遍历原字符串把每个单词复制到新字符串里。空间复杂度 O(n)时间 O(n)。实现思路是先找到最后一个单词的结束位置再往前找到起始位置复制到目标缓冲区然后加一个空格继续找倒数第二个单词。这样一次遍历就能完成。追问二如果空格之外还有制表符、换行符呢可以把判断条件从s[i] 改成isspace(s[i])包含空白字符的判断。不过要记得isspace需要包含ctype.h并且传入的值要能安全转换成unsigned char否则在极端字符集下可能有未定义行为。追问三如果字符串长度特别大内存有限怎么办原地三段反转本来就是为这种场景设计的因为额外空间 O(1)。但要注意int可能在超长字符串上溢出这时可以用size_t代替int。不过这样一来函数签名和参数类型都要调整代码里也要小心减法操作。实际面试中能说出“用size_t避免长度溢出”已经算加分了。追问四能不能只反转单词不改变单词内部的字符顺序这其实是一个不同的题目通常叫“反转字符串中的单词顺序”而不是“反转单词”。如果你只需要把单词顺序倒过来单词内部字母不动那整体反转局部反转的思路正好是反过来的先局部反转每个单词再整体反转。本质上是一个对称操作。4.3 一点个人心得最后分享一点我在实际刷题中的体会。这道题我前前后后写过不下十遍每次写都还有新收获。刚开始我总想着一步到位结果越写越乱。后来我养成了一个习惯任何字符串处理题先问“能不能原地”再问“分隔符怎么定义”然后写边界用例最后才动键盘。这套流程帮我省了很多调试时间。在刷题平台上比如 LeetCode 和 PTA这道题有不同的输入输出格式有的要求返回新字符串有的要求原地修改。我建议把原地版本和“从后往前复制新串”的版本都写一遍这两种思路几乎覆盖了所有变体。另外真的不要在本地用char *s hello world测试我用这个坑坑过自己好几次那种“代码明明没问题但一运行就崩”的感觉太折磨了。声明成字符数组或者动态分配内存才能安全地原地修改。如果你现在正在准备C语言相关的笔试或者面试这道题一定要练到闭着眼睛能写出来的程度。它不但是基础题里的常客更是检验你有没有掌握指针、数组、边界思维的一块试金石。写顺了这道题后面再遇到字符串相关的变体题你至少能有一个清晰的思考框架。
返回列表