☰
力扣第657题:机器人能否返回原点的算法与工程实践
2026/10/7 5:07:29 网站建设 项目流程

做算法题的朋友,一定不会对“机器人能否返回原点”这个题目陌生。它出自LeetCode第657题“Robot Return to Origin”,本质是一个字符串处理与坐标模拟问题:机器人从原点(0,0)出发,按指令序列依次执行U、D、L、R四个方向的移动,问执行完所有指令后,机器人是否正好回到原点。

这题标签是简单,但很多人在面试或笔试里第一反应就是直接开个switch去模拟,结果写出来要么边界漏掉,要么被追问复杂度时卡壳。也有人觉得这题太基础,刷一遍就扔了。实际我在带团队和做面试官的过程中发现,恰恰是这类“看着简单”的题,最能暴露一个人对问题本质的理解深度,以及写代码时的工程习惯。这篇文章就围绕这道题,把思路拆解、多语言实现、边界测试、现实工程联想和面试加分点一次讲透。

1.1 坐标建模:先把问题翻译成“数学题”

机器人返回原点的判定,本质上是在问:经过一系列离散位移后,最终位置是否等于起始位置。

把二维平面拆开看,水平方向只由L和R决定,垂直方向只由U和D决定。假设机器人初始坐标为(x=0, y=0),那么每一条指令都在修改这两个坐标值:

  • U:y加1
  • D:y减1
  • L:x减1
  • R:x加1

执行完整个指令串后,判断x是否等于0且y是否等于0即可。这个“坐标状态机”是整道题的最小模型,理解它之后再写代码,基本不会错。

举个例子。“UD”这个指令串的执行过程是:(0,0) -> (0,1) -> (0,0),最终回到原点,返回true。“LL”的执行过程是:(0,0) -> (-1,0) -> (-2,0),最终停在(-2,0),返回false。“UUDDLRLR”这类看起来花哨的指令串,只要每一步都在更新坐标,最终位置一目了然。

1.2 两条主流解法:模拟行走与数量配对

解法A:直接模拟,也是大多数人第一反应会写的方案。遍历字符串中的每个字符,用一个if-else或者switch结构判断方向,然后更新坐标。遍历结束后,检查坐标是否为(0,0)。这个解法的优点是直观、不容易逻辑混乱,缺点是代码行数会多一些,尤其是用Java或C++写switch的时候。

解法B:数量配对,是我个人更推荐优先想到的方案。因为水平方向移动只影响x,垂直方向移动只影响y,而回到原点的充要条件是:向右的步数总和等于向左的步数总和,且向上的步数总和等于向下的步数总和。用公式表示就是:count(L) == count(R) 且 count(U) == count(D)。

这个解法的代码极其精简,在Python里甚至可以压缩成一行:return moves.count('L') == moves.count('R') and moves.count('U') == moves.count('D')。它不需要维护坐标,不需要分支结构,只需要统计四个字符的出现次数。

这两种解法的复杂度其实是相同的,都是O(n)时间、O(1)额外空间(解法A的坐标变量固定两个,解法B的计数器固定四个)。但解法B从数学上更接近问题的本质:二维平面上的位置变化可以分解为两个互相独立的一维运动,回到原点的条件就是每个一维方向上的净位移为零。

1.3 把解法B再抽象一层:这就是“数轴走格子”的叠加

如果觉得“数量配对”有点跳跃,可以把它降维到一维理解。想象一条数轴,机器人从0出发,只能向左或向右走。走完所有步后要回到0,需要满足什么条件?显然,只有向左走的步数等于向右走的步数,才能保证净位移为0,因为每向左走一步贡献-1,向右走一步贡献+1,总数相等时求和为0。

二维情况就是两条数轴(X轴和Y轴)的叠加,各自独立计算。U和D是一对数轴上的左右,L和R是另一对数轴上的左右。两个方向都满足“净位移为0”,合起来就是回到原点。这个抽象过程,就是把复杂问题“拆成独立子问题”的能力,也是这道题真正想考察的东西。很多候选人能写出模拟法,但很少能在追问下说出“方向独立、数量配对”这个关键点,这就是刷题深度不够的典型表现。

2. 代码落地:四种常用语言的实现与性能细节

题目本身的逻辑很简单,但代码落地的过程还是有很多讲究。不同语言的最佳写法差异很大,尤其要注意字符串遍历方式、字符比较效率和代码可读性之间的平衡。

2.1 Python实现:精简和易读的平衡

Python写这道题有得天独厚的优势,字符串的count方法是C语言层面实现,性能很好。最简洁的写法:

def judgeCircle(moves: str) -> bool: return moves.count('L') == moves.count('R') and moves.count('U') == moves.count('D')

