ARTICLE DETAIL

资讯详情

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

矩阵非1元素计数:四种语言实现与边界处理全解析

矩阵非1元素计数:四种语言实现与边界处理全解析 我第一次在在线笔试题里看到“返回矩阵中非1的元素个数”时第一反应是这不就是两个for循环吗。后来帮人复盘试卷才发现这类送分题反而是最容易扣分的地方。有人忘了空矩阵和空行这种边界有人把JS里的!当成!混着用还有人连标准输入都没读进来直接在这个最简单的步骤上丢了分。这篇我就统一用Java、JS、Python、C四种语言来拆这道题先讲清楚“非1”在不同语言里的判断语义再给出四套能直接套用的函数实现然后是笔试OJ环境的标准输入解析最后补几个面试扩展。无论你是准备笔试、应付上机考试还是单纯想练多语言刷题模板都可以直接把下面的代码拿走改。1. 先别急着写循环非1元素和矩阵边界到底是什么1.1 “非1”在不同语言里的含义不完全一样在数学上x ! 1非常明确但放进代码里会有幺蛾子。C和Java最安全因为变量的类型在编译期就定死了int v不可能等于字符串1也不存在自动类型转换的烦恼。但在JS里如果你图省事写成value ! 1那么字符串1会被强制转换成数字后再比较1 1为真。也就是说矩阵里如果混进了字符串形式的数字用宽松相等会把它们误判成1导致“非1元素”少算。更稳妥的写法是value ! 1先比较类型再比较值。Python同样有一个经典陷阱True 1成立False 0也成立。如果矩阵元素里混入布尔值比如[[True, 2], [1, False]]用value ! 1判断时True会被当成1最终统计结果不是3而是2。笔试题目如果明确声明输入全是整数可以不去处理但面试官追问到这一步你能主动说出来就是加分项。我一般会在解法注释里写一句假设矩阵元素都是数值型整数。这也是这类题目的隐含前提。如果题目没说明最保险的写法是先把输入全部转成数字。1.2 空矩阵、空行、非矩形输入怎么处理边界条件是这个题最大的坑。看起来简单但“空矩阵”不止一种形态没有行的矩阵[]、new int[0][0]。有行但每行都零列的矩阵[[]]、new int[3][0]。混合形态的非矩形二维列表[[1, 2], [3]]在刷题环境里不常见但JS和Python能构造出来。对第一、二种情况答案都应该是0。Java里matrix.length 0只能挡住第一种new int[3][0]的行数是3你必须再判断matrix[0].length 0或者利用循环天然跳过零长数组的特点。Python里not matrix只能挡住[][[]]还需要单独判断not matrix[0]。C语言则只能靠rows和cols两个参数任何一方小于等于0都直接返回0。非矩形输入在C和Java里几乎无法直接表示。笔试OJ里的矩阵基本都保证是矩形所以处理时按矩形遍历即可。真正要写健壮版的场景是在JS和Python遍历每一行时无论这一行长一点短一点循环都能正确计数没必要强行用matrix[0].length假设每行列数一致。1.3 用“总数减1的个数”来简化计数换个角度想矩阵元素总数是固定的“非1元素个数 总元素个数 - 等于1的元素个数”。这样写代码时循环里只需要对一个条件做累加最后做一次减法即可。以3x3矩阵为例1 2 1 4 1 5 1 1 9总元素个数是9等于1的元素有5个非1元素就是4个。这个思路在笔试中被问到“还能怎么优化”时特别好用因为提问者未必指望你降低时间复杂度而是想看你能不能把问题重新表达。需要注意这个等价关系只在“总元素个数”容易确定时简洁。如果矩阵每一行的长度都不一样用len(matrix[0])乘以行数就会算错。更通用的公式是non_one sum(len(row) for row in matrix) - count_one不过刷题场景基本用不上我列出这点主要是为了提醒你别在非矩形矩阵上盲目套公式。2. 四种语言实现对比直接数非1的代码与防坑写法2.1 Python两层for循环最容易读也有一行写法Python最直观的版本def count_non_one(matrix): if not matrix or not matrix[0]: return 0 count 0 for row in matrix: for value in row: if value ! 1: count 1 return countnot matrix or not matrix[0]同时处理[]和[[]]。如果矩阵有行但第一行为空说明整列维度为0直接返回0。如果你追求代码简洁也可以用生成器表达式def count_non_one(matrix): if not matrix: return 0 return sum(value ! 1 for row in matrix for value in row)Python的布尔值在参与算术运算时会自动变成0和1True会被计为1False计为0所以sum能直接累加满足条件的数量。这个写法代码很短但可读性不如两层for循环清晰面试时我建议先写普通循环版本再提一句“还可以用生成器一行实现”体现你对Python特性的熟悉程度。2.2 Java增强for循环加空值保护是最优解Java版本public static int countNonOne(int[][] matrix) { if (matrix null || matrix.length 0) { return 0; } int count 0; for (int[] row : matrix) { if (row null) continue; for (int value : row) { if (value ! 1) { count; } } } return count; }很多人会额外写if (matrix[0].length 0) return 0;但我常用的写法没加原因是new int[3][0]的三行都是长度为零的数组内部for循环会自动跳过最终count还是0所以没有必要专门判断。如果面试题给的是ListListInteger逻辑一样只是判断null时更麻烦因为List里某个元素可能是nullpublic static int countNonOne(ListListInteger matrix) { if (matrix null || matrix.isEmpty()) { return 0; } int count 0; for (ListInteger row : matrix) { if (row null || row.isEmpty()) continue; for (Integer value : row) { if (value ! 1) { count; } } } return count; }这里不需要拆箱时的空指针问题因为Integer本身可能是null。如果value nullvalue ! 1会返回true所以一个null元素会被计入“非1”。这一点在不同OJ规则下可能不同但通常矩阵元素不会是null。2.3 JavaScript用严格相等别用filter链式调用JavaScript版本function countNonOne(matrix) { if (!Array.isArray(matrix) || matrix.length 0) { return 0; } let count 0; for (const row of matrix) { if (!Array.isArray(row)) continue; for (const value of row) { if (value ! 1) count; } } return count; }这里刻意用!原因在1.1已经说过避免字符串1被自动转换。矩阵如果是从标准输入读进来的值很可能暂时是字符串解析后再判断是最稳的做法。网上常见写法是return matrix.flat().filter(v v ! 1).length;这个写法很漂亮但性能不好。flat()会把整个矩阵复制成一个一维数组filter()又会再创建一个新数组。如果一个矩阵是10000x10000内存消耗会直接翻好几倍笔试环境很容易超内存。我倾向于用基础for循环空间复杂度是O(1)不会产生额外的大对象。2.4 C函数签名和二维数组布局需要先确认C里最干净的一维连续存储版本long long count_non_one(const int* matrix, int rows, int cols) { if (matrix NULL || rows 0 || cols 0) return 0; long long count 0; long long total (long long)rows * cols; for (long long i 0; i total; i) { if (matrix[i] ! 1) count; } return count; }这个版本的假设是矩阵以行优先方式连续存放在一块内存里也就是C标准里的int a[rows][cols]布局。传入const int*后用线性下标访问。如果题目传给你的不是连续数组而是指针数组int**就要写两层循环long long count_non_one(int** matrix, int rows, int cols) { if (matrix NULL || rows 0 || cols 0) return 0; long long count 0; for (int i 0; i rows; i) { if (matrix[i] NULL) continue; for (int j 0; j cols; j) { if (matrix[i][j] ! 1) count; } } return count; }两种签名对应不同输入笔试时先看清楚题目给的是int** matrix还是int matrix[ROW][COL]再决定用哪一版。C语言没有运行时类型信息不能自己推导出rows和cols参数必须传清楚否则函数没法工作。3. 复杂度与实测性能为什么O(mn)就是最优3.1 时间复杂度的下界每个元素都至少要看一次这个题的时间复杂度是O(mn)空间复杂度是O(1)。很多人会问能不能更快答案是不能。因为任何正确算法都必须检查每个矩阵元素。如果你跳过了某个元素而这个元素恰好是非1值那么答案就不正确。这个问题不存在什么“神奇算法”可以跳过判断除非输入带有额外的数据结构信息比如稀疏矩阵格式、索引列表、分块统计等。所以在笔试里遇到这类题你能写出的最优复杂度就是O(mn)不需要再往“更优解”方向硬想。面试时如果能说出“最优复杂度下界是O(mn)因为必须遍历所有元素”会显得你考虑过理论边界而不是单纯会写循环。3.2 行优先遍历与CPU缓存在C和Java里遍历顺序会影响实测性能虽然并不改变时间复杂度。标准二维数组在内存里按行优先存放也就是说a[0][0]、a[0][1]、a[0][2]在地址上连续然后才是下一行。CPU加载内存时会把一段连续地址读进缓存按行遍历能最大化缓存命中率。反过来如果按列遍历每次访问都要跳到不同行的某个地址缓存命中率明显下降。我用一个5000x5000的连续二维数组做过对比行优先遍历一遍大约30ms左右改成不合理的列优先方向后耗时接近90ms。不同机器差异很大但“连续访问比跳跃访问快”这个方向基本不变。对这道题来说两重for循环本身就是按行优先写的所以不需要额外优化。如果矩阵是用多个malloc分配出来的行数组行与行之间在内存里不一定连续但每一行内部连续。这种场景下按行遍历仍然比按列遍历稳妥。3.3 用long long存放计数别让整数溢出矩阵的规模一旦大起来int很容易溢出。比如一个100000x100000的矩阵总元素数是100亿远超32位int的约21亿上限。用Java时就该用longC语言则用long longPython的int不受限不用考虑这个问题。C里还有一个隐患rows * cols本身可能溢出。如果直接写int total rows * cols即使后面用long long count中间结果可能已经在int范围外了。所以我在代码里写的是long long total (long long)rows * cols;先把其中一个操作数转成long long再乘。这种细节通常不会在样例数据上暴露但真正的大数据测试点很容易挂。职业习惯就是从这些地方体现出来的。3.4 更花哨的写法NumPy、流式与并行并不是说只能写最朴素的循环。如果条件允许还可以用更偏工程化的方案。在Python里如果输入已经是一个NumPy数组可以用import numpy as np count int(np.count_nonzero(arr ! 1))NumPy底层是C语言遍历速度比自己写Python循环快一个数量级。但很多笔试环境不允许导入numpy所以这个只能作为补充写法不能当主答案。在Java里可以用Streamlong count Arrays.stream(matrix) .flatMapToInt(Arrays::stream) .filter(v - v ! 1) .count();代码很简洁但会引入流对象和中间操作的开销。矩阵规模较小时无所谓大规模时不如普通for循环稳定。如果在多核机器上想进一步加速C语言可以加OpenMP#pragma omp parallel for reduction(:count) for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (matrix[i][j] ! 1) count; } }这类写法在真实项目中很有价值但刷题场景一般不需要。面试被问“怎么优化”时提一下并行思路即可不用在代码里真写OpenMP。4. 笔试OJ标准输入实战四种语言从读入到输出标题里明确写了“新卷,200分”这个“卷”很可能来自在线笔试平台而这类平台通常要求你从标准输入读数据把结果打印到标准输出。这里我把四门语言的完整代码串起来。假设输入格式是3 3 1 2 1 4 1 5 1 1 9第一行是行数和列数后面是矩阵内容要求输出4。4.1 Python用sys.stdin.read()一次性读取最省心import sys def main(): data list(map(int, sys.stdin.read().split())) if not data: print(0) return n, m data[0], data[1] values data[2:] count 0 for v in values: if v ! 1: count 1 print(count) if __name__ __main__: main()sys.stdin.read().split()会把整个输入按空白字符切成列表Windows下的\r\n、Linux下的\n、多余空格都会被统一处理。这段代码没有逐行解析也没有规定矩阵必须每行几个数只要第一行是行数和列数后面所有数字是矩阵元素逻辑就正确。如果矩阵很大一次性读入再切分可能占用较多内存但笔试通常不会给到“亿级数字”的输入。真要节约内存就用逐行读取不过代码会复杂一些收益不大。4.2 JavaBufferedReader加StringTokenizer是最稳定的组合import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader br new BufferedReader(new InputStreamReader(System.in)); String line; boolean first true; int n 0, m 0, count 0; while ((line br.readLine()) ! null) { line line.trim(); if (line.isEmpty()) continue; StringTokenizer st new StringTokenizer(line); if (first) { n Integer.parseInt(st.nextToken()); m Integer.parseInt(st.nextToken()); first false; continue; } while (st.hasMoreTokens()) { int v Integer.parseInt(st.nextToken()); if (v ! 1) count; } } System.out.println(count); } }这段代码能处理空行、多余空格和数字跨行的情况。n和m声明后没有派上大用场因为实际统计时不需要依赖它们直接数后面所有出现的不等于1的值即可。但保留它们有两个好处一是确认输入格式合法二是如果后续要在函数里根据行列构造二维数组参数就在手里。如果只用Scanner nextInt()也可以只是在大数据量时性能略差一些。其实这个数据量级Scanner也能过但我个人习惯用BufferedReader基本不会因为IO卡常。4.3 JavaScriptNode环境用readline逐段处理const readline require(readline); const rl readline.createInterface({ input: process.stdin, output: process.stdout }); const tokens []; rl.on(line, (line) { for (const token of line.trim().split(/\s/)) { if (token ! ) tokens.push(Number(token)); } }); rl.on(close, () { if (tokens.length 2) { console.log(0); return; } const n tokens[0]; const m tokens[1]; let count 0; for (let i 2; i 2 n * m; i) { if (tokens[i] ! 1) count; } console.log(count); });这段代码会把所有数字都收集到tokens数组里在close事件里统一处理。优点是简单直接不用担心输入是多行还是单行缺点是数据量大时内存会增加。如果矩阵是百万级以下完全够用。如果想更省内存可以在每个line事件中实时更新count。核心逻辑是设置一个已处理数量计数器第一个token对之前先读行列之后每个token都判断是否为1。那样代码会更绕需要维护较多状态。笔试中数据量通常可控我建议先用更易读的缓冲版本能跑过就行。4.4 Cscanf就是最简洁的选择#include stdio.h int main(void) { int n, m; if (scanf(%d%d, n, m) ! 2) { puts(0); return 0; } long long count 0; long long total (long long)n * m; for (long long i 0; i total; i) { int v; if (scanf(%d, v) ! 1) break; if (v ! 1) count; } printf(%lld\n, count); return 0; }C语言的scanf天然跳过空白字符所以不需要额外处理空格和换行。只要输入按%d格式排列代码就很短。唯一要注意的是读取失败时的退出逻辑。如果实际行数不足n*m读取会失败循环用break退出count可能偏小但正规题目不会故意给不完整输入。还有人会用fgetsstrtok读一行解析一行但这在C笔试里通常没必要scanf已经够用了。4.5 输入解析最容易踩的坑我在帮人review代码时见过几个典型的翻车点第一Java里用readLine()直接读第一行如果文件末尾有换行可能漏读空字符串所以我在循环里加了if (line null || line.trim().isEmpty()) continue;。第二JS里忘记处理空行。空行经过.trim().split(/\s/)后会产生[]再Number()会得到0从而多算一个错误值。必须在split前过滤空token。第三Python里只按行读取假设每一行数据个数正好等于列数m。如果某行末尾多了个空格split也能处理但如果某行数据跨行比如题目把所有矩阵元素放在一行而代码仍然按照“每行m个”来读就会读错列。用sys.stdin.read()能规避这个问题。第四C语言的n * m直接写成int在大整数时溢出。前面代码里我用(long long)n * m解决了。4.6 本地自测方法写完别急着提交。先在本地准备一个文本文件比如test.txt内容就是3 3 1 2 1 4 1 5 1 1 9然后把程序的输出和预期结果4比对一遍。再换几个边界样例0 0预期输出0。2 2 1 1 1 1预期输出0。2 2 2 3 4 5预期输出4。这些样例覆盖了空矩阵和全1矩阵能暴露大多数粗心错误。线上笔试时间紧张但花一分钟跑三个样例比提交失败再改划算得多。5. 面试官还能怎么追问从计数到泛化5.1 把目标值抽成参数这个题最常见的变形是“返回矩阵中不等于target的元素个数”。只要把1换成参数代码就变成一道通用题def count_not_target(matrix, target1): if not matrix: return 0 count 0 for row in matrix: for value in row: if value ! target: count 1 return countJava和C同理。笔试里可能会改成“非0元素”“非-1元素”改法一样。提前把所有解题模板统一成“不等于某个值”现场就能少改一个变量。5.2 浮点数判断要加精度容差如果矩阵是浮点数直接判断value ! 1.0会有精度问题。一个值可能是0.9999999999并不等于1但在实际语义里你认为它接近1。这种情况下需要定义精度def is_one(value, eps1e-9): return abs(value - 1.0) eps这不是这道题的常见要求但面试官可能会顺势考你对浮点数精度和IEEE 754表示的理解。能答出“用绝对差阈值”就能过关能进一步说明“大数值应该用相对差”就更完整。5.3 如果矩阵太大或按稀疏格式存储假设矩阵是100000x100000但只存了很少的非零元素并且以CSR压缩稀疏行格式给出。这时候再用二维双重循环就不合适了因为存储层根本没有完整的二维结构。在CSR格式下你只拥有非零元素列表。要统计原始矩阵中非1元素个数可以这样拆解所有没有存储在非零数组里的位置默认值为0它们全部是非1元素。存储在非零数组里的元素如果值不等于1也要计入。存储在非零数组里但值等于1的元素不计入。因此结果可以写成总元素个数 - 非零元素个数 非零元素中等于1的个数。这里需要nnz和总行列数。能讲清这个思路说明你不只是会遍历二维数组而是真的理解数据结构的含义。5.4 统计每个值的出现次数可能更有用还有一个变形是假如后面还要问“非2的元素有几个”“非7的元素有几个”每次遍历一遍就太亏了。可以先用一次遍历建立计数表from collections import Counter def build_counter(matrix): counter Counter() for row in matrix: counter.update(row) return counter def count_not_target(counter, total, target): return total - counter.get(target, 0)这样只需要一次O(mn)扫描之后每次查询都是O(1)。空间额外消耗是不同值的数量。这个优化和“总数减1的个数”是同一个思想只是在数据结构上更进一步算是一个很自然的拓展。我在笔试里见过好几次这类“等不等于指定值”的题最后想以个人经验提醒你两点一是先花十秒确认输入格式是按行给、还是整个数字串给是空格分隔还是逗号分隔这决定了你在输入解析上采用什么策略二是别急着提交先自测空矩阵、全1矩阵和全非1矩阵三组样例。做到这两点这一题基本就能稳定拿满。
返回列表