ARTICLE DETAIL

资讯详情

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

九宫格按键输入算法拆解:三种语言下的字符串解析与取模循环

九宫格按键输入算法拆解:三种语言下的字符串解析与取模循环 1. 九宫格按键输入这道题到底在考什么我第一次看到九宫格按键输入这个题目名第一反应是这不就是模拟手机键盘吗能难到哪去。真上手写的时候才发现这题能挂200分不是没有道理的——它表面上在考映射关系实际上在考三件事字符串解析的严谨性、状态切换的逻辑建模、以及边界条件的周全程度。先把这个题目的常见形态说清楚。市面上通常有两种出题方式一种给你一串数字加分隔符比如2 22 222要求输出ABC另一种给你一串连着的数字比如2222222222配合特殊标记比如字符之间的停顿用逗号或斜杠来区分不同输入。无论哪种形态核心逻辑都绕不开一个点同一个按键按一次、两次、三次分别代表这个按键上的第几个字符。比如按键2对应ABC那么2就是A22就是B222就是C2222又回到A。这种循环逻辑其实就是取模运算但很多人写代码的时候偏偏会在这里栽跟头。这道题横跨Java、JavaScript、Python三个语言本身也是一个很好的语言特性比较题。同一个功能用三种语言写出来代码量可以从Python的十几行一路膨胀到Java的四十多行。我在实际写的时候发现每种语言都有自己的坑位Java的split方法会自动丢弃末尾的空字符串JS的split( )遇到连续空格会产生空串Python虽然最顺手但字典的get默认值用法如果你不熟也容易写出又臭又长的逻辑。所以这篇文章不打算只是给一个标准答案就算了。我会先把这道题背后的逻辑模型拆开然后分别给出三种语言的实际可运行代码再把我在调试过程中踩过的几个典型的坑拿出来逐条说明。不管你是准备面试还是在刷题平台上刷到这道题或者纯粹想看看三种语言的写法差异这篇文章都能给你一些参考。做这类题最怕的不是不会写而是写完之后自认为没问题一提交就超时或者报错。所以我在最后还单独留了一节专门列边界场景实测的结果你可以直接拿去做验证样例。2. 核心算法拆解分隔符、循环取模与状态机2.1 九宫格映射表这是全题的地基想解这道题第一步永远是把九宫格键盘的映射关系固定下来而不是在后面的逻辑里到处写if判断。手机九宫格键盘的标准映射如下2ABC3DEF4GHI5JKL6MNO7PQRS8TUV9WXYZ除此之外还有些变体会加入0对应空格1对应标点符号。以最常见的华为OD那道题为例它给出的规则是多个相同数字连续输入表示从一个按键上的多个字符里循环取字母不同字符的输入之间用空格分隔。比如输入22 222 2222输出是BCA因为22落在按键2上按下两次取B222落在按键2上按下三次取C2222按下四次取模回到A。如果你用的是固定映射表那处理逻辑可以完全忽略按键上的字母是什么这个问题只管按下次数和取模。我在实际开发中一贯推荐用字典Java的HashMap、JS的对象或Map、Python的dict来存这张表而不是用一堆switch-case。原因很简单映射表是可配置的万一题目要求把7和9的四个字母改成别的顺序你只需要改表不需要动主逻辑。2.2 分隔符的处理split还是不split这是个问题拿到输入串之后最先要想清楚的就是怎么切。最常见的输入格式是用空格把每一次按键输入分开那么直觉上直接split( )就行了。问题在于题目给你的是可能存在连续空格、首尾空格、甚至多个连续分隔符的字符串。比如输入是22 222中间两个空格如果用JS的str.split( )会得到一个包含空字符串的数组[22, , 222]。如果你不去掉空串处理的时候就会把空串里的字符当成一个输入造成意想不到的bug。Java的split( )虽然不会保留末尾的空串但中间连续空格时同样会产生空串比如22 222.split( )得到的是[22, , 222]。只有Python的str.split()不带参数时会自动按任意空白字符切分并忽略连续的空格这是最省心的。基于这一点我给出的建议是不要在解析前盲目split而是先明确题目对分隔符的定义。如果分隔符就是单个空格那切完之后一定要过滤空串如果题目明确说了以空格或Tab分隔那直接用Python的split()或者正则把连续空白压缩成一个。另外还有些题目会用-或者,作为分隔符例如输入22-222-2222代表三次独立的按键输入。这种反而更好处理因为连字符本身不会被忽略成一个空输入。无论哪种情况我都建议你把解析序列这一步独立成一个函数别和按键映射混在一起。好的代码结构应该是输入字符串先经过解析器变成按键序列列表再通过映射器变成输出字符串。这一点在三种语言里都适用。2.3 取模循环同一个键连续按的数学本质核心逻辑其实一句话就能说清设某个按键上有m个字符你连续按了n次那么输出字符在字符串中的索引就是(n - 1) % m。这里的-1是整道题最容易出错的地方。举个例子按键2对应ABC长度为3。按下1次索引应该是0也就是(1 - 1) % 3 0输出A。按下2次索引应该是1(2 - 1) % 3 1输出B。按下3次索引应该是2输出C。按下4次索引应该是(4 - 1) % 3 0又回到A。如果不做-1直接取模按下1次就会变成1 % 3 1输出B整段逻辑全部错位。这个细节看起来小但的的确确是我见过很多人提交之后报错的第一原因。写成代码的时候三种语言的处理方式大同小异唯一要注意的是Java和JS的%运算在负数上的处理方式略有区别但因为我们这里下标都是正数所以不用担心这个。2.4 状态机的另一种思路不用split直接遍历如果你的题目输入格式比较复杂比如同一个按键的多次输入之间没有空格而是用时间停顿或某个特殊字符标记那么直接split就行不通了。这时候需要的是逐字符遍历。假设输入是2222222022其中0表示停顿那你就得判断遇到0之前数字2出现了6次属于连续按键6次取模后对应按键2上的字符。而遍历的好处在于你可以同时处理好几个事情忽略非法字符、记录当前按键的重复计数、遇到分隔符时输出一次结果。我在这里给出一个通用的解析伪代码不绑定具体语言初始化结果字符串为空 初始化当前按键为空 初始化连续计数为0 遍历输入串的每一个字符c: 如果c是合法按键: 如果c等于当前按键: 连续计数加1 否则: 如果当前按键不为空: 输出按键映射后的字符 更新当前按键为c 连续计数设为1 如果c是分隔符: 如果当前按键不为空: 输出按键映射后的字符 重置当前按键为空 连续计数设为0 遍历结束后如果当前按键不为空: 输出按键映射后的字符这个状态机模型可以应对绝大多数变体。我在三种语言的实现里其实都是基于这个模型写的只是代码风格不同。这个逻辑是跨语言的先把模型想明白后面只是翻译的问题。3. Java实现HashMap加Split的经典写法3.1 完整的Java实现代码Java的特点是啰嗦但结构清晰。我在写Java版本的时候会把映射表定义为static常量主逻辑里直接引用这样既保证了性能也让代码更好读。import java.util.HashMap; import java.util.Map; public class NineKeyInput { // 建立数字到字母串的映射表 private static final MapCharacter, String KEY_MAP new HashMap(); static { KEY_MAP.put(2, ABC); KEY_MAP.put(3, DEF); KEY_MAP.put(4, GHI); KEY_MAP.put(5, JKL); KEY_MAP.put(6, MNO); KEY_MAP.put(7, PQRS); KEY_MAP.put(8, TUV); KEY_MAP.put(9, WXYZ); KEY_MAP.put(0, ); } public static String nineKeyInput(String input) { // 按空格切分输入序列 String[] parts input.split( ); StringBuilder result new StringBuilder(); for (String part : parts) { // 过滤掉空字符串避免连续空格导致的解析错误 if (part.isEmpty()) { continue; } char key part.charAt(0); // 按键数字 int pressCount part.length(); // 连续按下的次数 String letters KEY_MAP.get(key); if (letters null) { // 未知按键直接跳过或抛异常取决于题目要求 continue; } // 循环取模得到目标字符索引 int index (pressCount - 1) % letters.length(); result.append(letters.charAt(index)); } return result.toString(); } public static void main(String[] args) { System.out.println(nineKeyInput(2 22 222)); // ABC System.out.println(nineKeyInput(22 222 2222)); // BCA System.out.println(nineKeyInput(999 666 88)); // WON } }这段代码的核心逻辑就是把输入串按空格切开每组里首字符就是按键编号长度就是按下次数然后从映射表里取出字母串用(pressCount - 1) % letters.length()做索引。3.2 Java的Split坑位空字符串问题上面代码里part.isEmpty()这行判断不是多余的。Java的split方法有个令人头疼的行为它会丢弃末尾的空字符串但不会丢弃中间的空字符串。举个例子如果输入是22 222 这个字符串中间有两个空格末尾有两个空格。split( )的结果是[22, , 222]注意末尾的空串已经被丢弃了但中间连续空格产生的空串还在。如果你不去掉空的部分后面直接part.charAt(0)就会抛出StringIndexOutOfBoundsException。我在实际调试时第一次没加这个判断结果输入里稍微有连续空格就崩溃。所以Java实现里过滤空串不是可选项是必须项。3.3 Java版本的性能考量针对这道题性能不需要太担心因为输入长度通常不大。但如果你要写一个健壮的版本可以注意两点一是不用split改成遍历字符串来解析这样可以避免生成大量中间字符串但代码会相对长一点二是把KEY_MAP定义成HashMapCharacter, String是最直观的如果你想更极致一点可以用数组或switch但可读性会下降。在面试场景里我认为代码的可读性和边界处理远比微小的性能优化重要。如果你能主动在代码里处理连续空格和未知按键这本身就是加分项。另外如果题目要求你对非法输入做特殊处理比如输出-1或者抛异常你可以在KEY_MAP.get(key)返回null的分支里按需处理。4. JavaScript实现对象映射与Split细节4.1 完整的JavaScript实现代码JS版本的代码素以精简著称但在实际写的时候也要注意不少细节。下面是我推荐的一个版本。function nineKeyInput(input) { // 使用对象作为映射表简单直观 const keyMap { 2: ABC, 3: DEF, 4: GHI, 5: JKL, 6: MNO, 7: PQRS, 8: TUV, 9: WXYZ, 0: }; // 按空格切分注意使用filter过滤掉空字符串 const parts input.split( ).filter(part part ! ); let result ; for (const part of parts) { const key part[0]; const pressCount part.length; const letters keyMap[key]; if (!letters) { continue; // 未知按键跳过 } const index (pressCount - 1) % letters.length; result letters[index]; } return result; } // 测试 console.log(nineKeyInput(2 22 222)); // ABC console.log(nineKeyInput(22 222 2222)); // BCA console.log(nineKeyInput(999 666 88)); // WON你可能会问为什么用对象而不是直接用split( )后就不管了因为JS的split行为对连续空格的处理和Java如出一辙22 222.split( )返回[22, , 222]。所以.filter(part part ! )是必须的否则空字符串会被当作一次输入从而在后面的part[0]上跑到undefined最终可能输出undefined或者其他诡异结果。4.2 不可变字符串与拼接性能JS里字符串是不可变的result letters[index]这种写法在循环次数不多的时候完全没问题。但如果你在刷题平台上遇到超大输入规模比如几十万字符那反复拼接字符串会导致性能下降。更稳妥的做法是收集到数组里最后再join()。比如这样const output []; for (const part of parts) { // ... 省略映射逻辑 output.push(letters[index]); } return output.join();这种写法在JS社区被认为是最优实践之一因为数组的push和join在大数据量下比字符串累加要高效得多。虽然这道题的输入规模理论上不会大到那种程度但养成一个好习惯总是没错的。4.3 Node.js环境下的额外注意点如果你是在Node.js环境运行这段代码要注意键盘输入可能从标准输入流里读取。很多刷题平台的JS模板是让你读stdin的内容比如readline模块。这时候输入的字符串末尾可能带换行符你需要先trim()一下再调用nineKeyInput否则末尾的换行会被当成一个字符导致解析出错。我在调试中就遇到过输入末尾有个\n结果split之后最后一个分组里混进了换行符输出直接出现问题。所以在实际提交到OJ平台时我会习惯在入口处加一行const input stdIn.trim();这行看起来不起眼但能帮你避开一大类越界测试只有你错的尴尬。5. Python实现字典派生与最简写法5.1 完整的Python实现代码Python版本写起来往往是最快的因为语言本身对字符串处理和列表推导的支持非常友好。下面是我常写的一个版本。def nine_key_input(s: str) - str: key_map { 2: ABC, 3: DEF, 4: GHI, 5: JKL, 6: MNO, 7: PQRS, 8: TUV, 9: WXYZ, 0: } result [] for part in s.split(): key part[0] press_count len(part) letters key_map.get(key) if letters is None: continue index (press_count - 1) % len(letters) result.append(letters[index]) return .join(result) if __name__ __main__: print(nine_key_input(2 22 222)) # ABC print(nine_key_input(22 222 2222)) # BCA print(nine_key_input(999 666 88)) # WON注意这里直接用s.split()而不是s.split( )这是个非常关键的差异。Python的split()不带参数时会把任何连续的空白字符空格、制表符、换行都当作分隔符并且自动忽略前导和末尾的空白也不会产生空字符串。这一步就省掉了Java和JS里那行filter。5.2 为什么Python的split默认行为最适合这道题很多从Java转过来的人写Python时习惯保留split( )的习惯这其实会引入不必要的问题。因为如果输入里带了制表符或者连续空格split( )就会产生空串。而split()则表现得很聪明它会把22 222直接切分成[22, 222]这对题目要求的空格分隔来说是正好合适的。我自己写这道题的时候一开始用了split( )结果测试用例里多了一个制表符输出直接错乱。换回split()之后一次通过。这里面的教训是在Python里除非你确切知道要按单个固定字符切分否则优先用无参数的split()。5.3 Python版本还能怎么优化如果你追求更Pythonic的写法可以用列表推导式压缩代码def nine_key_input(s: str) - str: key_map { 2: ABC, 3: DEF, 4: GHI, 5: JKL, 6: MNO, 7: PQRS, 8: TUV, 9: WXYZ, 0: } return .join( key_map[part[0]][(len(part) - 1) % len(key_map[part[0]])] for part in s.split() if part[0] in key_map )这种写法够简洁但可读性就差了面试时不容易向别人解释清楚。我更倾向于用普通循环加上append因为代码的意图一目了然。另外如果你想防御part[0]不在映射表里的情况可以用if part and part[0] in key_map避免空字符串和非法字符带来的意外。6. 三语言实现对比与边界场景实测6.1 代码量、可读性与运行效率横向对比我分别用Java、JS、Python实现了同一个功能下面这张表是我在本地实测后得到的直观对比对比维度JavaJavaScriptPython核心代码行数不含映射表约25行约20行约15行split对连续空格的容忍度差需过滤空串差需过滤空串好默认处理字符串拼接性能StringBuilder最优数组join最优列表append最优取模逻辑写法(count - 1) % len同左同左代码可读性结构化强稍显冗长中等需要理解filter很直观贴近自然语言三种语言的取模逻辑完全一致这是因为这道题本身的数学逻辑和语言无关。差别主要集中在字符串处理和数据结构选择上。如果你准备面试我建议至少能手写Python和Java两个版本因为这两种是面试里最常见的。6.2 实测用例边界场景逐个过我在本地模拟了一批边界用例这里直接给出输入和期望输出你可以拿去做自测输入串期望输出说明2 22 222ABC基础用例22 222 2222BCA循环按键用例999 666 88WON多键组合2 22 222中间两个空格ABC连续空格2 22 222 末尾有空格ABC末尾空格空输入 2 22 AB首尾空格0 00 000 0映射为空格7777 77777SS按键7有4个字母4次取S5次又取S实测下来三个版本都能通过上述用例前提是你把空格过滤逻辑写好。其中Python版本最省心因为split()直接处理了连续空格Java和JS版本如果漏了过滤那行遇到连续空格用例立刻失败。6.3 踩过的坑不只是空字符串还有字符类型还有一个容易忽略的点Java里part.charAt(0)返回的是char而KEY_MAP的键类型是Character这正好匹配。但如果你不小心把KEY_MAP定义成MapString, String然后拿char去get()就会编译报错。JS里也有类似的类型隐患part[0]拿到的是字符串而对象键默认会转成字符串所以没问题。Python里part[0]是单字符字符串字典键也是字符串同样顺畅。另一个我在JS版本里踩过的坑是如果用Map而不是对象那就必须用keyMap.get(key)而不是keyMap[key]。这个差异很基础但在紧张答题的时候特别容易搞混。我建议要么统一用对象要么统一用Map别换来换去。6.4 变种题应对思路不同分隔符与跨键输入如果你遇到的题目不是用空格分隔而是用-或者,比如输入22-222-2222那处理方式就变成了先按-切分再执行同样的映射逻辑。这时候Python和JS的split(-)不会产生空串问题Java的split(-)同样不会因为中间连续分隔符的情况较少。但如果分隔符连续出现比如22--222那又会冒出一堆空字符串过滤逻辑依然不能省。还有一种变体是输入本身就是一串连续数字比如2222220222要求把0当作停顿。这种情况下就不能用split了得用我在开头说的状态机逐字符遍历。我在实际写这种变体时通常会用一个currentKey变量记录当前按键一个count变量记录连续次数遇到0或字符变化时立即输出。这种方法可以覆盖绝大多数题目变形建议你也掌握。7. 我的实操体会写这类模拟题的通用套路说实话九宫格按键输入这道题本身不难真正难的是写完之后能一次通过所有边界用例。我做了这么多年编程题总结出来的套路无非这么几条第一先画解析流程图再写代码。哪怕只是在纸上草草写几行也要理清楚输入怎么切分和按键怎么映射两件事。最忌讳的是上来就直接在循环里又是判断又是拼接写到一半把自己绕晕。第二编码之前就想清楚空输入、非法按键、连续分隔符三种例外。这三种情况不算复杂但覆盖了大部分报错点。你可以先写好过滤空串的代码再去处理主逻辑。第三不管用哪种语言把映射表单独抽出来。固定常量放在最前面后面只引用这样以后要改按键布局只需要动一行。这种思路不仅适用于这道题也适用于其他任何类似通过编号映射到实体的问题。第四多语言对比的价值不仅在取巧更在提升你对语言特性的敏感度。比如Python的split()默认行为、Java的StringBuilder、JS的数组join这些细节平时可能不会注意但恰恰是这类模拟题里决定代码质量和生死的地方。最后再分享一个小技巧如果你在OJ平台上提交一般会有多个隐藏测试用例。拿不准的时候自己先在本地把包含连续空格、末尾空格、空字符串的用例跑一遍再提交。这个习惯帮我避免过无数次本地正常线上全红的惨剧。希望这篇拆解能让你再遇到九宫格按键输入时不再只是背一个答案而是真正理解背后的逻辑模型然后在三种语言之间游刃有余地迁移。
返回列表