四行代码解决战斗。但直接四个count其实会遍历字符串四次,时间复杂度依然是O(n),只是常数项优秀。如果追求严格的一次遍历,可以这样写:

def judgeCircle(moves: str) -> bool: x = y = 0 for move in moves: if move == 'L': x -= 1 elif move == 'R': x += 1 elif move == 'U': y += 1 elif move == 'D': y -= 1 return x == 0 and y == 0

这种写法可读性更好,逻辑一目了然,适合面试时边写边解释。我个人的建议是:面试中优先写模拟法,因为它的思路最容易被面试官理解,也方便后续跟着追问扩展;而count配对法可以在写完模拟法之后主动提一句“其实还可以通过统计个数实现”,这样展示的代码能力更立体。

2.2 Java与C++:switch结构的正确姿势

Java实现时,最自然的写法是for循环加switch:

public boolean judgeCircle(String moves) { int x = 0, y = 0; for (char c : moves.toCharArray()) { switch (c) { case 'L': x--; break; case 'R': x++; break; case 'U': y++; break; case 'D': y--; break; } } return x == 0 && y == 0; }

C++则推荐范围for循环配合if或switch,写法类似。注意一点:不要用Java的String.charAt(i)去逐个取字符也行,但toCharArray后遍历会多一次数组拷贝;另一种更高效的方式是moves.length()作为循环边界,配合moves.charAt(i),避免拷贝。不过刷题场景下toCharArray足够,性能差异可以忽略。真正的性能差别在字符串本身的遍历次数上。

2.3 JavaScript与Go实现

JavaScript的写法非常灵活:

var judgeCircle = function(moves) { let x = 0, y = 0; for (let move of moves) { if (move === 'L') x--; else if (move === 'R') x++; else if (move === 'U') y++; else y--; } return x === 0 && y === 0; };

Go需要注意switch的写法,以及rune和byte的类型区别:

func judgeCircle(moves string) bool { x, y := 0, 0 for _, c := range moves { switch c { case 'L': x-- case 'R': x++ case 'U': y++ case 'D': y-- } } return x == 0 && y == 0 }

Go的switch默认不会穿透到下一个case,写起来比Java干净不少。使用range遍历字符串时,这里实际上c是int32类型(rune),和字符字面量比较也没问题。

2.4 复杂度的两个真相:时间和空间都没得省

时间复杂度方面,不管是模拟还是计数,都必须读取每一个字符才能做出判断,所以最优时间复杂度就是O(n),不存在低于O(n)的解法。空间复杂度上,可以用固定的四个计数器,也可以用两个坐标变量,都是O(1)。即便用HashMap统计,由于键最多只有四种,空间复杂度也仍然是O(1),但这么做完全没有必要。

我在面试中经常问候选人的一个问题是:“这个算法能优化到O(n)以下吗?”正确答案是不能,因为任何指令字符都必须被读取至少一次,否则无法确定是否有未抵消的移动。这是一个典型的信息论下界问题,值得在讲解时主动说明,会让面试官觉得你对复杂度有真正深刻的理解。

3. 边界测试与隐藏坑点排查

很多题目不是败在主思路上,而是败在边界条件。这道题也不例外,虽然简单,但边界情况一旦漏掉,很容易在面试时被追问,甚至在某些变种题目中翻车。

3.1 边界用例清单

下面这些测试用例是我在实际验证代码时必跑的,整理成一个速查表:

输入期望输出原因分析
空字符串 ""true未移动,起始位置就是原点
"U"false只向上走一步,y不为0
"UD"trueU和D相互抵消
"LR"trueL和R相互抵消
"LLRR"true左右各两步,完全抵消
"LLR"false多出一步L,无法抵消
"ULRD"true四个方向各一次,整体净位移为0
"URDD"falseU和D抵消一次,还剩一个D,无法回归

空字符串这个用例很容易被忽略。从数学上讲,没有移动时位置就是原点,应该返回true;从工程逻辑上讲,边界条件定义的是“最终位置是否为原点”,而不是“是否发生过移动”。这两个概念不一样,想清楚就不会错。

3.2 我实际踩过的坑:方向约定不一致

最典型的坑是坐标轴的朝向。LeetCode原题意是U向上、D向下、L向左、R向右,没有明确指定y正方向朝上还是朝下。我在一次写C++代码时,习惯性地把U处理为y减1(因为很多图形库的屏幕坐标是Y轴朝下),结果同样的代码在LeetCode上就挂了。后来仔细一看,题目逻辑是“机器人从原点出发”,数学坐标里Y正方向朝上才是符合直觉的约定。

