news 2026/10/5 5:20:49

二维数组子数组筛选全解析:C#与C语言实现及边界避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
二维数组子数组筛选全解析:C#与C语言实现及边界避坑

上机笔试碰到“取出指定条件二维数组的子数组”这种题,我的第一反应不是高兴它简单,而是警铃大作。原因很简单:这类题看起来三五句话就能写完,实际挂分点比很多中等难度的算法题都多。它正好卡在“你懂不懂二维数组本质”和“你会不会把模糊需求翻译成严谨代码”之间,是笔试里最典型的“看似送分、实则筛人”的题目。

这篇文章就把这道题彻底拆开揉碎,从条件类型、接口设计、C#与C语言两套实现,到边界条件和变体题,一次性讲透。无论你是正在准备校招机考的应届生,还是工作中需要处理二维数组筛选的开发者,这篇文章都能让你少踩几个坑。

1. 这道题背后真正想考察的三种能力

先说结论:面试官出这道题,不是为了考你会不会用for循环,而是想通过一个很小的切口,观察你面对“二维数据 + 条件筛选”时的抽象能力、边界意识和代码组织能力。

1.1 考察点一:索引坐标系和维度抽象

二维数组的核心是坐标系。array[i, j]里i是行索引、j是列索引,这个大家都会写,但一放到“子数组”这个概念里就开始乱了。

什么叫“子数组”?很多人的第一反应是“取矩阵的一部分”,也就是从第rowStart行到第rowEnd行、第colStart列到第colEnd列的子矩阵。也有人理解成“按条件挑出若干行”,还有人理解成“按条件挑出若干元素组成新数组”。这三种理解,对应完全不同的代码结构。

笔试里最常考的是前两种:按条件筛选行,或者按范围截取子矩阵。但题目原文往往只给一句“取出指定条件二维数组的子数组”,这时候你必须在脑子里立刻建立一个二维坐标模型,明确“行是外层、列是内层”,后续所有代码都建立在这个坐标系上。我见过不少同学,代码写到一半,行列索引搞反,或者把行索引当列索引用,最后输出全错。问题根源就是没有在动手前把坐标系画清楚。

1.2 考察点二:把“条件”参数化

“指定条件”这四个字是整道题的题眼。条件是什么?是值大于某个阈值?是行内元素全部满足某个规则?是行内某一列的值落在某个区间?还是行内所有元素的和超过某个数?

如果你在函数里写死if (row[2] > 10) { ... },那这道题你只能得基础分。稍微有点区分度的评分标准,都会看你能不能把“条件”从函数里抽出来,作为一个参数传进去。这就是策略模式在算法题里的降维应用。

能写出FilterSubArrays(int[,] matrix, Predicate<int[]> condition)这种签名的人,和写死条件的人,在面试官眼里是两个档次的候选人。前者展示的是可扩展性思维,后者展示的是“我能跑通这一个用例”。后面我会给出具体代码,你一看就明白。

1.3 这种题为什么比 LeetCode 难题更容易翻车

LeetCode 难题你一看就知道自己不会,反而会认真读题、仔细分析。这种“简单题”才是最危险的,因为它给你的安全感是虚假的。

我见过真实的笔试统计,像“二维数组的子数组”这类题目,通过率往往不到 50%。挂掉的原因五花八门:有的人没处理空数组直接越界;有的人返回了引用导致后续修改污染原始数据;有的人没有考虑一行数据恰好满足/不满足条件的边界;还有的人压根没读懂“子数组”到底按行还是按列取。

所以别小看这道题。把它当作一次完整的编码流程来对待:拆解需求 → 设计接口 → 实现 → 自测边界 → 复盘扩展。本文就是按这个流程组织的。

2. 动手前先拆题:四种常见的“指定条件”

题面只给了“指定条件”四个字,具体条件要靠你从上下文里猜。我总结这些年笔试里见到的高频条件,基本逃不出下面四类。

2.1 条件类型一:按行数据本身筛选

这是最直观的写法:二维数组的每一行是一个单位,如果这一行里“存在某个元素大于阈值”或者“所有元素都大于阈值”,就把这一行作为子数组加入结果。

