背景: 最近在复习数据结构和算法,顺带刷刷题,虽然很长时间不刷题了但还是原来熟悉的味道,每一次重学都是加深了上一次的理解。本次我们看一下如何将C语言的二维数组进行函数传参,C语言实现。
其实这个比较简单,但复习了一下,有几个地方需要记录下来,以防忘记。
问题1: 假设你有一个二维数组,需要将这个二维数组传入某个函数进行计算,你如何传入,传入的方式方法有哪些?
肯定是需要传输指针,而不是 复制这个二维数组然后传值进去,这样对内存消耗较大。
关于二维数组的一些基本定义,可以参考之前的文章我记不住的那些编程语言的语法(数组)-1
第一种: 将二维数组的指针 转换为一维数组的指针,然后再将元素总数量传值即可
void average(float* p, int n){
float* end;
float sum=0,aver;
end = p+n-1;
for(;p<=end;p++){
sum+= (*p);
}
aver = sum/n;
printf("average=%5.2f\n",aver);
}
float score[3][4] = {65,67,70,60,80,87,90,81,90,99,100,98};
// *score 等价于 *(score+0) 也就是 *(score+0) + 0
// *score 就是 &score[0][0]
average(*score,12);
// 也就是说,等价于
average(&score[0][0],12);
第二种: 将二维数组的指针 转换为一维数组的指针,然后再将矩阵的行数和列数传值即可
void average2(float* p, int m, int n){
float sum=0,aver;
for(int i =0;i<m;i++){
for(int j=0;j<n;j++){
sum+= *(p+i*n+j);
}
}
aver = sum/(m*n);
printf("average=%5.2f\n",aver);
}
float score[3][4] = {65,67,70,60,80,87,90,81,90,99,100,98};
// *score 这个传值和 第一种是一个道理
average2(*score,3,4);
// 也就是说,等价于
average2(&score[0][0],3,4);
第三种: 将二维数组的指针作为 行指针进行传参,然后获取某行的数据
// 这里的 float (*p)[4] 等价于 float p[][4]
// 代表的是一个二维数组,但只知道是4列的二维数组,此时行数是一个变值
// 这种方式其实 就是二维数组 传给 二维数组,属于同一个类型的传参
void search(float (*p)[4],int k){
printf("the score of No. %d are: \n",k);
for(int i=0;i<4;i++){
printf("%5.2f ",*(*(p+k)+i));
printf("%5.2f ",p[k][i]);
}
}
void search2(float p[][4],int k){
printf("the score of No. %d are: \n",k);
for(int i=0;i<4;i++){
printf("%5.2f ",*(*(p+k)+i));
printf("%5.2f ",p[k][i]);
}
}
float score[3][4] = {65,67,70,60,80,87,90,81,90,99,100,98};
// 这里传 score 意思是 score代表是一个 行指针数组
// score 与 *score 不同的是 offset,
// score offset是一行数据
// *score offset是一个数据, *score 等效于 *(score+0)+0 ,其实就是 score[0][0]
// 可参考文章 我记不住的那些编程语言的语法(数组)-1
search(score,2);
search2(score,2);
第四种: 将二维数组的指针作为 行指针进行传参,C99兼容, 然后获取某行的数据
// 4.
// C99 VLA (variable-length array) 变长数组
// 变长数组作为参数,变量n一定要在变量数组前面,这样运行时才能确定数组的大小
// 这里的 float (*p)[n] 等价于 float p[][n]
// 代表的是一个二维数组,但只知道是n列的二维数组,此时行数是一个变值
// 这种方式其实 就是二维数组 传给 二维数组,属于同一个类型的传参
void searchVLA(int n, float (*p)[n], int k){
printf("the score of No. %d are: \n",k);
for(int i=0;i<n;i++){
printf("%5.2f ",*(*(p+k)+i));
printf("%5.2f ",p[k][i]);
}
}
void searchVLA2(int n, float p[][n], int k){
printf("the score of No. %d are: \n",k);
for(int i=0;i<n;i++){
printf("%5.2f ",*(*(p+k)+i));
printf("%5.2f ",p[k][i]);
}
}
float score[3][4] = {65,67,70,60,80,87,90,81,90,99,100,98};
// 这个和上一种类似,只不过是用了 变长数组,将列数通过参数进行传递
searchVLA(4,score,2);
searchVLA2(4,score,2);
第五种: 将二维数组的指针转为 双重指针(指针的指针),
这种一般是动态分配,
缺点是: 行与行之间是割裂开的,也就是说地址是不连续的,而各行内部的元素的地址是连续的,所以和原始的二维数组是有区别的。
优点是: 相比于原始二维数组定义,这种动态分配可以充分利用内存空间,而不用找较大的一块连续空间盛放数据,利用率较高
double** arr 代表的不是一个二维数组,而是指针的指针,如果出现这种情况的函数形参的传值,那说明 调用的函数例如main函数里面一定是 使用malloc等动态分配数组的方式进行的,而不是原始的二维数组的定义,例如 int a[3][4]
// 5. 动态分配,dynamically allocated,这种方式使用较多
// 这种方式是因为动态分配,所以各行的元素并不是连续的
// 所以和二维数组是有区别的。
void assign(float** arr,int m,int n,float* p,int size){
for(int i =0;i<m;i++){
for(int j=0;j<n;j++){
//arr[i][j] = *(p++);
*(*(arr+i)+j) = *(p++);
}
}
}
void printarray( float **array, int m,int n ){
int i;
int j;
for( j = 0; j < m; j++ ){
for( i = 0; i < n; i ++){
printf( "%.2f ", array[j][i] );
}
printf( "\n" );
}
}
float score[3][4] = {65,67,70,60,80,87,90,81,90,99,100,98};
float** arr = (float**)malloc(3* sizeof(float*));
for(int i = 0;i<4;i++){
arr[i] = (float*)malloc(4*sizeof(float));
}
assign(arr,3,4,*score,12);
全部代码可参考上传代码包。
和矩阵相关的几道题
LeetCode 74题:给定一个有序的二维数组,查找某个元素是否存在于这个二维数组中。
分析:这道题很简单,其实我认为就是二维数组,将其转换为一维数组,且是有序的,那么我直接二分查找即可, 使用的是C语言写一下,这种情况需要将 mid转换为 i 和 j 坐标。
bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target){
int rows = matrixSize;
int columns = *matrixColSize;
int size = rows * columns;
int start = 0;
int end = size -1;
while(start<=end ){
int mid = start+ (end-start)/2;
int i=mid/columns;
int j=mid%columns;
// *(*(matrix+i)+j) 也是 OK的
if(matrix[i][j] == target){
return true;
}else if(matrix[i][j] < target){
start = mid+1;
}else {
end = mid-1;
}
}
return false;
}
这种方法有点性能一般,是因为你计算了mid,还得计算数组的i和j, 为什么我们要计算i和j,我们直接将这个二维数组按照一维数组来做,直接使用mid做判断,是否可行? 能否通过AC呢?
这个问题我们留到最后讨论。
还有另外一个解法更高效,其实就是二叉搜索树 Binary Tree,把这个二维数组通过逆时针旋转,想象成为一个二叉搜索树,每走一步将选择 左子树或右子树,这样每一步缩小一行或一列的范围。
bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target){
int rows = matrixSize;
int columns = *matrixColSize;
int row = 0;
int column = columns - 1;
while(row < rows && column>=0 ){
// *(*(matrix+row)+column) 这种表达也可以
if(matrix[row][column] == target){
return true;
}else if(matrix[row][column] < target){
row++;
}else {
column--;
}
}
return false;
}
这个74题的难点在于 给定的函数的形参,一开始我有点懵逼,如下面所示:
bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target)
问题1: matrix可以理解为是数据,是一个双重指针,指针的指针,它和 二维数组什么关系?
这种双重指针 并不能代表真正的二维数组,而是对二维数组的一种模拟,区别在于真实的二维数组行与行之间的地址是连续的,而双重指针则不连续,但是这种方式提高了内存的使用率,这种方式也能使用 matrix[i][j] 和 *(*(matrix+i)+j) 进行访问。
问题2:matrixSize是 行数, matrixColSize是列数
为什么行数和列数传参又有int整型,又有int指针,这是为什么? 直接传 两个int整型不就行了,为什么还要传指针,
说实话,这个我没有找到答案,不知道为啥传指针又传int整型
问题3: 上面我们提到的,可否使用 mid来做判断,不用再将其转换i和j了
答:从形参 int** matrix 可知,这个函数的调用者Caller使用malloc进行分配的内存空间,而不是常规的定义的二维数组,所以 这个 二重指针的matrix的各行之间的地址不是连续的,也就是说有 matrixSize行的数据但每一行数据之间是不连续的,所以我们无法将其转换为一维数组再使用指针去进行二分查找操作。 二分查找操作应该满足 有序且连续 这两个条件。
总结:
1. 你有一个二维数组,如何传给其他函数呢?
答: 要看这个二维数组是怎么创建的,是常规的定义,还是malloc方式定义。
目前针对有三种方式:
第一种方式是 函数形参也是二维数组,即 二维传给二维;
例如: 上面的 第3和第4
void search(float (*p)[4],int k)
void search2(float p[][4],int k)
void searchVLA(int n, float (*p)[n], int k)
void searchVLA2(int n, float p[][n], int k)
第二种方式是 函数形参是一维数组,即 二维 转一维;
例如:上面的 第1和第2
void average(float* p, int n)
void average2(float* p, int m, int n)
第三种方式是 函数形参是 双重指针
例如: 上面的 第5
void assign(float** arr,int m,int n,float* p,int size)
void printarray( float **array, int m,int n )
从二维数组推广到n维数组也是同理,所以无论如何变化,都是这三种的变形而已。