这个坑的教训是:刷题时必须以题目描述为准,不要用自己熟悉的图形库坐标系强行套用。在面试这种时间紧迫的场合,先确认方向与坐标的映射关系再动手写代码,看起来是浪费了半分钟,实际上避免了一个隐蔽的bug。

3.3 测试用例设计思路:从“等价类”到“穷举”

这道题因为输入空间是离散且有限的,理论上可以写出完整的穷举验证。比如只考虑前3步的所有指令组合,共有4的3次方等于64种情况,可以写个脚本验证模拟法和计数法的结果是否完全一致。这种“双实现互验”的方法,是我在确认某个算法实现正确性时经常使用的手段,虽然题目简单,但方法论可以迁移到更复杂的场景。

在日常工程中,我们当然不会为这么简单的函数写64个测试用例,但等价类划分的思想很关键:空串、单字符、成对抵消、不成对抵消、大小写非法字符混入,这几个等价类覆盖了所有逻辑分支,配合一两个随机生成的超长字符串做压力测试,基本就稳了。

3.4 一个容易被忽视的问题:非法指令怎么办

LeetCode的约束条件是字符串只包含U、D、L、R四种字符,但在真实工程中,输入数据往往不会这么干净。我在处理机器人控制日志时,就遇到过因为传感器误码而在指令串里混入了其他字符的情况。如果严格按原题逻辑,遇到非法字符直接忽略,结果可能会误判;正确做法应该是在解析时直接抛出异常或者标记为无效,而不是静默跳过。

我在面试中也会用这个点作为加分追问,询问候选人:“如果指令串里可能出现非法字符,你的代码会怎么处理?”大部分人会回答“忽略”,而更好的回答是“需要根据业务语义决定,如果非法指令代表一次错误运动,就应该直接失败返回,而不是继续执行剩余指令”。这种对异常语义的思考深度,往往比代码本身更能打动面试官。

4. 从这道题看真实世界的坐标与轨迹管理

刷题不能只停留在认知过这道题。这个“机器人能否返回原点”模型,在真实工程中到处都能看到它的影子,只不过包装得隐蔽一些。理解这些真实场景,反过来也能加深对题目本身的理解。

4.1 移动机器人里程计与实际场景中的“回到原点”

在两轮差速机器人中,通常会使用编码器累加计算左右轮的位移,从而推算机器人的全局坐标,这个过程叫做航迹推算或里程计计算。真实场景中有一个重要概念叫“回环检测”,即机器人逛了一大圈后,如何确认自己确实回到了出发点。这和题目逻辑非常像:位移向量是矢量,把每一段位移叠加起来,如果总和为零向量,理论上位置就回到了起点。

但真实世界比题目残酷得多,由于轮子打滑、编码器噪声、累积误差等原因,即使位移向量之和恰好为零,机器人的实际位置也可能与原点偏差几十厘米。这时候,工程师通常不会问“机器人能否返回原点”,而是问“机器人的闭环误差有多大”。这个误差的量化分析,就是现实版的题目变形。理解了这一点,你就明白了刷题时计算的“净位移”只是理想环境下的数学模型,工程上还要叠加误差修正参数。

4.2 游戏开发中的角色移动逻辑

游戏里的角色移动,如果把角色的位置表示成二维坐标,每帧根据按键更新坐标,本质上就是在执行一串动态生成的“指令序列”。判断角色是否回到了某个起点,比如玩家在迷宫地图里转了一圈是否回到出生点,用的就是同一个坐标累加模型。更实用的是,在实现“走迷宫自动回退”功能时,需要保存历史轨迹坐标并检验是否与当前位置重复,这相当于在每一帧都执行一次本题的判断逻辑。

很多游戏角色的动画循环、地图滚动的循环边界判定,也都隐含着“回到原点”的数学思想。比如一个NPC沿着固定路径巡逻,走了很长一段之后要回到起始位置,最简单也是目前工程上最稳妥的做法,不是去算整条路径的几何距离,而是记录路径点的坐标并按向量求和,判断是否闭合。闭合判定成功,才允许NPC转身走下一圈,否则会越走越远。

4.3 题目变形:三维、障碍物与随机指令

如果面试官想在这个基础上深入考察,通常会抛出几个变形题。

变形一:进入三维空间。机器人可以走U、D、L、R、F、B六个方向,分别对应三维坐标中的三个轴。这个变形的解法其实没有任何新的复杂度,因为X、Y、Z三个维度完全独立,依然是每个维度上配对数量相等即可返回原点。

变形二:增加障碍物。如果地图上有墙,X方向走一步可能被阻挡,这时数量匹配法就不成立了,因为向右走了三步但向左走了两步,若第三步撞墙,实际位置和理论净位移会不一致。这类题需要回溯或动态规划,难度会上一个台阶。但从这个对比也能看出,题目之所以限定向左和向右的步数天然互相独立,正是为了让我们用最简单的线性解法。