对应到代码上的关键点是:你需要先决定“条件作用在行上”,然后设计遍历顺序。外层循环走行,内层循环走列,遇到满足条件的行就复制出来。谁在外层、谁在内层,看似是小细节,其实决定了你的代码是否清晰。

另一个容易忽略的问题是“复制”。如果直接把原数组的某一行返回出去,在 C# 里int[,]转行数组本来就要复制;但在 C 语言里返回int*就要小心。后面我会专门讲。

2.2 条件类型二:按位置范围截取子矩阵

这种变体更接近“切片”语义,给定rowStart, rowEnd, colStart, colEnd,取出矩阵中的一个矩形区域。它在上机题里出现频率很高,因为实现思路清晰:外层从rowStart循环到rowEnd,内层从colStart循环到colEnd,把值逐格复制到新矩阵。

坑在哪里?坑在于题目给的坐标是“闭区间”还是“半开区间”。有的题写[rowStart, rowEnd],有的写[rowStart, rowEnd),很多人的代码在第二个用例直接翻车,就是因为没确认区间语义。如果题目没说清楚,建议在注释里写明你的理解,然后按半开区间实现,因为 C# 的Array.Copy、Python 的切片都是左闭右开,这个约定更符合通用习惯。

2.3 条件类型三:按行聚合结果筛选

比如“取出所有行和大于 100 的行”“取出所有行平均数大于 50 的行”。这类条件要求你先对行内元素做一次聚合计算,再用聚合结果做判断。

实现上有一个优化点:如果题目要求反复筛选,可以先把每行的聚合值算出来存到一个一维数组里,避免每次筛选都重新遍历整行。笔试里虽然数据量通常不大,但这体现的是你“有没有考虑重复计算”的意识,是加分项。

2.4 条件类型四:组合条件与数组越界的边界

真正拉开差距的是组合条件。比如“取第三列大于 0 且行和大于 100 的行”,或者“取某个元素值落在一个动态变化区间内的连续区域”。

这种题不仅考你的条件抽象能力,还考你对数组越界的敏感度。比如你判断row[j] > threshold时,有没有先确认j确实小于列数?如果传入的数组本身是“不规则二维数组”(比如交错数组int[][]),每一行的列数可能不同,你是不是还假设了所有行等长?

我见过一个真实案例,候选人写的代码在规则二维数组上全部通过,但测试用例里有一行的列数比其它行小,结果直接 IndexOutOfRangeException,前功尽弃。所以动手拆题时,一定要确认:这个二维数组是规则的int[,]还是不规则的int[][]。

3. C#版本:接口设计、完整代码与复杂度分析

C# 写这类题有天然优势,Predicate<T>委托、List<T>动态扩容,能让你把注意力放在逻辑上。但优势用不好也会变成劣势——很多人只会写死条件,不会抽象。

3.1 C#实现:用 Predicate<int[]> 筛选行

先给出按行筛选的完整实现。核心思想是:把“条件”定义为一个接收int[]返回bool的委托,函数只负责遍历和收集。

