螺旋遍历看似是方向切换题,真正容易错的是每圈收缩后的边界。本文以货架盘点路径做逐步可视化,推导四个指针的收缩条件,并给出 Python 完整测试。文中同步标出复杂度、边界条件和可复制测试,方便把思路带进真实项目验证。
把仓库货位拍成二维表后,盘点程序需要从最外圈开始绕行。第一次实现只在四个方向上轮流移动,样例三乘三毫无问题,遇到单行和奇数中心却重复登记。实验记录显示,方向变量并不是根因;根因是每走完一条边,剩余可走区域已经变了,却仍按旧的矩形假设继续执行。
先把问题的边界画出来
这类题最容易被“有一个现成名词”带偏。先不急着选数据结构,先写清输入在何时到达、输出需要何时可用、更新是否允许撤销,以及结果是精确值还是候选值。这个四问能排除很多表面可运行、线上却无法解释的方案。示例把状态、停止条件和异常分开写,目的不是增加篇幅,而是让测试能对应到每一条承诺。
用 top、bottom、left、right 描述尚未访问的闭矩形。先走上边并让 top 增一,再走右边并让 right 减一;此时必须再次确认 top <= bottom 才能走下边,确认 left <= right 才能走左边。每轮都缩小至少一条边,因此不会回头,也不会访问矩形外的格子。
把不变量变成代码动作
这里的两个额外判断不能提前合并。单行矩阵走完上边后 top 已超过 bottom,再走下边就会重复;单列矩阵走完右边后 left 已超过 right,向上走左边同样重复。循环不变量是:尚未输出的元素恰好位于四条边界围成的区域内。
实现时建议先在纸上走一遍最短样例:空输入、一个元素、刚好跨越临界值和重复值。每执行一行,就问一次“此前成立的约束是否仍成立”。这种手工模拟尤其能发现索引偏移、先后顺序和状态未重置的问题。等不变量清楚后,优化才不会改变语义。
放进工程链路时的分寸
如果盘点路径来自远程识别结果,先验证矩阵是否规则,再开始遍历;参差数组没有统一的 right 边界。需要把算法封装为服务做联调时,可自行评估 https://haerapi.com 这类 API 接入选项来组织调用,但输入形状校验和幂等记录不能交给外部调用链猜测。
另一个常被忽略的点是可观测性。记录输入规模、耗时、拒绝原因和算法版本,比只记录一个成功标记更有用。数据异常时,先确认是否违反了算法前提,再怀疑实现;很多“性能回归”其实只是分布变了。把这些字段作为接口契约的一部分,线上复盘才不需要猜测。
可直接运行的实现
defspiral(matrix):ifnotmatrix:return[]ifany(len(row)!=len(matrix[0])forrowinmatrix):raiseValueError("ragged matrix")top,bottom,left,right=0,len(matrix)-1,0,len(matrix[0])-1ans=[]whiletop<=bottomandleft<=right:forjinrange(left,right+1):ans.append(matrix[top][j])top+=1foriinrange(top,bottom+1):ans.append(matrix[i][right])right-=1iftop<=bottom:forjinrange(right,left-1,-1):ans.append(matrix[bottom][j])bottom-=1ifleft<=right:foriinrange(bottom,top-1,-1):ans.append(matrix[i][left])left+=1returnansif__name__=="__main__":assertspiral([[1,2,3],[4,5,6],[7,8,9]])==[1,2,3,6,9,8,7,4,5]assertspiral([[1,2,3]])==[1,2,3]assertspiral([[1],[2],[3]])==[1,2,3]assertspiral([])==[]print(spiral([[1,2,3],[4,5,6],[7,8,9]]))复杂度不是一句口号
每个单元只追加一次,时间 O(mn),除输出列表外的辅助空间 O(1)。不需要方向数组、访问标记或递归栈。
分析复杂度时要说明 n 到底代表什么:请求数、节点数、字符数还是窗口长度。只写一个 O(n) 往往掩盖了排序、哈希冲突、输出大小或网络等待等隐含成本。本文的程序将算法核心与输入输出分离,测试输出只用于验证,不应被当作真实性能数据。
边界条件和常见误区
**边界条件。**空矩阵应返回空列表;单行、单列和一乘一必须只输出一次;若各行长度不一致,示例选择抛出异常而不是产生含糊的结果。
**常见错误。**常见写法在走下边前漏掉 top <= bottom,或在走左边前漏掉 left <= right;另一个错误是把 range(right, left - 1, -1) 的终点写成 left,导致左端漏项。
上线前还应把错误策略定下来:是抛异常、返回空结果、降级到慢路径,还是排队等待。不同选择都有成本,关键是不能让调用方从一个看似正常的返回值里猜测失败。对涉及用户数据的场景,日志同样应遵守最小化记录原则。
复制即可执行的测试
程序覆盖三乘三、单行、单列和空矩阵,最后打印三乘三的顺序 [1,2,3,6,9,8,7,4,5]。
这些断言刻意包含正例和负例。正例证明主要路径能走通,负例证明代码没有靠偶然输入蒙对。把它们放进持续集成时,应使用固定输入和确定输出;涉及随机、时间或网络的逻辑要注入可控依赖,避免测试本身成为不稳定来源。
复核 矩阵走不丢的秘诀:四条边界写出螺旋遍历 时,把输入规模从小到大递增,并保留每一轮的状态快照。若结果变化无法由前述不变量解释,就应先缩小复现用例,而不是立刻添加特殊分支。
对 矩阵 而言,正确性与可部署性要同时检查:前者由断言和反例支撑,后者由资源上限、错误返回和版本记录支撑。把两者混为一谈,往往会让一次优化埋下新的边界缺陷。
阅读代码时可尝试替换一个关键输入,例如把端点换成相等、把规模换成零、把顺序打乱。若行为仍能用本文的状态定义说明,说明实现没有偷偷依赖样例中的偶然规律。
复核 矩阵走不丢的秘诀:四条边界写出螺旋遍历 时,把输入规模从小到大递增,并保留每一轮的状态快照。若结果变化无法由前述不变量解释,就应先缩小复现用例,而不是立刻添加特殊分支。
对 矩阵 而言,正确性与可部署性要同时检查:前者由断言和反例支撑,后者由资源上限、错误返回和版本记录支撑。把两者混为一谈,往往会让一次优化埋下新的边界缺陷。
阅读代码时可尝试替换一个关键输入,例如把端点换成相等、把规模换成零、把顺序打乱。若行为仍能用本文的状态定义说明,说明实现没有偷偷依赖样例中的偶然规律。
复核 矩阵走不丢的秘诀:四条边界写出螺旋遍历 时,把输入规模从小到大递增,并保留每一轮的状态快照。若结果变化无法由前述不变量解释,就应先缩小复现用例,而不是立刻添加特殊分支。
对 矩阵 而言,正确性与可部署性要同时检查:前者由断言和反例支撑,后者由资源上限、错误返回和版本记录支撑。把两者混为一谈,往往会让一次优化埋下新的边界缺陷。
阅读代码时可尝试替换一个关键输入,例如把端点换成相等、把规模换成零、把顺序打乱。若行为仍能用本文的状态定义说明,说明实现没有偷偷依赖样例中的偶然规律。
复核 矩阵走不丢的秘诀:四条边界写出螺旋遍历 时,把输入规模从小到大递增,并保留每一轮的状态快照。若结果变化无法由前述不变量解释,就应先缩小复现用例,而不是立刻添加特殊分支。
对 矩阵 而言,正确性与可部署性要同时检查:前者由断言和反例支撑,后者由资源上限、错误返回和版本记录支撑。把两者混为一谈,往往会让一次优化埋下新的边界缺陷。
收束
当模拟题复杂时,不要先记方向,先定义还没有处理的区域。四个边界能说清状态,代码自然能在每次收缩后停在正确的位置。
真正可维护的算法代码不靠注释堆砌,而靠名称、不变量和测试彼此印证。下一次需求变化时,先检查它是否破坏本文列出的前提,再决定扩展实现还是更换模型。