变形三:随机指令序列。把指令串换成随机过程,问“机器人经过n步之后回到原点的概率是多少”,这就变成了经典的概率论中的随机游走问题。二维随机游走的回路概率在数学上有严格结论,但在无限步数条件下回到原点的概率是1,这是概率论里的著名概念。从这道简单题能联想延伸到随机过程,如果面试中能落到这个层面,基本就是碾压级的表现。

4.4 面试中这道题真正考察的能力点

这几年我作为候选人、面试官和技术评审,多次反复遇到这道题。它的价值不在于考察“会不会判断四个字母是否配对”,而在于一系列软素质:能否从题目文字中准确提取数学模型,能否把二维空间运动分解为两个独立的一维问题,能否在写完代码后主动补充边界测试,能否主动分析时间复杂度的下界,能否在追问下把问题扩展到三维、障碍物、概率学等方向。

很多候选人能把模拟法写得又快又对,但被问到“你是不是可以用统计法”时会愣住,说明他们停留在背题层面,没有形成“问题归约”的思维习惯。反过来,也有一些候选人一上来就写count配对法,但当我追问“如果指令串是『UUURRR』你能很快判断吗”时,反而要算半天,暴露了对坐标分解本质理解的不足。这两种表现都说明没有真正吃透题目。正确的呈现方式,是先讲清楚物理意义,再给出两种解法,并说明各自的适用场景和等价性。

5. 常见问题速查表与排查实录

整理了我在调试和辅导时遇到的高频问题,供各位参考。

现象可能原因解决办法
空字符串返回了false缺少空串判断或逻辑写成“必须有移动”明确“最终位置是原点”就应返回true
输入"UD"返回true但"DU"返回false方向映射写反,导致坐标一增一减逻辑错乱统一约定,写出坐标映射表再编码
大量字符时超时重复扫描字符串多次用一次遍历模拟,或选用count的底层优化实现
混入大小写字母后结果错误没有做输入合法性校验增加输入清洗或异常处理逻辑
代码能过样例但漏掉"RRLL"没有测试多组配对情况补充等价类测试,至少覆盖所有抵消组合
面试官追问最优复杂度时回答O(log n)没有理解必须读取每个字符主动说明O(n)是信息论下界

除此之外,我在本地调试时习惯加一行临时日志,打印每一步后的坐标变化。虽然题目简单,但看着坐标一步步走的过程,能快速发现方向映射错误。等确认无误后,再把日志删掉。这个“临时打印”的习惯在复杂题中价值更大,放在这道题上是杀鸡用牛刀,不过对于新手理解坐标累加过程非常有帮助。

分享一个我实际遇到过的奇葩case。“UUDDLRLR”这个串,模拟法执行到前半部分时坐标一度偏离得很远,但最终回到了原点。这种数据能有效区分两种解法:计数法一眼看出U和D各2个、L和R各2个,直接返回true;而模拟法如果某个方向映射写反,会非常容易暴露错误。这类“中间偏离、最终回归”的数据,是我推荐的必测用例,它最能考验方向映射是否正确。

第二个经验是关于代码优化的时机。有人看到解法B简洁,就放弃模拟法直接背写法。但只背答案的代价是,一旦题目改成“指令可能包含非法字符”或者“需要返回每次移动后的坐标”,你就会无从下手。我建议两种解法都亲自写一遍,并且自己用同一组边界用例去验证两者结果一致。写完之后再想一想,为什么两个解法等价?它们的对应关系是什么?想通了,这道题才算真正过关。

第三个经验是关于随机测试和暴力验证。LeetCode不难,但如果你用的是Java语言且通过的是判题系统,我建议你在本地把题目改造成“随机生成1000条长度18的合法指令串”,再用自己实现的判断函数逐一检查,配合一个直接暴力模拟的实现交叉验证结果。这种生成器加双实现互验的做法,是排查逻辑bug最有效的工具。简单题的价值不在题本身,而在于你能通过它养成一套可迁移到难题上的工程化验证习惯。

我个人在实际操作中的体会是,这道题最好的打开方式不是在稿纸上把答案默写出来,而是先把它当成一道建模题来做:用三个自然段向自己解释“为什么二维空间位移可以拆成两个一维位移”“为什么回到原点的充要条件是各方向数量匹配”,确认逻辑闭合之后再写代码。这样一来,代码只是这个逻辑的忠实翻译,几乎不可能错。很多简单题,出错的关键都不在编程语言上,而在理解层面。希望这篇拆解能帮助你把“机器人能否返回原点”从一道刷过的题,变成真正理解透彻的算法模型,顺手也能把坐标分解的思维迁移到你的实际项目中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询