如果只看几何定义“正方形的判断”大概是我能想到的最简单的题四边相等、四角直角一句话的事。可这句话一旦搬进笔试和面试的编辑器里就开始长出各种犄角——输入的点有没有顺序坐标是整数还是浮点四个点里有没有重复如果给你十万个点让你数一数里面能组成多少个正方形又该怎么枚举我见过太多人栽在这道题上不是不认识正方形而是代码里少了一个边界判断或者多了一次没法比较的sqrt。这篇文章我想把这道题从最简单的版本拆到最刁钻的版本讲清楚题目变体、正确建模、现场作答节奏以及不同语言里容易踩的坑。不管是准备笔试面试的工程师还是想提升建模能力的学生应该都能拿走点东西。1. 这题凭什么能一路考到终面面试官真正在看什么先说一个反直觉的观察越像“送分题”的题目现场淘汰率往往越高。因为面试官不怕你不会就怕你把一个看似简单的问题写得很随便。判断正方形这道题表面上在考几何实际上在考你面对一个需求时能不能把“直觉”翻译成“逻辑”再把“逻辑”翻译成“不会跑偏的代码”。我帮朋友做模拟面试时见过几种典型的挂法。有人听到题就开心觉得四边相等就完事了结果菱形也被他判成正方形。有人老老实实算了六条边但最后一比对用了开根号浮点精度一抖差不多的数就是不相等然后开始怀疑人生。还有候选人写对了核心算法却因为没有先问“输入是四个点还是一堆点”“坐标是不是整数”把简单版和进阶版搞混了白费半天功夫。这些表现背后是同一类问题缺乏边界意识和复杂度迁移能力。面试官真正想看的东西我总结成四件事。第一需求澄清。输入到底长什么样是固定四个点还是一个点集四个点是无序排列还是已经按四边形顺序给出坐标范围是多少有没有重复点这些问题在真实工作中就是需求沟通面试官很在意你会不会先问清楚再动手。第二逻辑完备性。正方形判定有很多等价的几何条件但每个条件的适用场景不同。只看四边相等会误收菱形只看对角线垂直会误收某些风筝形必须把条件组合好并且排除四点重合、三点共线这些退化情况。第三数值稳定性。能用整数运算的地方就不用浮点能用平方距离的地方就不开根号。这不是洁癖是工程上的自我保护。第四复杂度迁移。固定四个点时怎么暴力都无所谓反正就六条边。但一旦变成“点集里数正方形”四重循环在n 10^5时直接爆炸。面试官想看你能不能在几秒内意识到规模变了、模型也得跟着变。这道题真正的难度不是那条几何定理而是同一句话在不同输入规模下有完全不同的解法。这也是为什么它能在笔试里反复出现——它像一把刻度尺量得出你写代码的工程素养而不只是数学记忆。2. 固定四个点的判断六条边排序法为什么是标准答案先处理最简单也最常见的版本给你四个点无序判断它们能不能组成正方形。很多人的第一反应是算四边相等这不够。反例太好找了菱形就是四边都相等的四边形但邻边夹角不是九十度。那算四个边相等、再算一个角是直角行不行理论上可以但代码写起来容易难看。要选一个点当顶点判断另外两个点构成的向量是否垂直再判断两条邻边长度是否相等还要处理“选哪个点当顶点”的分支。如果输入顺序不固定你得先找个确定能当直角顶点的点逻辑多一点就多一处出错的机会。所以我自己在面试里最推荐的是“六条边排序法”。四个点两两之间一共可以连出六条线段把它们的长度都算出来。正方形有个很漂亮的签名这六条距离排序之后前四个小值相等后两个大值相等而且大值刚好是小值的两倍。后一句话等价于对角线边长是边长的根号二倍而平方之后就是二倍关系用整数运算就能精确判断。from itertools import combinations def is_square(points): distances [] for (x1, y1), (x2, y2) in combinations(points, 2): dx, dy x1 - x2, y1 - y2 distances.append(dx * dx dy * dy) distances.sort() side, diag distances[0], distances[4] return side 0 and \ distances[0] distances[1] distances[2] distances[3] and \ distances[4] distances[5] and \ diag 2 * side这里有一个关键的设计决策我们比较的是距离平方不是距离。坐标是整数时距离平方是整数比较起来毫无误差而一旦开根号哪怕边长真的是根号二浮点表示也可能差一点点。笔试环境千奇百怪尽量把问题变成整数比较是永远不会错的原则。这个方法的充分性也值得想清楚。表面上它只检查了“四个短边相等、两个长边相等、长边是短边的两倍”为什么就足以推出正方形可以从四边形的不等式角度理解四个点里最远的两个点一定构成某条对角线剩下四个距离是四条边的长度。四边相等说明这是一个菱形再加上对角线平方等于两倍边长平方说明对角线长度等于边长的根号二倍。菱形的对角线互相垂直且平分当两条对角线长度相等时它只能是正方形。画个图验证一遍就踏实了。还有个防御判断差点被我漏掉side 0。如果四个点里有重复点六个距离里会出现零整个签名可能被意外满足所以先排除。这是我写这类题养成的习惯——任何几何判断先问自己“退化输入会不会误判”。如果题目明确告诉你四个点是按顺序给的也就是已经构成了一个四边形那确实可以更快算四条边都相等再算任意一条对角线长度的平方等于两倍边长平方就行。但我很反对一上来就默认输入有序。无序才是最常见的描述你要么先问要么就直接上六条边法。在面试里多写两行代码换取“绝不出错”是非常划算的交易。3. 变体二一堆点里数正方形核心是把“正方形”变成公式固定四个点的版本通常只算热身题真正的分水岭是这个进阶版给你n个点坐标都是整数统计其中能组成正方形的四元组有多少个。第一反应是C(n, 4)枚举所有四元组然后套六条边法可惜n 10^4时组合数已经到了十的十四次方量级笔试环境直接超时面试官也会摇头。这个版本的破局点在于不要枚举四元组枚举一条边。给定两个点作为正方形的一条边另外两个顶点是可以被唯一算出来的不需要搜索。怎么算设这条边从点A (ax, ay)指向点B (bx, by)向量是(dx, dy) (bx - ax, by - ay)。把这个向量逆时针旋转九十度得到(-dy, dx)然后分别加到B和A上就得到另外两个候选顶点。def count_squares(points): point_set set(points) ans 0 for ax, ay in points: for bx, by in points: if (ax, ay) (bx, by): continue dx, dy bx - ax, by - ay x3, y3 bx - dy, by dx x4, y4 ax - dy, ay dx if (x3, y3) in point_set and (x4, y4) in point_set: ans 1 return ans // 4旋转公式怎么记才不容易忘我习惯从复数角度理解一个向量(x, y)对应复数x yi乘一个i得到(-y, x)就是逆时针旋转九十度。如果你更习惯几何直觉验证也很简单点乘(dx, dy) · (-dy, dx) -dx·dy dy·dx 0说明垂直模长分别是dx² dy²说明长度不变确实是旋转而不是缩放。为什么选“边模型”而不是“对角线模型”两个点作为对角线也能推出另外两个顶点先找中心点再把对角向量旋转九十度加减到中心点上。但问题在于整数坐标的对角线可能给出半整数顶点比如中心点是(0.5, 0.5)。一旦出现半整数哈希键的存储和比较就开始恶心了。边模型里全是整数加减法没有半点精度问题这是它最大的工程优势。计数的时候有个非常隐蔽的细节我上面的代码枚举的是有序点对也就是每个A, B都会和B, A各算一次。一个正方形有四条边每条边恰有一个方向能旋转出这个正方形本身所以同一个正方形会被命中四次最后要除以四。你可能会想那我改成只枚举i j的无序点对最后不就不用担心了吗事实恰恰相反无序点对只取每条边的一个方向同一个正方形可能被命中两次也可能一次都没命中完全取决于点的索引顺序。我试过用i j加ans // 4本地样例过了换一组顺序就少了一半。所以要么老老实实枚举有序点对最后除以四要么用排序后的四元组去重。去重的写法也顺便给出来复杂度多一个log但思路更直白适合当场写代码容易慌的人。def count_squares(points): point_set set(points) seen set() ans 0 n len(points) for i in range(n): ax, ay points[i] for j in range(i 1, n): bx, by points[j] dx, dy bx - ax, by - ay x3, y3 bx - dy, by dx x4, y4 ax - dy, ay dx if (x3, y3) in point_set and (x4, y4) in point_set: quad tuple(sorted([(ax, ay), (bx, by), (x3, y3), (x4, y4)])) if quad not in seen: seen.add(quad) ans 1 return ans这两种写法面试官都能接受关键是你要说清楚自己为什么选这一种。如果追求最优复杂度用有序点对版时间复杂度是O(n²)空间是O(n)去重版会多一个O(log n)的排序因子但逻辑上容错率高。4. 那些年我见过的翻车现场浮点、菱形、半整数与去重这一节我想专门聊坑。因为这道题思路说穿了都不难但现场写起来坑是一个接一个。第一个坑是菱形冒充正方形。只看四条边相等菱形会骗过你。我自己见过最典型的错误代码是四行写完信心满满结果测试里塞一个(0, 0), (1, 0), (2, 0), (1, √3)就露馅了。所以六条边排序法里的长边判据不是可有可无它对菱形的“对角线不等于两倍边长平方”做了精确拦截。第二个坑是sqrt精度。坐标是浮点数时开根号后的比较更是玄学同一个数值在不同编译器、不同优化级别下可能得到不同的表示。如果你非要比较浮点长度得写fabs(a - b) eps而eps取多少又是一个新问题。所以整数坐标下用平方距离是这个领域的基本功我甚至会在面试里主动说一句“为了避免精度问题这里全部用平方距离比较”这句话本身就是加分项。第三个坑是重复计数。点集计数版里那个除以四特别容易出错。我前面就说过如果枚举无序对再除以四结果会随索引顺序变化同一个正方形可能只被统计两次。实话实说这个坑我第一次刷题就踩过后来才意识到应该枚举有序点对。现在看到别人代码里写i j然后除以四我都会下意识警惕。应对方法无非两种除以四前确认自己枚举了所有有序点对或者用排序去重彻底绕开这个数学问题。第四个坑是半整数哈希键。如果采用对角线模型中心点可能是(0.5, 0.5)旋转后的顶点也可能是半整数。你没法直接把(0.5, 0.5)塞进以坐标为键的哈希表里因为浮点键在比较时会让你怀疑人生。有人会说“那我判断分子是不是偶数不就行了”——可以但到处是if ... % 2 0的分支代码可读性直接下降。用边模型就永远遇不到这种情况这也是我强烈推荐它在面试中当主方案的原因。第五个坑是退化输入。四个点里有重复、三点共线、甚至全部重合这些情况不能进入正方形判定。六条边排序法里的side 0就是干这个用的。点集计数版通常假设输入没有重复点但如果题目没说你要么先问要么在开头point_set set(points)时意识到这一点。这些坑没有一个需要高等数学全是工程细节但它们才是面试的真正筛选器。能把细节处理干净的人往往就是能让人放心把模块交给他的人。5. 面试现场怎么把这题演成加分项技术准备是一回事现场表达是另一回事。这道题在面试里能不能变成加分项很大程度上取决于你怎样组织说话顺序。我建议拿到题之后前三十秒不要急着写。先确认三件事输入规模是多少是固定四个点还是一个点集点是有序还是无序坐标是不是整数、有没有重复。这个提问本身就在向面试官传递信号这人会先厘清需求而不是上来就干。如果面试官说“先按最简单的处理”那你就从固定四点版开始如果他说“要考虑大规模”那就直接讲点集计数版。真正写之前先用一句话说明思路。固定四点版就说“四个点两两之间六条距离排序后检查长度签名”。点集计数版就说“枚举一条边旋转九十度生成另外两个点用哈希集合查存在性”。话不用多但要让面试官跟得上你。写代码的时候我建议按“主逻辑→防御判断→测试用例”的顺序来。不要一上来就纠结边界先把中间那段核心逻辑写出来比如旋转公式和哈希查询然后再补side 0这类判断。这样即使时间不够面试官也能看到你的核心思路是完整的。跑测试用例这里很多人会跳过非常可惜。固定四条点这个题我会主动报出几个边界用例一个普通正方形一个旋转了任意角度的正方形一个菱形一个四点重合一个只有三个不同点。点集计数版再补一个“两个正方形共用一条边”的用例既能验证除以四的逻辑又能展示你对重复计数有意识。主动跑用例这个动作在面试里的价值甚至比代码本身还高。最后如果面试官追问最常见的几个问题要能接上。问“为什么平方距离不用开根号”答整数精度问题。问“为什么是充分条件”答四边相等推出菱形对角线平方两倍推出对角线是边长的根号二倍合起来就是正方形。问“如果不能用哈希表额外空间怎么办”可以先答排序点集后用二分查找验证候选点或者用邻接矩阵把空间复杂度换成时间再说明这种做法的适用条件。能把这些问题接住这道题你就真正立于不败之地了。6. 换一种语言就换一种活法主流语言实现的细节差异同一个算法换个笔试语言坑的位置就完全不一样了。基本解法大家都懂但语言层面的细节往往是临场翻车的重灾区。语言哈希观感常见坑Pythontuple天然可哈希写起来最顺别用浮点当 key组合数别写成四重循环Java没有内置Pair类要用自定义类或字符串拼接hashCode重写可别漏字符串拼接注意分隔符Cunordered_setpairint,int需要自定义哈希或者用long long压位但别把负号弄丢JavaScript对象 key 会被强制转字符串推荐用SetString数组做 key 会退化成引用比较永远查不到Kotlindata class自带结构相等与哈希很省心用listOf当 key 前要排序否则同一四边形可能有不同表示先说 Python。这篇里的代码全是 Python 风格核心原因是tuple直接可以放进set和dict不需要写任何哈希辅助逻辑。要注意的是如果你非要把坐标拼成字符串x,y其实也能工作但每次都要split既慢又容易错。Python 里更自然的写法是直接用(x, y)。Java 会麻烦一点。老的笔试环境里没有现成的Pair很多人的第一反应是HashMapString, Integer用x , y当 key。这个能用但要小心分隔符坐标里不会出现逗号所以,是安全的不要把 key 拼成xy这种会冲突的格式。更好的方案是用long压位((long) x 32) ^ (y 0xffffffffL)一个long就能唯一表示一对整数坐标。这个技巧不复杂但能让你的代码去掉大量字符串操作。C 的坑在负数和自定义哈希。unordered_setpairint,int需要你提供一个哈希函数直接塞没法编译。实战里我有两个偏好一个是定义struct Key { int x, y; bool operator(const Key) const; }然后写哈希另一个就是转long long把x和y都先转成无符号整数再移位拼接处理负数时要小心符号位。更省心的做法是用setpairint,int查询变O(log n)但总复杂度从O(n²)变成O(n² log n)如果面试官较真复杂度你得提前想好怎么解释。JavaScript 环境最容易被对象哈希坑到。很多人写const set new Set()然后把点存成{x, y}结果两个结构相同但对象引用不同的点永远被认为不相等查存在性直接失败。老老实实用SetStringkey 就拼x,y简单可靠。Kotlin 因为有data class Point(val x: Int, val y: Int)结构相等和哈希都是自动的是这几个语言里最省心的。语言差异这部分看似是细节其实在笔试里能救命。很多题目你不是不会是哈希键没写好导致超时或查不到。记住了这些坑换什么环境都能稳一点。最后聊一点个人体会。我一度觉得这种题太基础不值得专门准备。后来有一次围观别人现场写一个候选人洋洋洒洒写了四十多行把旋转公式、哈希去重全都搞定了很漂亮另一个候选人五秒写出超出预期的简单解法最后却挂在浮点比较上提交瞬间才发现不对。那一刻我才明白判断正方形真正考的不是“知不知道为什么”而是“能不能在高压下写出绝不越界的代码”。从那以后凡是能用整数算的几何我绝不开根号凡是能用平方距离的地方我绝不去比较长度。这个小习惯帮我躲过了很多莫名其妙的测试用例也希望对你有点用。