
1. 一场笔试最常考的“基本功”先搞清楚题目在考什么前阵子有位读者发来一道上机笔试题原题只有一句话“取出指定条件二维数组的子数组”。我第一反应是——这题看起来短实际上把二维数组、子数组、条件筛选、边界处理这些基础考点全串起来了。面试官出这种题不是真想让你写多牛的算法而是想看你拿到一个模糊需求时能不能把它拆成清晰、可落地的逻辑。二维数组在笔试里几乎是标配考题形态从“输出某个元素”“按行列遍历”到“提取满足条件的子区域”层层递进。而“子数组”这个词又比“子矩阵”更口语化很多人在LeetCode刷惯了“最大子数组和”那种一维题突然碰到二维的提取题容易在“怎么定义行”“怎么定义列”“要不要包含边界”这些细节上翻车。这道题适合谁看一是准备校招、实习机试的同学尤其是目标岗位要写C#、Java或C的二是工作中需要处理像素矩阵、表格数据、地图数据频繁做二维区域裁剪的开发者。就算你暂时不面试把二维数组的条件提取逻辑梳理清楚对日常处理“筛选出某几行某几列”的数据需求也很有帮助。先说结论这道题的关键不是“会不会遍历二维数组”而是“能不能把不明确的‘指定条件’翻译成一行行严谨的判断代码”。下面我会从思路拆解、核心实现、参数计算到常见坑位完整过一遍。我用C#写主示例最后也补了C语言版本因为热词里很多人搜“C语言传二维数组要有个数字”笔试现场用C写这题时函数签名就够让人头疼。2. 拿到题目别急着写码先把“指定条件”翻译成程序逻辑2.1 拆解需求二维数组里取子数组本质是“行列范围裁剪”在纸上画一个3行4列的二维数组你很容易理解“子数组”就是其中某个矩形块。比如从第1行到第2行、第2列到第3列框出来的那部分。但上机题不会给你画图它只会用文字说“取出满足指定条件的子数组”于是你必须自己定义清楚条件是针对行的还是针对列的还是行列都要满足条件是比较数值大小还是匹配某个特征比如该行第一个元素大于10提取结果是新建一个二维数组还是只打印出来原始数组的行列数是否固定有没有空数组、单行单列的边界情况这些在题目里没写全恰恰是考察点。我的习惯是先把“条件”抽象成过滤器再把“取出”抽象成范围映射。过滤器负责判断“这一行/这一列/这个元素是否保留”范围映射负责把原数组的索引换算成新数组的索引。2.2 先想清楚提取策略逐行筛选而不是直接复制矩形网上搜索热词里出现“最大子数组和”“最长连续递增子序列”这类经典题说明很多人在准备动态规划类问题。但“取出指定条件二维数组的子数组”往往不是DP它更偏向数组切片。切片有两种常见策略策略一按行逐条判断符合条件就整行保留。适合“筛出所有行首元素大于某值的行”“筛出包含某个元素的行”这类条件。实现简单时间复杂度O(m×n)空间复杂度取决于结果大小。策略二先算出目标子数组的行列范围再一次拷贝。适合“取第r1到r2行、第c1到c2列”这种固定矩形。实现时先确认范围合法性再通过两层循环填充新数组。复杂度同样O(m×n)但逻辑更直观容易在纸上先写伪代码。笔试时我推荐“策略一为主策略二为辅”。因为大多数“指定条件”是模糊的逐行筛选更灵活还能顺便处理“结果行数不固定”的情况。如果你提前知道自己只是取固定矩形那就用策略二少绕弯。2.3 为什么很多人会在这里卡住把“子数组”想复杂了我见过有人一看到“二维数组的子数组”就想着要用递归、回溯或者滑动窗口去“找”。这其实是把题目想复杂了。“取出指定条件二维数组的子数组”这句话里的“指定条件”是前提不是让你去搜索最优解。如果题目真的让你“找出满足某条件的最大子数组”那是另一类题但这里动词是“取出”更像是筛选并复制。一个类比你有一本Excel表格二维数组现在要“取出所有满足‘销售额大于10000’的行并且只要产品名称和日期两列子数组”。这就是典型的筛选裁剪。没有动态规划没有双指针就是老老实实遍历、判断、拷贝。3. 核心细节解析从条件抽象到索引映射每一步都有讲究3.1 条件怎么抽象成可以替换的“钩子”上机题最大的变数是“条件”二字可能千奇百怪。今天考“取所有行首元素大于10的行”明天可能考“取所有包含负数的列”后天可能考“取行号和列号之和为偶数的矩阵块”。你不可能为每种条件都写一套循环所以最好把“条件”抽象成接口或方法。在C#里我通常写一个委托Funcint[], bool rowCondition row row[0] 10;意思是给定一行数组返回true表示保留这行。只要条件变改这一行lambda就行主逻辑不动。同样的思路可以套到列筛选、元素筛选上。笔试中你不需要真的定义接口但写一个内部方法比硬编码条件要好得多因为面试官可能会追问“如果条件变成XX你怎么改”。到时你只要回答“换一个过滤函数”就够了。3.2 行列分离先确定要哪几行再确定要哪几列“二维数组的子数组”有一个隐含步骤你可能不只是整行取有时候要的只是某些列。比如原始数组是4行5列但最终只要第0列、第2列和第3列。这种需求一旦出现索引映射就复杂一点。我的做法是分两步先用一个List 收集满足条件的行索引。再用一个List 收集需要保留的列索引如果列也有条件。然后新数组的大小是int newRowCount validRows.Count; int newColCount validCols.Count; int[,] result new int[newRowCount, newColCount]; for (int i 0; i newRowCount; i) { for (int j 0; j newColCount; j) { result[i, j] source[validRows[i], validCols[j]]; } }这段代码的巧妙之处在于无论是跳行还是跳列只要索引集合是连续的整数列表映射关系就完全一致。你不需要一个“合法位置矩阵”只需要两个一维索引表。3.3 参数计算目标数组的行列数怎么来很多人栽在目标数组的尺寸上。新数组的尺寸不是“原数组尺寸”而是筛选后的尺寸。如果条件筛选出3行你却new了一个4行的数组后面赋值时就会越界如果少new了一行又会漏数据。计算尺寸的边界情况如果一行都不满足条件result应该是0行还是保持原样规范做法是返回一个0x0的空数组或者在函数注释里明确约定。如果某些列无效但行有效新数组列数要重新计算。如果原数组是0行或0列直接返回原数组引用避免无意义的逻辑。在笔试中我习惯先做防御性判断if (source null || source.GetLength(0) 0 || source.GetLength(1) 0) return source;这一行能挡掉很多边界用例也能向面试官展示你考虑问题周全。3.4 哪个是行、哪个是列先跟面试官确认还是自己定这道题有个经典陷阱二维数组在C#里用arr[row, col]表示但有些材料里二维数组写作int[][]交错数组这时候arr.Length是行数arr[0].Length是列数。笔试现场最容易搞混的就是GetLength(0)和GetLength(1)到底谁是行谁是列。我的经验是写代码前先用注释明确// source[row, col] // row GetLength(0), col GetLength(1)一句话的事能避免后面赋值时把行和列搞反。许多考生不是不会写逻辑而是写到一半分不清i和j。注释是给自己看的也是给面试官看的。4. 实操过程与核心环节实现从C#到C语言一次写透4.1 C#版本按行条件筛选并提取指定列下面是一个能直接跑的C#控制台示例。假设需求是从4行5列的数组中取出“行首元素大于5”的所有行并且只要第0列和第2列。using System; using System.Collections.Generic; class Program { static void Main() { int[,] source new int[,] { { 3, 1, 8, 2, 9 }, { 7, 4, 1, 0, 6 }, { 9, 3, 5, 7, 2 }, { 2, 8, 4, 1, 3 } }; int[,] result ExtractSubArray(source, row row[0] 5, new int[] { 0, 2 }); PrintArray(result); } static int[,] ExtractSubArray(int[,] source, Funcint[], bool rowCondition, int[] targetCols) { if (source null || source.GetLength(0) 0 || source.GetLength(1) 0) return source; int rowCount source.GetLength(0); int colCount source.GetLength(1); Listint validRows new Listint(); for (int r 0; r rowCount; r) { int[] row new int[colCount]; for (int c 0; c colCount; c) { row[c] source[r, c]; } if (rowCondition(row)) { validRows.Add(r); } } int[] cols new int[targetCols.Length]; for (int i 0; i targetCols.Length; i) { if (targetCols[i] 0 || targetCols[i] colCount) throw new ArgumentOutOfRangeException(nameof(targetCols)); cols[i] targetCols[i]; } int[,] result new int[validRows.Count, cols.Length]; for (int i 0; i validRows.Count; i) { for (int j 0; j cols.Length; j) { result[i, j] source[validRows[i], cols[j]]; } } return result; } static void PrintArray(int[,] arr) { int rows arr.GetLength(0); int cols arr.GetLength(1); for (int r 0; r rows; r) { for (int c 0; c cols; c) { Console.Write(arr[r, c] \t); } Console.WriteLine(); } } }运行结果是7 1 9 5第0行首元素是3不大于5剔除第1行首元素7保留第2行首元素9保留第3行首元素2剔除。然后只取第0列和第2列所以7那行取7和19那行取9和5。这段代码有两点值得学习一是把行包装成临时一维数组传给lambda这样条件写法很直观二是目标列做了合法性校验避免用户在参数里传了个越界列号。4.2 固定矩形裁剪另一种高频子数组提取笔试另一种常见问法是“取从第x1行到x2行、第y1列到y2列的子数组”。比如取source第1行到第2行、第2列到第3列。直接看代码static int[,] Crop(int[,] source, int r1, int r2, int c1, int c2) { int rowCount r2 - r1 1; int colCount c2 - c1 1; int[,] result new int[rowCount, colCount]; for (int i 0; i rowCount; i) { for (int j 0; j colCount; j) { result[i, j] source[r1 i, c1 j]; } } return result; }这段代码就是典型的偏移量映射目标第i行对应原数组第r1i行目标第j列对应原数组第c1j列。如果你理解了这个映射所有子数组提取题的核心你都掌握了。实际调用时可以先检查r10、r2原行数、c10、c2原列数否则抛出异常。4.3 C语言版本传参时的“数字”到底指什么热词里有一条“c语言传参传二维数组要有个数字”这正是C语言笔试的经典痛点。在C语言里函数参数如果是二维数组必须指定第二维的大小因为编译器要根据第二维来计算地址偏移。比如void extract(int arr[][5], int rows, int target[][2], int *resultRows);这里arr[][5]中的5是列数不能省略。如果省略编译器不知道arr[1][0]应该跳多少个元素。很多同学第一次写C版本时喜欢写int arr[][]编译直接报错原因就在这里。解决方案有两种一是固定列数二是把二维数组降维成一维数组手动计算偏移量。给一个C语言按行筛选的完整参考#include stdio.h #define COLS 5 int extractByFirst(int arr[][COLS], int rows, int result[][2], int *resultRows) { int count 0; for (int i 0; i rows; i) { if (arr[i][0] 5) { result[count][0] arr[i][0]; result[count][1] arr[i][2]; count; } } *resultRows count; return 0; } int main() { int source[4][COLS] { {3, 1, 8, 2, 9}, {7, 4, 1, 0, 6}, {9, 3, 5, 7, 2}, {2, 8, 4, 1, 3} }; int result[4][2]; int resultRows 0; extractByFirst(source, 4, result, resultRows); for (int i 0; i resultRows; i) { printf(%d %d\n, result[i][0], result[i][1]); } return 0; }这个版本里result列数固定为2因为题目只需要第0列和第2列。如果目标列数不确定C语言处理起来就会麻烦很多这也是为什么笔试遇到复杂条件时我一般优先用C#或Java而不是C。4.4 针对“二维字符数组”的变体条件筛选同样适用热词里出现“二维字符数组”我再补充一种场景。如果source不是int[,]而是char[,]条件可能是“取出所有含有字母a的行”。做法完全一样只是遍历时比较的是char类型static Listchar[] ExtractCharRows(char[,] source) { Listchar[] result new Listchar[](); int rows source.GetLength(0); int cols source.GetLength(1); for (int r 0; r rows; r) { bool hasA false; for (int c 0; c cols; c) { if (source[r, c] a) { hasA true; break; } } if (hasA) { char[] row new char[cols]; for (int c 0; c cols; c) { row[c] source[r, c]; } result.Add(row); } } return result; }注意这里返回的是Listchar[]而不是char[,]因为筛选后行数不固定。这也是一种常见做法用List存储动态结果而不是先预估一个固定大小的二维数组。5. 常见问题与排查技巧实录上机现场最容易踩的5个坑5.1 越界异常new了一个不存在的行或列这是最常见的报错。原因通常是计算新数组大小时用了原数组的行列数但筛选后行数变少。排查技巧在new结果数组之前先输出validRows.Count和targetCols.Length对比是否越界。或者用try-catch包装赋值循环一旦越界马上定位到具体i、j。5.2 GetLength(0)和GetLength(1)反了C#的二维数组GetLength(0)是行数GetLength(1)是列数。有些人习惯性认为“第一个就是列”然后整个循环全反了。解决办法是开头写一行注释。C语言里则要清楚“定义时[行][列]访问时arr[i][j]i是行j是列”。5.3 空数组和null没有处理如果source是null或0行0列很多代码会直接抛异常。笔试用例可能专门放一个空数组。防御性代码就三行if (source null) throw new ArgumentNullException(nameof(source)); if (source.GetLength(0) 0 || source.GetLength(1) 0) return source;5.4 目标列号不固定用了固定列数的结果数组比如需求是“保留满足条件的行但列由另一个条件动态决定”。如果你直接new int[validRows.Count, 3]而实际列只有2列后续赋值就会出现空洞。解决思路先收集所有要保留的列索引再通过索引映射填充不要假设列数总是3或总是2。5.5 C语言里二维数组传参漏写列数如果你选择用C语言函数声明里第二维数字必须写否则编译过不了。如果不确定列数可以内部用一维指针模拟二维访问int GetElement(int *arr, int colCount, int r, int c) { return arr[r * colCount c]; }这样传参时就只需要传一维数组和列数绕开二维数组传参的语法限制。笔试中这是个非常实用的技巧建议提前准备好。5.6 一个速查建议先画表再写循环我的个人经验是在上机环境里先别急着写代码。花30秒在纸上或注释里画一个3行4列的表格标出要提取的区域、行索引、列索引然后把对应关系写清楚。这一步看着浪费时间实际上能减少一半的调试时间。很多人代码改来改去不是逻辑不会而是最初就没把行列对应关系想明白。6. 由这道题延伸出去的几个变体学会底层逻辑更容易举一反三这道“取出指定条件二维数组的子数组”还可以变形成很多形态。热词里提到的“二维数组排序sort”“二维像素数组转换成图片”也跟它相关。6.1 变体一先排序再取子数组有些题目会要求先对二维数组按某列排序再取出前N行作为子数组。这时候就不能只筛选而要引入排序。C#里可以用Listint[]存储行数据然后按指定列排序再Take前N行转换回二维数组。核心代码Listint[] rows new Listint[](); for (int i 0; i rowCount; i) { int[] row new int[colCount]; for (int j 0; j colCount; j) row[j] source[i, j]; rows.Add(row); } rows.Sort((a, b) a[1].CompareTo(b[1])); // 按第1列排序这种方式比直接对二维数组排序更清晰也自然衔接了“取出前K行”的需求。6.2 变体二二维像素数组转图片热词里有“c#二维像素数组转换成图片”。如果你能搞定二维数组的子数组提取那转换图片其实就是把筛选后的数组逐像素写入BitmapBitmap bmp new Bitmap(cols, rows); for (int i 0; i rows; i) { for (int j 0; j cols; j) { int gray pixelArray[i, j]; bmp.SetPixel(j, i, Color.FromArgb(gray, gray, gray)); } } bmp.Save(output.png);这类题把“取出子数组”作为第一步再转图片作为第二步环环相扣。所以把数组提取逻辑练好是在给更复杂的应用场景打底。6.3 变体三最长连续递增子序列的二维版思路热词里也提到“找出最长连续递增子序列的长度”这跟“取出子数组”不冲突。二维版本可以要求“找出按行递增的最长子数组”这时候你的筛选条件就不再是静态的而是依赖前一行结果。它的底层仍然需要“取出指定条件子数组”的骨架只不过条件从简单比较变成了动态规划判断。我把这个关联写在这里是想提醒大家上机题很少孤立出“取出指定条件二维数组的子数组”它往往是作为某道大题的中间步骤。如果你能在白板上快速写出一个健壮的提取函数后面接排序、接统计、接转换都会顺很多。7. 写在最后这道题背后真正考验的三件事我自己刷题和带人模拟面试时发现这道题最考验的不是算法而是三件事。第一能不能把模糊需求拆成行、列、条件三个维度第二能不能处理好动态尺寸下的数组分配第三能不能在最基础的遍历赋值中不搞反索引。只要这三件事过关不管题目怎么变你都有底气。还有一个我经常给准备机试的朋友的建议平时练习时不要只在LeetCode上刷“现成题目”多尝试把一个简单题目自己改条件、改输出形式。比如把“取出所有行首元素大于5的行”改成“取出所有包含数字7并且行数编号是偶数的区域”你会发现同一个函数只要换lambda几乎不用动主逻辑。这种灵活应变的能力才是上机笔试真正拉开差距的地方。最后再分享一个小技巧机试时如果允许用在线编译器先跑一个最小用例验证你的提取逻辑比如2x2数组配一个最简单的条件。确认输出正确后再上完整用例。这比一次性写完直接提交要稳妥太多。