为方便对于数组的复习和掌握,本篇博客对于一维数组/二维数组进行全面的梳理,主要包括以下内容 :一维数组的定义及在内存中的存储方式、一维数组的访问方式/使用、二维数组的定义及在内存中的存储方式、二维数组的访问方式/使用、数组的函数封装(数组作为函数参数)、数组的基本操作。这里复习的是静态数组,即数组的长度固定,无法动态改变,实际开发常用的是通过动态内存申请的顺序表,可以根据实际需要动态的改变数组的长度。
目录
一、数组的基本概念
为方便对大量相同数据类型的数据进行处理,C语言引入数组,数组从形式上来看,是指一组数目固定,数据类型相同的若干个元素的有序集合,它们按照一定的先后顺序排列,使用统一的数组名和不同的下标来唯一标识数组中的每一个元素,这一组数称为一个数组,每个数据称为一个数组元素。
更加深刻的理解如下:数组是一组相同数据类型变量的集合,并且局部数组存储在栈区的一块连续的内存空间,这使得可以使用指针来对数组的元素进行操作(增删改查),这也正是访问数组元素的下标从0开始的原因,这是由指针的访问方式是由偏移量决定的。并且数组元素的地址随着下标的增大而增大,即低地址向高地址开辟内存空间。需要注意的是:数组名是数组首元素的起始地址,因此数组名可以看作是一个指针变量,即arr等价于&arr[0];这意味着我们可以通过数组名加偏移量的方式来访问数组元素,此外数组作为函数参数传递的时候,传递的是数组的指针,便可以通过传递数组名即可。
利用数组来取代同类型的多个变量,将数组与循环结构结合起来,利用循环对数组中的元素进行操作,可以使算法大大简化,程序更加容易实现。
二、一维数组的定义及在内存中的存储方式
1、一维数组的定义及初始化
一维数组的定义:数组的元素类型 数组名 [数组的元素个数/长度],需要注意的是:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
注意:
1、数组名的命名规则遵循 C 语言中标识符的规则;
2、在定义数组的时候,必须指定数组元素的个数,数组长度必须是一个常量。
3、定义了数组,就相当于是在内存中开辟了一块连续的空间,空间的大小等于 sizeof(数组元素的类型)*数组的长度。
4、数组元素的下标是从 0 开始。
方式一:全部初始化,对数组的所有元素赋初值,直接用花括号括起来,元素之间用逗号隔开,数组的维度可以省略。
int arr[]={1,2,3,4,5,6,7,8,9,10};
方式二:声明一个数组,但并未进行初始化,函数内部定义的局部数组存放在栈区,其值为随机值,并未开辟内存空间,只是告诉编译器要申请一个数组。
int arr[10];
方式三: 部分初始化,对于指定大小的一维数组进行部分初始化,那么未初始化的数据元素赋值为类型的默认值。
char :'\0' short、int、long、long long :0 double、float : 0.0 bool :false
int arr[10]={1,2, 3}; //数组的前三个数据元素为1 2 3,其余剩下的元素赋值为类型的默认值0
//在定义数组的同时 给所有的数组元素赋值为 0
int arr3[10] = { 0 };//只有 0 可以这样赋值
int arr4[10] = { 1 };//这样子 只有 arr4[0] = 1 其余的是0
2、一维数组在内存中的存储方式
数组在内存中是连续存放的,并且数组的地址随着下标的增大而增大,这使得可以通过指针来访问数组元素。
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int len = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<len; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
从上面可以看到数组每个元素与前一个数组元素的地址相差4,也就是数组元素所占的字节数。
需要注意:
C 语言规定,数组变量一旦声明,数组名指向的地址就不可更改。因为声明数组时, 编译器会自动为数组分配内存地址,这个地址与数组名是绑定的,不可更改。 因此,当数组定义后,再用大括号重新赋值,是不允许的。下面的代码会报错:等号赋值,左边必须是变量!
//定义数组 C 语言在内存中开辟一块连续的空间 首地址与数组名绑定在一起
//也就是说数组名就是一个地址 是一个常量
int arr[10];
//在给通过{}初始化列表给赋值 就会报错
arr = {1,2,3,4,5,6,7,8,9,0 };//报错!!
arr[10] = { 1,2,3,4,5,6 };//报错!!!
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int brr[10];
//数组名是首元素的地址 说白了 就是一个常量
brr = arr;//报错!!!将一个数组名 赋值给 另一个数组名也会报错
int brr[10];
brr = NULL;//报错
3. 动态内存申请一维数组 (长度可变)
方式一:直接申请
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main()
{
int *p=(int*)malloc(sizeof(int)*10);
assert(p!=NULL);
for(int i=0;i<10;i++)
{
p[i]=i+1; //相当于*(p+i)=i+1;本质上就是指针加偏移量,然后解引用找到这块内存空间
}
for(int i=0;i<10;i++)
{
printf("%d\t",p[i]);
}
free(p);
p=NULL;
return 0;
}
方式二:间接申请
第一种:传参的方式
第二种:返回值的方式
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//传参的方式
void MyArr(int** p, int len) //必须传递二级指针
{
*p = (int*)calloc(len,sizeof(int)); //在内部解引用修改指针的指向
assert(*p != NULL);
for (int i = 0; i < len; i++)
{
//对二级指针解引用得到一级指针,再进行偏移,解引用访问这块内存空间
//相当于*((*p)+i),放在等号左边,得到的是这块内存空间,相当于变量a的名字
(*p)[i]= i + 1;
}
}
int main()
{
int* p = NULL;
int len = 10;
MyArr(&p, len);//必须传递的是二级指针(一级指针的地址)
for (int i = 0; i < len; i++)
{
printf("%d\t", p[i]);
}
free(p);
p=NULL;
return 0;
}
void MyArr(int* p, int len)
{
//这种写法是错误的,修改了形参指针变量的指向,只能通过传地址,也就是传一级指针的地址,即二级指针,因此形参设计为二级指针,在函数内部通过对二级指针解引用便可以修改一级指针的指向
p = (int*)calloc(len,sizeof(int)); //在内部解引用修改指针的指向
assert(p != NULL);
for (int i = 0; i < len; i++
{
p[i]= i + 1;
}
}
//返回值的方式
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int* MyArray(int len)
{
//堆区申请的内存在函数调用结束后,不会被释放,因此返回堆区的地址是可以的
int* p = (int*)malloc(sizeof(int) * len);
assert(p != NULL);
return p;
}
int main()
{
int* p = NULL;
int len = 10;
p = MyArray( len);
for (int i = 0; i < len; i++)
{
p[i] = i + 1; //相当于*(p+i)=i+1;本质上就是指针加偏移量,然后解引用找到这块内存空间
}
for (int i = 0; i < len; i++)
{
printf("%d\t", p[i]);
}
free(p);
p = NULL;
return 0;
}
三、一维数组的遍历方式
由于数组在内存中是连续存放的,这样在对数组元素进行大量移动或者拷贝时,可以考虑使用内存函数memcpy()、memmove(),更加简单高效,提高开发效率,后续总结内存函数。
1、下标引用操作符访问
数组元素的访问,是通过下标引用操作符操作的,使用方式同变量的使用,arr[i]代表数组中第i个元素,并且数组元素的下标是从0开始的,数组长度减1结束,因此对于数组的使用时要对于边界问题进行检查,防止出现数组越界问题,超出了数组合法空间的访问,会出现程序崩溃。 C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就 是正确的, 所以程序员写代码时,最好自己做越界的检查。对于数组的遍历通常是通过循环结构,如for循环,i既可以作为循环变量,又可以作为数组元素访问的下标定位。
数组的元素个数(数组的长度)可以通过公式求解,方便程序的实现,但需要注意的是数组的定义和sizeof(arr)在同一个作用域里面,sizeof(arr)代表整个数组的大小,即整个数组所占的字节数。公式如下:
数组长度:int len=sizeof(arr)/sizeof(arr[0])
或者: int len=sizeof(arr)/sizeof(数组的类型如:int);
#include<stdio.h>
int main()
{
int arr[10]={0};
int len=sizeof(arr)/sizeof(arr[0]);
//通过for循环为数组赋初值
for(int i=0;i<len;i++)
{
scanf("%d",&arr[i]);
}
printf("\n");
//通过for循环打印数组元素
for(int i=0;i<len;i++)
{
printf("%d",arr[i]);
}
printf("\n");
return0;
}
2、通过指针自增或者指针+偏移量访问
由于数组在内存是连续存放的,因此可以通过指针对其进行访问,这是由于指针的自增运算和指针加偏移量是具有意义的,这与指针加1的能力有关,更与指针所指向的数据元素的数据类型有关,通过指针可以找到数据存放的内存地址,然后解引用便可以访问到这块内存空间,修改所存储的数据,或者遍历。一维数组在内存中是连续存放的,因此可以用指针进行访问;注意:数组的长度为5,下标的范围为:0-4;比如:a+1或者p+1就是a[1]的地址,a+2或者p+2就是a[2]的地址;最大的访问的地址为a+4或者p+4;
一、 通过指针加偏移量的方式,此时循环变量可以作为指针的偏移量的次数,从而实现对整个数组元素的访问。需要注意的是:
*(p+i)等价于*(arr+i)也等价于p[i] ,还等价于arr[i] , 第一种是先对指针进行偏移,在进行解引用访问这块内存空间,第三种底层实现本质上和第一种是一样的,数组名是数组首元素的起始地址,因此,这四种使用方式本质一样,常用的是p[i]和arr[i]。
因此,从下述代码可以得出:数组名arr、指针变量p、&arr[0]这三种写法等价,都指向数组的第一个元素。
#include<stdio.h>
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9,10};
int len=sizeof(arr)/sizeof(arr[0]);
int *p = arr;
//相当于int *p=&arr[0]; 数组名是数组首元素的起始地址,现在p指向数组首元素的起始地址
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
printf("%4d",*(p+i));//先指针偏移再解引用,也可以使用p[i]相当于arr[i]
}
return 0;
}
#include<stdio.h>
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9,10};
int len=sizeof(arr)/sizeof(arr[0]);
//循环变量在这里充当偏移量的角色
for(int i=0;i<len;i++)
{
//数组名是数组首元素的起始地址,因此,数组名本质上也是一个指针,可以充当指针变量使用
printf("%4d",*(arr+i));
}
return 0;
}
二、通过指针的自增运算符,访问数组元素 ,指针自增1,代表指针偏移所指向的数据类型大小的字节数。这种访问方式更加高效!
#include<stdio.h>
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9,10};
int len=sizeof(arr)/sizeof(arr[0]);
int *p = arr;
//相当于int *p=&arr[0]; 数组名是数组首元素的起始地址,现在p指向数组首元素的起始地址
for(;p<arr+len;p++)
{
printf("%4d",*p); //解引用
}
return 0;
}
指针自增或者指针+偏移量访问数组元素的区别:数组名代表数组首元素的地址(十六进制的数),是一个常量,而数组指针p是变量(除非特别指明它是常量)它的值可以任意改变,即数组名只能指向数组的开头,而数组指针可以指向数组的开头,再指向其他元素,这对于自增运算符是不同的,因为自增运算符针对的是变量,而数组名arr是一个常量地址,不可以进行改变,因此不能使用自增运算符,而指针变量p是一个变量,保存的是数组元素的地址,是可以改变的,故它可以使用自增运算符。
由于自增运算符是针对变量的,因此,*p++可以使用,但是*arr++不可以使用。
此外,int *p=arr; p++; p[-1];下标可以为负值,代表指针向低地址偏移4个字节,也就是第一个元素!但是数组的下标不可以为负值!
四、二维数组的定义及在内存中的存储方式
1、二维数组的定义及初始化
二维数组能够体现矩阵中数据之间的关系,简化以行和列方式组织的一组数据的处理过程。二维数组的定义如下:数据类型 数组名[行数][列数]; 注意:行和列只能用常量表达式,二维数组的元素个数等于行数乘以列数,并且每一维的下标都从0开始。
可以这么理解二维数组:二维数组 a 可以看成由 3 个一维数组构成。他们的数组名分别是 a[0],a[1],a[2]. 这 3 个一维数组分别有 4 个元素,如 a[0]的四个元素是 a[0][0],a[0][1],a[0][2],a[0][3],a[0][4];
arr[0]、arr[1]、arr[2]便是第一行元素、第二行元素、第三行元素的起始地址。
方式一:对二维数组元素全部进行初始化。一个花括号代表一行;
int arr[2][3]={{1,2,3},{4,5,6}};
方式二: 不进行初始化,局部数组未初始化,数组元素为随机值;
int arr[2][3];
方式三: 部分初始化,未赋值的元素将其赋值为类型的默认值;
int arr[3][4]={{1,2},{3,4},{5,6}};
注意事项:若对二维数组的所有元素赋初值,即完全初始化,则数组定义的第一维行数可以省略不写,但是列数不可以省略;如下所示:
int arr[][3]={{1,2,3},{4,5,6}};
int arr[][4]={{1,2},{3,4},{5,6}};
2. 动态申请二维数组
方式一:直接申请
第一种:二级指针:二级指针可以保存多个一级指针的地址
第二种:数组指针
二级指针的方式:
简单复习二级指针
int num=0; *p等效于num
int* p=# *p等效于p, *p等效于p[0],即*(p+0) **q等效于num
int* *pp=&p; //定义二级指针保存一级指针的地址
int* p=(int* )malloc(sizeof(int)*num);
(指针的类型)malloc(sizeof(指针所指向的数据的类型)*NUM)
指针的类型: 强制类型转换 去掉变量名 int*
指针所指向的数据的类型:操作的数据类型 去掉变量名和* int
void initArr(int** arr, int row, int col);
void printArr(int** arr, int row, int col);
//动态创建一个二维数组
int main()
{
int row = 0, col = 0;
int** par = (int**)malloc(sizeof(int*) * row);
assert(par!=NULL);
memset(par, 0, sizeof(int*) * row);
for (int i = 0; i < row; i++)
{
相当于*(par+i),它是一个一级指针,指向一个int类型的数组
par[i] = (int*)malloc(sizeof(int) * col);
memset(par[i], 0, sizeof(int) * col);
}
initArr(par, row, col);
printArr(par, row, col);
//注意:内存释放与申请的顺序相反
for (int i = 0; i < row; i++)
{
free(par[i]);
par[i] = NULL;
}
free(par);
par = NULL;
return 0;
}
数组指针的方式:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
int i, j;
int(*arr)[3] = (int(*)[3])calloc(4, sizeof(int[3]));//4行3列
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d %p\n", arr[i][j], &arr[i][j]);
}
}
free(arr);
arr = NULL;
return 0;
}
方式二:间接申请
第一种:传参数的方式
第二种:返回值的方式
传参的方式:
返回值的方式:
3、二维数组在内存中的存储方式
从表面看二维数组的逻辑结构为一个类似表的结构,而从物理存储结构看: 二维数组在内存中也占用一片连续的存储区域,在C语言中二维数组元素的存放顺序是按照行存放的,即在内存中先顺序存放第一行元素,在接着存放第二行元素。
#include <stdio.h>
int main()
{
int arr[3][4];
int row=sizeof(arr)/sizeof(arr[0]); //利用公式求数组的行数
int col=sizeof(arr[0])/sizeof(arr[0][0]); //利用公式求数组的列数
//双层for循环实现对二维数组的遍历,外层循环变量控制行数的变化,内层循环变量控制列数的变化
for(int i=0; i<row; i++)
{
for(int j=0; j<col; j++)
{
printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
}
}
return 0;
}
二维数组的逻辑结构:按照矩阵的方式存储。 二维数组的物理存储结构:连续的按照行优先存储。
C 语言中的多维数组基本的定义是以数组作为元素构成的数组,二维数组的数组元素是一维数组,三维数组的数组元素是一个二维数组,依此类推。也就是说,多维数组用的是一个嵌套的定义。
4. 指针与二维数组
二维数组在内存是按照行连续存放的,因此,它可以用指针进行访问,二维数组名a 可以看成由 3 个一维数组构成。他们的数组名分别是 a[0],a[1],a[2],这 3 个一维数组分别有 4 个元素,如 a[0]的四个元素是 a[0][0],a[0][1],a[0][2],a[0][3],a[0][4];
二维数组名a代表第0行的地址,它是一个数组指针,指向第0行,因此,a+1的步长跨过第0行;a+2跨过第1行(每次的步长为1行);
a[0]代表的是二维数组的第一个一维数组的数组名,一维数组的数组名代表首元素的地址,因此a[0]是第0行第0列的地址,它是一个列地址,属于整型指针,此时,a[0]+1会加上4个字节。注意事项:行地址和列地址可以互相转换
行地址变为列地址加*号。如下:
(1)a<->*a=a[0] 代表的含义:a代表第0行的地址,*a代表第1行的首地址,相当于a[0]
(2)a+1<->*(a+1)=a[1] 代表的含义:a+1代表第1行的地址,*(a+1)代表第1行的首地址,相当于a[1]。
(3)a+2<->*(a+2)=a[2] 代表的含义:a+2代表第2行的地址,*(a+2)代表第2行的首地址,相当于a[2]。列地址变行地址加&;如下:
a[1]<->&a[1]
#include<stdio.h>
int main()
{
int a[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
printf("%p\n", a);//第 0 行的行地址
printf("%p\n", a + 1);//加 16 个字节
printf("%p\n", a[0]);//第 0 行 0 列的地址
printf("%p\n", a[0] + 1);//加 4 个字
return 0;
}
//a[0] 可以写成 *(a+0), a[1] 可以写成 *(a+1)
printf("%p -- %p -- %p \n", a[0], *(a + 0),&a[0][0]);//第 0 行第 0 列的地址
printf("%p -- %p -- %p \n", a[1], *(a + 1), &a[1][0]);//第 1 行第 0列的地址
printf("%p\n", &a[0]);//a[0]是第 0 行一维数组的数组名,对其&运算将列地址转为行地址
printf("%p\n", &a[0] + 1);//+1 加 16 个字节
printf("%p\n", a + 1);//行地址
printf("%p\n", *(a + 1) + 1);//加 4个字节
用指针的方式遍历二维数组
第一种:使用数组指针
第二种:使用一级指针
#include <stdio.h>
//参数设计为数组指针
void print(int(*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *((*(p + i)) + j));
//也可以写成printf("%d ", *(p[i] + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
//二维数组名本质上是一个数组指针
print(arr, 3, 5);
return 0;
}
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
int* p;
for (p = arr[0]; p <= arr[2] + 4; p++)
{
if ((p - arr[0]) % 5 == 0)
{
printf("\n");
}
printf("%5d", *p);
}
return 0;
}
五、二维数组的遍历方式
二维数组元素的访问,同样是通过下标引用操作符[][]操作的,使用方式同变量的使用,通过下标定位二维数组中的每一个元素。arr[i][j]; 二维数组有两个维度行和列,因此,想要遍历一个二维数组,用双层for循环即可,外层循环变量控制行数的变化,内层循环变量可以用来控制列数的变化;
二维数组的行数和列数作为循环变量的终止条件,同样可以通过公式求解,方便程序的实现,但需要注意的是数组的定义和sizeof(arr)在同一个作用域里面,sizeof(arr)代表整个数组的大小,即整个数组所占的字节数。公式如下:
计算行数: int row=sizeof(arr)/sizeof(arr[0]); //总的字节数除以每行的字节数
计算列数:int col =sizeof(arr[0])/sizeof(arr[0][0]); //一行的字节数除以一个元素的字节数
#include <stdio.h>
int main()
{
int arr[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//利用公式计算行数和列数
int row=sizeof(arr)/sizeof(arr[0]);
int col=sizeof(arr[0])/sizeof(arr[0][0]);
//双层for循环进行遍历
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
printf("%4d",arr[i][j]);
}
printf("\n");
}
return 0;
}
练习:打印杨辉三角
#include <stdio.h>
int main()
{
int arr[10][10];
for (int i = 0; i < 10; i++)
{
for (int j = 0; j <= i; j++)
{
if (j == 0 || i == j)
{
arr[i][j] = 1;
}
else
{
arr[i][j] = arr[i - 1][j] + arr[i - 1][j - 1];
}
}
}
for (int i = 0; i < 10; i++)
{
for (int j = 0; j <= i; j++)
{
printf("%-5d", arr[i][j]);
}
printf("\n");
}
return 0;
}
六、数组的函数封装
数组作为函数参数
数组是一系列数据的集合,无法通过参数将他们一次性传递到函数的内部,如果希望在函数的内部操作数组,必须传递数组的指针(地址),函数封装时的参数为同类型的指针变量接收传入的地址,这样在函数内部通过解引用便可以操作函数外部的数据,并且,此时定义的指针变量参数仅仅是一个指针,在函数内部无法通过这个指针获得数组的长度,(在x86操作系统,指针的大小为4个字节,固定值)。因此,必须在主函数通过计算数组公式来计算数组的长度,作为函数的参数传递到函数的内部。想要通过改变形参来影响实参,必须要:传指针,解引用
指针变量作函数参数!!!
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁,像数组、字符串、动态分配的内存都是一些函数集合,没有办法通过一个参数全部传入函数内部,只能传递他们的指针,在函数内部,通过指针解引用来影响这些数据集合。
//查找数组中的某个元素
#include <stdio.h>
#include <cassert>
//函数封装,以下三种写法相同,底层实现都是退化成整形指针
int Searchval(int arr[5],int len,int val)
int Searchval(int arr[],int len,int val)
int Searchval(int *arr,int len,int val)
int Searchval(int *p,int len,int val)
{
assert(p!=NULL);
for(int i=0;i<len;i++)
{
if(arr[i]==val) //或者if(p[i]==val);或者*(p+i)==val;
{
return i;
}
}
return -1;
}
int main()
{
int arr[]={2,4,8,11,6,7,21,35};
int len=sizeof(arr)/sizeof(arr[0]);
int res=Searchval(arr,len,7); //数组名arr是数组首元素的起始地址
if(res>=0 && res<len)
{
printf("该数组存在此元素!下标为%d\n",res);
}
else
{
printf("该数组不存在此元素!");
}
return 0;
}
封装函数,实现数组逆序。(要求:用指针实现)
void reverseArr(int *p ,int len)
{
int* q = p + (len-1);//指向最后一个元素
while (p<q)
{
int t = *p;
*p = *q;
*q = t;
p++;
q--;
}
}
void printArr(int arr[], int len)
{
int* p = arr;
for ( ; p < arr+len; p++)
{
printf("%5d", *p);
}
printf("\n");
}
int main()
{
int a[5] = { 1,2,3,4,5 };
reverseArr(a, 5);
printArr(a, 5);
return 0;
}
以下四种形式,在底层传递的都是指针(地址),来传递整个数组,但是不论是哪一种方式都不能在函数封装的内部求数组的长度,因为仅仅是一个指针而不是真正的数组,所以必须在额外增加一个参数来传递数组的长度!
int Searchval(int arr[5],int len,int val);
int Searchval(int arr[],int len,int val);
int Searchval(int *arr,int len,int val);
int Searchval(int *p,int len,int val);
七、数组常见操作
7.1 计算数组中的特征值
定义一个 int 型的一维数组,包含 10 个元素,然后求出数组中的最大值,最小值,总和,平均值,并输出出来。
基本思想:把第一个数组元素定义为最大值或者最小值,然后逐个进行比较。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int randNum(int min,int max)
{
int num = min + rand()% max;
return num;
}
int main()
{
time_t t;
//初始化随机数发生器 也就是种子 有了种子 C 语言会根据一个公式去产生随机数
srand((unsigned)time(&t));
int arr[10];
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
arr[i] = randNum(1,20);
printf("%5d", arr[i]);
}
printf("\n");
int min, max, sum,avg;
sum = avg = 0;
min = max = arr[0];
for (int i = 0; i < len; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
sum += arr[i];
}
avg = sum / len;
printf("min=%d,max = %d,sum=%d,avg=%d\n", min, max, sum,avg);
return 0;
}
7.2 数组的复制
方法一:使用循环--数组元素逐个复制
#include <stdio.h>
#define LENGTH 3
int main()
{
int a[LENGTH] = {10, 20, 30};
int b[LENGTH];
// 复制数组 a 到数组 b
for (int i = 0; i < LENGTH; i++)
{
b[i] = a[i];
}
// 打印数组 b 的内容
printf("复制后的数组 b:");
for (int i = 0; i < LENGTH; i++)
{
printf("%d ", b[i]);
}
printf("\n");
return 0;
}
方法 2:使用 memcpy()函数
memcpy() 函数定义在头文件 string.h 中,直接把数组所在的那一段内存,再复制一 份。3 个参数依次为:目标数组、源数组以及要复制的字节数。
#include <stdio.h>
#include <string.h>
#define LENGTH 3
int main()
{
int a[LENGTH] = {10, 20, 30};
int b[LENGTH];
// 使用 memcpy 函数复制数组 a 到数组 b
memcpy(b, a, LENGTH * sizeof(int));
// 打印数组 b 的内容
printf("复制后的数组 b:");
for (int i = 0; i < LENGTH; i++)
{
printf("%d ", b[i]);
}
printf("\n");
return 0;
}
两种方式对比:
1. 循环复制: 优点:简单直观,容易理解和实现。不需要引入额外的头文件。 缺点:需要编写循环代码来遍历数组并逐个赋值,相对而言可能稍显繁琐。 不适用于复制大型数组或复杂数据结构。
2. memcpy 函数复制:优点:使用标准库提供的函数,可以实现快速且高效的内存复制。适用于大型数组或复杂数据结构的复制。可以直接复制字节数,不需要遍历数组。缺点:需要包含 头文件。对于简单的数组复制,可能有些过于繁重。
7.3 数组元素的反转
方式 1:循环交换的方式
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9};
int size = sizeof(arr) / sizeof(arr[0]); //数组的长度
printf("原始数组:");
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
for(int i = 0;i < size / 2;i++)
{
int temp = arr[i];
arr[i] = arr[size - 1 - i];
arr[size - 1 - i] = temp;
}
printf("反转后的数组:");
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
方式2:指针的方式
int main()
{
int arr[] = {1, 2, 3, 4, 5,6,7,8,9};
int size = sizeof(arr) / sizeof(arr[0]); //数组的长度
printf("原始数组:");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
int left = 0; // 起始指针
int right = size - 1; // 结尾指针
while (left < right)
{
// 交换起始指针和结尾指针指向的元素
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
// 更新指针位置
left++;
right--;
}
printf("反转后的数组:");
for (int i = 0; i < size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
7.4 数组的排序-冒泡排序算法
冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是多次遍历待排序的序列,每次遍历都比较相邻的两个元素,如果它们的顺序不正确就交换它们,这样一轮遍历之后,最大(或最小)的元素就会移动到序列的末尾。重复这个过程,直到整个序列有序。
基本思想如下:
1. 比较相邻元素:从序列的第一个元素开始,依次比较相邻的两个元素,比较它们的大小。
2. 交换元素位置:如果发现顺序不对(比如前面的元素大于后面的元素),就交换这两个元素的位置,使得较大的元素向后移动。
3. 一轮遍历后的效果: 一轮遍历之后,最大的元素就会移动到序列的末尾。
4. 重复遍历:重复以上步骤,每一轮遍历都会使得未排序部分的最大元素移动到正确的位置。为了确保序列完全有序,需要进行 n-1 轮遍历(n 是序列的长度)。
5. 优化:可以在每一轮遍历中加入一个标志位flag,若一轮遍历中没有发生交换操作,说明序列已经有序,可以提前结束排序。
假设数组的长度为N(有N个元素),则需要比较(N-1)趟,每趟需要比较(N-1-i)对数据,因此双层for循环的遍历区间为:
内层循环:0->len-1;外层循环:0->len-1-i;
冒泡排序是一种简单但效率较低的排序算法,其时间复杂度为O(n^2),其中n是序列的长度。在实际应用中,在大规模数据排序时,更常使用效率更高的排序算法,如快速排序、归并排序等。
#include <stdio.h>
#include <cassert>
void bubblesort(char* arr, int len)
{
int i, j;
bool flag;
assert(arr != NULL);
for (i = 0; i < len - 1; i++) //控制趟数(n个数需要进行n-1趟排序)
{
flag = false; //设立一个标志位,如果数据已经有序,不需要进行排序则退出循环
for (j = 0; j < len - 1 - i; j++) //每一趟需要进行比较的数据(n-1-i)
{
if (arr[j] > arr[j + 1])
{
//进行数据的交换,换座位
flag = true;
char temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (flag == false)
{
break;
}
}
for (int j = 0; j < len; j++)
{
printf("%c\t", arr[j]);
}
}
int main()
{
char arr[] = { 'a','h','c','b','y','q','d','u','o','s' };
int len = sizeof(arr) / sizeof(arr[0]);
bubblesort(arr, len);
return 0;
}
7.5 二分查找/折半查找的递归实现与非递归实现算法
二分查找(Binary Search)是一种高效的搜索算法,其基本思想是通过将查找范围缩小为一半,逐步缩小搜索范围,直到找到目标元素或确定目标元素不存在。
以下是二分查找的基本步骤:
1. 初始条件:对于二分查找,首先要确保待查找的序列是有序的,通常是一个升序排列的序列。
2. 确定搜索范围:确定整个序列的搜索范围,初始时是整个有序序列。
3. 计算中间位置:计算搜索范围的中间位置,可以使用 (left + right) / 2或 left + (right - left) / 2 计算中间位置(防止溢出数据表示的范围)。其中,left是当前搜索范围的起始位置,right是结束位置。
4. 比较中间元素: 将目标元素与中间位置的元素进行比较。
1)如果目标元素等于中间位置的元素,搜索完成,找到目标元素。
2)如果目标元素小于中间位置的元素,说明目标元素在左半部分,将搜索范围缩小为左半部分。
3)如果目标元素大于中间位置的元素,说明目标元素在右半部分,将搜索范围缩小为右半部分。5. 更新搜索范围:根据比较的结果,更新搜索范围。如果目标元素在左半部分,更新 right 为中间位置的前一个位置;如果在右半部分,更新 left 为中间位置的后一个位置。
6. 重复过程:重复步骤3到步骤5,直到找到目标元素或者搜索范围缩小为空。
7. 结束条件:如果搜索范围为空,则说明目标元素不存在于序列中。
二分查找非递归实现算法
#include <stdio.h>
#include <stdlib.h>
#include <cassert>
int binarysearch(int* arr, int len, int val)
{
assert(arr != NULL && len > 0); //参数检验
int left = 0, right = len - 1;
while (left <= right) //查找的条件
{
int midindex = (left + right) / 2;
//面试进行优化,利用位运算更偏底层运行速度更快。
//优化一:midindex = (left + right) >> 1;
// 提示:若: 数据量很大 [0,200] left=150,right = 160; -> 310, 有可能超出范围如何解决? 算术运算符操作的数据范围只能在基本数据类型的范围之内
//优化二:midindex = ((right - left)>>1) + left;
if (arr[midindex] == val)
{
return midindex;
}
else if (arr[midindex] < val)
{
left = midindex + 1;
}
else
{
right = midindex - 1;
}
}
return -1;
}
int main()
{
int arr[] = { 2,5,6,8,9,14,15,16,20,22,24,28,30,41 };
int len = sizeof(arr) / sizeof(arr[0]);
int res = binarysearch(arr, len, 15);
if (res == -1)
{
printf("该数字不存在!\n");
}
else
{
printf("该数字存在,下标为:%d\n", res);
}
return 0;
}
二分查找递归实现算法
#include <stdio.h>
#include<cassert>
int binarysearch0(int* arr, int len, int value, int left, int right)
{
if (left > right)
return -1;
int midindex = (left + right) / 2;
if (arr[midindex] == value)
{
return midindex;
}
else if (arr[midindex] > value)
{
return binarysearch0(arr, len, value, left, midindex - 1);
}
else
{
return binarysearch0(arr, len, value, midindex + 1, right);
}
}
int binarysearch(int* arr, int len, int value)
{
assert(arr != NULL);
return binarysearch0(arr, len, value, 0, len - 1);
}
int main()
{
int arr[] = { 2,5,6,8,12,18,20,21,25,32,35,40 };
int len = sizeof(arr) / arr[0];
int res = binarysearch(arr, len, 18);
printf("查找结果为:(结果为-1,代表未找到)%4d \n", res);
return 0;
}
二分查找的优势在于其每一步都将搜索范围缩小为原来的一半,因此它的时间复杂度是 O(log n),其中 n 是序列的长度。相比于线性搜索,二分查找通常更快,尤其在大型有序序列中。但要注意,二分查找要求序列是有序的,如果序列无序,需要先进行排序。 因此,二分查找算法的使用前提是数据完全有序,二分查找常常作为笔试题的变形考察,需要重点掌握。
以上便是一维数组和二维数组需要全面掌握内容,掌握基本的理论,遇到实际问题需要具体分析,才不会出现语法使用错误,提高开发效率, 更多精彩内容见下期!欢迎交流分享!