news 2026/10/6 12:09:43

字母大小写全排列:递归走完一条路,为什么要删掉最后一个字符?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
字母大小写全排列:递归走完一条路,为什么要删掉最后一个字符?

力扣784:字母大小写全排列给一个只含英文字母和数字的字符串。每个字母可以保留或切换大小写,数字不变,返回所有结果,顺序不限。长度为1至12。

我原来的代码用StringBuffer path记录走过的字符,用递归处理下一位。实现没有必要换掉,真正要补的是:同一个path被所有分支共享,为什么递归返回后,要删除最后一个字符?

1. 这不是调换字符的位置

输入a1b2,数字始终在原位置,a和b也不交换位置,只改变大小写:

a1b2 a1B2 A1b2 A1B2

每个字母恰好有两种选择:保留原字符,或者切换大小写;数字只有一种。

原文把“不处理、大写转小写、小写转大写”列成三个情况,这是代码判断的分类,不是每个字母都会产生三个分支。一个字符不可能同时既是大写又是小写。

如果有k个字母,结果数就是2^k。例如aa有aa、aA、Aa、AA四个结果,不是两个;两处a是两个独立位置。

2. 先说明dfs的任务,再相信它

约定dfs(s, pos)进入时:

  • path已经选好了输入前pos个位置的字符,因此长度为pos。
  • dfs负责枚举剩余位置的全部选择,并记录完整结果。
  • 返回时,path恢复到进入这一层之前的样子。

例如进入dfs("a1b2", 2),path可能是a1,也可能是A1。这一层负责处理b以及后面的2。

“相信递归”不是跳过逻辑,而是用这个约定缩小问题:只处理当前位,把后面的位交给下一层,同时保证自己没有破坏调用者的path。

3. 读原图:向下走与向上返回,是两件事

图的左半边先选a,右半边改为A;1和2只向下延伸,b的位置分成b与B。红色回程箭头表示恢复path,再进入另一条路。

这是一张执行过程图,不只是四个答案拼在一起。底部的ret/path标注是过程中间状态,完整结果应是第一节的四个字符串。

把图里path="a1"的那一层单独展开:

进入处理b的这一层:path = "a1" 追加b: path = "a1b" 调用下一层处理2: 保存"a1b2" 下一层返回: path = "a1b" 本层删除自己加的b: path = "a1" 追加B: path = "a1B" 调用下一层处理2: 保存"a1B2" 下一层返回: path = "a1B" 本层删除自己加的B: path = "a1" 本层结束: path与进入时相同

处理2的下一层也要恢复:它追加2,保存结果,删除2,再返回。不是一层把整段后缀全部删除,而是每层负责撤销自己追加的那个字符。

4. 为什么保存的答案不会跟着path被删?

走到pos == s.length(),说明每个位置都已经选择完毕。此时执行:

ret.add(path.toString());

这里存的是当前内容对应的String,而不是可变的path对象。之后删除path的末尾字符,不会把已经存入列表的字符串截短。

这个区别很重要:如果另一种回溯题把同一个可变列表直接放入答案,再不断修改它,就可能让所有结果指向同一份变化中的内容。这里的toString()把当前字符内容保存为字符串结果。

终止条件也不是“发现一个字母”或“发现一个数字”,而是所有位置处理完成。否则得到的只是前缀,不是答案。

5. Java代码:保留原来的追加与删除方式

补齐imports后,这份代码可以脱离题库模板编译。字段每次调用入口都重新初始化,所以同一个Solution对象可以连续处理多个输入。

import java.util.ArrayList; import java.util.List; class Solution { StringBuffer path; List<String> ret; public List<String> letterCasePermutation(String s) { path = new StringBuffer(); ret = new ArrayList<>(); dfs(s, 0); return ret; } public void dfs(String s, int pos) { if (pos == s.length()) { ret.add(path.toString()); return; } char ch = s.charAt(pos); path.append(ch); dfs(s, pos + 1); path.deleteCharAt(path.length() - 1); if (ch < '0' || ch > '9') { path.append(change(ch)); dfs(s, pos + 1); path.deleteCharAt(path.length() - 1); } } public char change(char ch) { if (ch >= 'a' && ch <= 'z') return (char) (ch - 32); return (char) (ch + 32); } }