using System; using System.Collections.Generic; public static class MatrixHelper { /// <summary> /// 按条件筛选二维数组中的行。 /// </summary> /// <param name="matrix">源二维数组</param> /// <param name="condition">行条件:接收一行数据,返回是否保留</param> /// <returns>满足条件的行组成的 List</returns> public static List<int[]> FilterRows(int[,] matrix, Predicate<int[]> condition) { var result = new List<int[]>(); if (matrix == null || condition == null) return result; int rowCount = matrix.GetLength(0); int colCount = matrix.GetLength(1); for (int i = 0; i < rowCount; i++) { int[] row = new int[colCount]; for (int j = 0; j < colCount; j++) { row[j] = matrix[i, j]; } if (condition(row)) { result.Add(row); } } return result; } }

调用方式也很直观:

int[,] data = new int[,] { { 1, 5, 9 }, { 12, 3, 4 }, { 7, 8, 15 }, { 20, 1, 2 } }; // 取出“至少有一个元素大于10”的行 var rows = MatrixHelper.FilterRows(data, row => { foreach (int val in row) { if (val > 10) return true; } return false; }); foreach (var row in rows) { Console.WriteLine(string.Join(", ", row)); } // 输出: // 12, 3, 4 // 7, 8, 15 // 20, 1, 2

有没有发现,把这个设计里的List<int[]>换成List<List<int>>,再把int[,]换成int[][],代码几乎不用改?这就是抽象的好处:条件变了、数据结构微调了,核心逻辑纹丝不动。

3.2 C#实现:截取矩形子矩阵

如果是按坐标截取矩形区域,上面的按行筛选就不够用了。你需要写一个接收行范围、列范围的函数。

public static int[,] GetSubMatrix(int[,] matrix, int rowStart, int rowEnd, int colStart, int colEnd) { if (matrix == null) throw new ArgumentNullException(nameof(matrix)); int rowCount = matrix.GetLength(0); int colCount = matrix.GetLength(1); // 边界检查:防止索引越界 if (rowStart < 0 || rowStart >= rowCount || rowEnd < rowStart || rowEnd >= rowCount || colStart < 0 || colStart >= colCount || colEnd < colStart || colEnd >= colCount) { throw new ArgumentOutOfRangeException("取值范围超出矩阵边界"); } int subRowCount = rowEnd - rowStart + 1; int subColCount = colEnd - colStart + 1; int[,] result = new int[subRowCount, subColCount]; for (int i = 0; i < subRowCount; i++) { for (int j = 0; j < subColCount; j++) { result[i, j] = matrix[rowStart + i, colStart + j]; } } return result; }

这里我特意写了显式边界检查。笔试时这道检查语句可能就是 5 分和满分的区别。因为测试用例里大概率会出现rowStart = 0, rowEnd = 0(单行矩阵)或者colStart = colEnd(单列矩阵)的极端情况,没有边界检查,你的代码会在最后一组用例上挂掉。

3.3 调用示例与复杂度分析

两种实现的时间复杂度都很容易分析:

操作时间复杂度额外空间
按条件筛选行O(rowCount * colCount)O(k * colCount),k 为满足条件的行数
截取矩形子矩阵O((rowEnd - rowStart + 1) * (colEnd - colStart + 1))O(子矩阵元素个数)

如果笔试数据量在10^3 * 10^3量级,这个复杂度完全够用。如果列数特别大,还可以在条件判断前先把矩阵“扁平化”为交错数组,减少索引计算的开销。但笔试阶段不建议为了这种优化牺牲可读性,先保证逻辑正确,再考虑优化。

4. C语言版本:二维数组传参的正确姿势与动态返回

C 语言版本才是这道题的“重头戏”。因为 C 语言里二维数组本身就有很多坑,尤其是函数传参,一个不留神就编译不过或者内存泄漏。下面我先把最核心的坑讲清楚,再给一份能直接跑通的完整实现。

4.1 二维数组传参为什么必须给列数

先看一个很多人踩过的坑。你写:

void process(int arr[][], int rows, int cols);

编译直接报错。原因其实不复杂:二维数组在内存里是连续存放的,arr[i][j]等价于*(arr + i * cols + j)。编译器必须知道cols才能计算出每一行的起始地址,否则它根本无法定位arr[i][j]的位置。

所以数组形参里,第一维可以省略,第二维必须写死,或者用 C99 的可变长数组:

// 方式一:固定列数 void process(int arr[][4], int rows); // 方式二:C99 可变长数组(调用时 cols 必须在前面) void process(int rows, int cols, int arr[rows][cols]);

方式二写起来灵活,但需要注意参数顺序:rows和cols必须出现在数组参数之前。很多老旧的笔试环境用的是 C89 标准,上机时要先确认编译器是否支持 VLA,如果不支持,就老老实实用宏定义固定列数,或者像我下面那样用int**手动构建“指针数组”来模拟二维数组。

4.2 函数指针实现条件回调

C语言没有委托,实现“把条件参数化”要用函数指针。

#include <stdio.h> #include <stdlib.h> #include <stdbool.h> // 条件函数类型:接收一行数据,返回是否保留 typedef bool (*RowCondition)(int *row, int cols); // 示例条件:行内存在元素 > threshold typedef struct { int threshold; } ThresholdConditionContext; bool AnyGreaterThan(int *row, int cols, void *context) { ThresholdConditionContext *ctx = (ThresholdConditionContext *)context; for (int j = 0; j < cols; j++) { if (row[j] > ctx->threshold) return true; } return false; }

这里我把context指针也传进去了,这样就解决了“函数指针无法携带额外参数”的问题。C语言写条件筛选,这一招是标配,否则每次要改阈值就得重新写一个函数,代码会爆炸。

4.3 输出结构设计与内存管理

C语言里函数不能直接返回动态二维数组,最常用的方案是返回一个二级指针,同时通过一个出参返回筛选出的行数。

// 筛选结果用结构体封装,省得参数太多容易记混 typedef struct { int **rows; int rowCount; int colCount; } MatrixResult; MatrixResult FilterRows(int rows, int cols, int matrix[rows][cols], bool (*condition)(int *, int, void *), void *context) { MatrixResult result = { NULL, 0, cols }; // 第一遍遍历,先数出有多少行满足条件 int count = 0; for (int i = 0; i < rows; i++) { if (condition(matrix[i], cols, context)) { count++; } } result.rowCount = count; if (count == 0) return result; // 分配二维结果空间 result.rows = (int **)malloc(sizeof(int *) * count); int idx = 0; for (int i = 0; i < rows; i++) { if (condition(matrix[i], cols, context)) { result.rows[idx] = (int *)malloc(sizeof(int) * cols); for (int j = 0; j < cols; j++) { result.rows[idx][j] = matrix[i][j]; } idx++; } } return result; }

调用方记得释放内存:

void FreeMatrixResult(MatrixResult *result) { if (result->rows == NULL) return; for (int i = 0; i < result->rowCount; i++) { free(result->rows[i]); } free(result->rows); result->rows = NULL; result->rowCount = 0; }

为什么要先数一遍再分配?因为malloc是按精确数量分配,如果你边筛边分配,就需要realloc或者维护一个变长列表,逻辑复杂还容易出错。两遍遍历虽然在时间上多了一遍 O(rows * cols),但换来的内存利用率是 100%,对这种笔试场景非常划算。

5. 一测就崩的隐藏坑:边界条件清单

代码能跑通示例用例不算完,上机笔试的判题系统会用大量边界用例来考验你的代码。以下这些坑,我在真实考前辅导里反复强调。

5.1 空数组与空条件

第一个隐藏测试用例永远是空数组。C# 里int[,]可以用GetLength(0)拿到 0,然后你的循环压根不进去,返回空列表,这没问题。怕的是你忘了判空,直接访问matrix[0, 0]抛异常。

C语言里更危险:如果你处理的是int**类型,传进来的可能是NULL。我在第 4 节的实现里没有显示判空,实际笔试时必须加上:

MatrixResult result = { NULL, 0, cols }; if (matrix == NULL || condition == NULL || rows <= 0 || cols <= 0) return result;

这些防御性代码看起来多,但它是区分“能用”和“能上生产”的关键。笔试时判空不会扣分,但缺失判空会直接导致运行时错误,一票否决。

5.2 行内索引越界与条件边界

条件里经常会有“取第k列做判断”这种逻辑。你写row[k] > 0之前,先问自己:k一定小于cols吗?如果题目里的k是从外部传入的参数,而矩阵列数恰好小于k,那你的代码会直接越界。

还有一个常见的边界逻辑错误是“大于”和“大于等于”的混用。条件写的是“大于 0”还是“不小于 0”?“行内所有元素大于 0”和“行内存在元素大于 0”是两套完全不同的逻辑。前者初始化为true,遇到<=0就标false;后者初始化为false,遇到>0就标true。千万别搞反。

5.3 笔试环境里的输入输出陷阱

很多在线笔试平台用的是“标准输入 + 标准输出”的判题方式。这时候单纯的函数实现是不够的,你还需要写出正确的Main函数去读取输入、构造二维数组、调用函数、打印结果。

C# 的输入解析常用这一套:

// 输入示例: // 3 3 // 1 2 3 // 4 5 6 // 7 8 9 // 10 // 表示 3行3列,按条件取,阈值 10 string[] firstLine = Console.ReadLine().Split(); int rows = int.Parse(firstLine[0]); int cols = int.Parse(firstLine[1]); int[,] matrix = new int[rows, cols]; for (int i = 0; i < rows; i++) { string[] line = Console.ReadLine().Split(); for (int j = 0; j < cols; j++) { matrix[i, j] = int.Parse(line[j]); } } int threshold = int.Parse(Console.ReadLine()); var result = MatrixHelper.FilterRows(matrix, row => row[0] > threshold); foreach (var row in result) { Console.WriteLine(string.Join(" ", row)); }

这里最容易被忽略的坑是:Console.ReadLine()可能返回null(尤其在通过管道输入时)。严谨地处理需要在解析前判空,但笔试环境通常不会卡这么严。不过养成习惯总是好的。

6. 从这道题延伸出去:像素矩阵、最大子数组和与排序变体

“取出指定条件二维数组的子数组”绝不是一道孤立的题。把它吃透了,很多看似不相关的题目都能找到同一套底层抽象。

6.1 C#二维像素数组转图片:子数组的实际业务场景

我实际做图像处理的时候,经常遇到“从一张大图里抠出一块区域”的需求。位图在内存里本质上就是二维像素数组,Bitmap.GetPixel和SetPixel性能太差,专业的做法是先把整张图读成byte[,]像素数组,用数组切片的方式提取子区域,再转成新图片。

这就是我们第 3 节“截取矩形子矩阵”的工业版:

public static Bitmap CropBitmap(Bitmap source, Rectangle region) { int[,] pixels = new int[source.Height, source.Width]; // 用 LockBits 把整张图读进二维数组 // ... int[,] subPixels = MatrixHelper.GetSubMatrix(pixels, region.Top, region.Bottom - 1, region.Left, region.Right - 1); // 再把 subPixels 转成新的 Bitmap // ... }

所以说笔试题目不是空中楼阁。你在考场上写的GetSubMatrix,稍微封装一下就能用在实际项目里,处理截屏优化、图像裁剪、表格区域提取这些问题。

6.2 二维最大子数组和:从“筛选”到“搜索”

如果把“指定条件”从“某行满足某个布尔条件”升级为“行和最大的子矩阵”,就变成了经典题“二维最大子数组和”。

这个变体里,条件不再是简单的谓词,而是一个优化目标。常见解法是枚举上下行边界,用前缀和把二维问题降维成一维的“最大子数组和”,再用 Kadane 算法在 O(n) 内求解。

// 一维 Kadane:求数组中和最大的连续子数组 static int MaxSubArray(int[] nums) { int maxSoFar = nums[0]; int maxEndingHere = nums[0]; for (int i = 1; i < nums.Length; i++) { maxEndingHere = Math.Max(nums[i], maxEndingHere + nums[i]); maxSoFar = Math.Max(maxSoFar, maxEndingHere); } return maxSoFar; }

二维最大子数组和的整体复杂度是 O(rows^2 * cols)。为什么是 rows^2?因为你枚举了所有上下边界的组合,每种组合都要跑一次 Kadane。这个推导过程在笔试面试中非常加分,因为它展示了你对复杂度模型的把握。

6.3 二维数组排序:以列为键、以行为单位

另一个高频变体是“按某一列对二维数组的行排序”。C# 里最简洁的写法是用 LINQ:

int[][] data = /* 交错数组 */; var sorted = data.OrderBy(row => row[colIndex]).ToArray();

但笔试里有时候不让用 LINQ,或者题目要求你手写排序,这时候你要掌握Array.Sort的重载:

Array.Sort(data, (a, b) => a[colIndex].CompareTo(b[colIndex]));

注意这里data必须是int[][]而不是int[,],因为int[,]不是可索引集合的数组。这也是为什么我建议在动手写之前先确认清楚输入到底是一维数组的数组还是规则的二维数组。

C 语言里的对应方案是qsort,关键是传对的比较函数:

int compareByCol(const void *a, const void *b) { int *rowA = *(int **)a; int *rowB = *(int **)b; int colIndex = 1; // 按第1列排序,用全局变量或结构体传入 return rowA[colIndex] - rowB[colIndex]; }

6.4 还有哪些一眼看不出来同源的问题

我列一个可以用来自查的清单,看你能不能把这些题都归纳到“二维数组子数组”的框架下:

  • 找出二维矩阵中所有连续递增的路径(条件变成了“路径相邻”)
  • 找出每一行的最大最小值位置(条件变成了“聚合函数”)
  • 布尔矩阵里连通的 1 区域(条件变成了“连通性”)
  • 蛇形打印、螺旋打印矩阵(条件变成了“访问顺序”)

这些问题表面上是不同题型,本质都是在二维数组上定义“访问规则”和“保留规则”。你把最基础的筛选函数写明白之后,后面每一种变体都只是换条件和遍历顺序。

拿这道“取出指定条件二维数组的子数组”的题来说,我的实际备考建议是:不要只背代码,而是准备一张空白纸,把题目对应的坐标系画出来,把条件的严格定义用自然语言写一遍,把自己实现的接口签名默写一遍。这三步做完了,无论笔试平台用什么语言、题目怎么变,你都能快速迁移。

还有一个小技巧:真正上机前,先在本地把“空数组”“单行单列”“全部满足”“全部不满足”“索引越界”这五个用例跑通,再提交到判题系统。这一步能筛掉 80% 的隐藏坑,是我这几年刷题和带人最实用的经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 5:20:18

FMCW毫米波雷达原理详解:测距测速、FFT与参数设计

做雷达感知的人&#xff0c;应该都对 FMCW 这三个字母不陌生。FMCW&#xff08;Frequency Modulated Continuous Wave&#xff0c;线性调频连续波&#xff09;是毫米波雷达最常用的信号体制&#xff0c;从 24GHz 的工业测距模块到 77GHz 的车载前向雷达&#xff0c;核心原理其实…

作者头像 李华
网站建设 2026/10/5 5:20:00

大模型API统一管理实战:AI网关架构设计与落地

1. 多模型接入的乱局&#xff1a;为什么统一管理成了刚需我最早接触多模型接入是在两年前&#xff0c;当时团队同时用着三家厂商的对话模型、两家的向量模型&#xff0c;还有一套自部署的开源模型。每个项目组各自申请密钥、各自记账、各自写调用代码&#xff0c;结果就是月底对…

作者头像 李华
网站建设 2026/10/5 5:19:57

隔离内网AI Agent实战:从技术选型到性能调优

隔离内网这个词&#xff0c;做过企业软件的人一听就懂&#xff1a;机器、集群、系统从部署到运行都不能碰外部公网&#xff0c;所有模型权重、依赖包、容器镜像、配置文件都要人工带进去。今年我们交付了一个完全隔离内网的 AI Agent 项目&#xff0c;把“大模型智能体”搬进了…

作者头像 李华
网站建设 2026/10/5 5:19:33

红外热成像仪原理与应用:从黑体辐射到温度可视化

1. 从“看见温度”说起&#xff1a;红外热成像仪到底在解决什么问题不管是做设备巡检、电子研发&#xff0c;还是搞自动化产线&#xff0c;很多人第一次接触红外热成像仪&#xff0c;都是被它的“透视”能力吸引。黑灯瞎火的车间里&#xff0c;它能一眼看出哪块电路板在发热&am…

作者头像 李华
网站建设 2026/10/5 5:16:59

小波变换信号降噪原理与MATLAB仿真调参实战

先讲一个背景。设备状态监测和工业信号处理里&#xff0c;信号降噪是最基础、也最容易翻车的一步。以前我拿到数据&#xff0c;第一反应就是低通滤波或者做一次 FFT 再甩掉高频&#xff1b;后来发现这套方法对付平稳信号还行&#xff0c;一遇到非平稳信号&#xff0c;要么削了真…

作者头像 李华