这里保留原实现的两个前提:

  1. “不是数字”就当作字母,只因为输入保证由英文字母和数字组成;不能拿去处理标点或任意文本。
  2. 英文大小写相差32,是这份代码处理ASCII英文字母的办法,不是通用Unicode大小写转换规则。

StringBuffer沿用原笔记;它的方法同步不意味着这个带共享字段的Solution可以并发调用。若设计通用库,应另外考虑输入校验与可变状态的隔离,但不必为了这道题引入一套复杂框架。

6. 只漏一行恢复,会怎样?

以输入a1为例。先走保留a的分支,得到a1。处理数字的下一层返回时已删掉1,所以path回到a。

如果处理a的这一层没有删掉自己追加的a,就直接追加A:

应当:"a" -> "" -> "A" -> "A1" 错误:"a" -------> "aA" -> "aA1"

结果不仅内容错,连长度也变了。这是可变path泄漏到兄弟分支,不是递归次数不够。

数字也要追加后递归、返回后删除。没有第二个分支,不代表它可以把自己留下;它仍要遵守“返回时恢复进入状态”的约定。

7. 复杂度要把输出本身算进去

设输入长度为n,字母数为k。

结果有2^k个,每个结果长度为n,保存完整字符串就需要O(n × 2^k)的时间和结果空间;不能只说“递归深度n,所以O(n)”。

不含结果列表时,递归栈和path使用O(n)空间。字母全部不存在时,只有一个结果;12个位置全是字母时,输出4096个结果。

这个输出规模不是实现不够聪明造成的。题目要求全部结果,就不能把4096个长度为12的字符串省略成一个“有4096种”的统计数字。

8. 怎么验证完整性,而不是只打印四个样例?

本地另写一份位掩码枚举:先记录字母位置,对于0至2^k-1的每个掩码,逐位选择小写或大写,其余数字保持不动。

它不递归、不共享path,也没有追加后删除的流程。大小写通过两份英文字母表对应,不复用原代码的加减32逻辑。

对原Java和整理版,核对以下内容:

检查项能发现什么
结果集合与位掩码枚举相同漏分支、错误字符或多余结果
列表长度等于2^k,且去重后长度不变重复答案;只比较Set会漏掉这个问题
每个结果长度为n,数字位置不变漏恢复、误改数字
入口返回后path为空完整调用结束时现场没清理
同一个对象连续调用,旧返回列表仍保持原结果结果列表未重建或跨调用污染

输入覆盖a1b2、3z4、全数字、原本大写、重复字母和12位全字母;再穷举字母表{a,A,0,z,Z,9}上长度1至5的全部字符串,并加入固定种子的随机输入。

最后故意删掉第一个分支后的恢复语句,确认测试抓到a1变出aA1的问题。另一项故意重复答案,确认它不能通过“集合相同”的表面检查。

原图的耗时排名不用于评价这次修订的性能。正确性来自每一位选择的覆盖与现场恢复,测试则检查它们有没有写漏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 12:05:41

SO-DIMM物理兼容性指南:DDR3与DDR4不可互插的四大铁律

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:05:22

DeepSeek大模型智慧办公落地指南:从部署到避坑的完整工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:04:44

Allegro模块复用实战:Place Replicate与Group五分钟高效布局布线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:04:10

TPU 3D打印总失败?Simplify3D切片参数调优全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:04:09

FB_LLC谐振变换器死区时间与ZVS量化设计指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/6 12:03:31

香薰机离线语音控制芯片选型:WTK6900与WT2606A深度对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华