C语言总结六:一维/二维数组详细总结

       为方便对于数组的复习和掌握,本篇博客对于一维数组/二维数组进行全面的梳理,主要包括以下内容 :一维数组的定义及在内存中的存储方式、一维数组的访问方式/使用、二维数组的定义及在内存中的存储方式、二维数组的访问方式/使用、数组的函数封装(数组作为函数参数)、数组的基本操作。这里复习的是静态数组,即数组的长度固定,无法动态改变,实际开发常用的是通过动态内存申请的顺序表,可以根据实际需要动态的改变数组的长度。

目录

一、数组的基本概念

二、一维数组的定义及在内存中的存储方式

1、一维数组的定义及初始化

 2、一维数组在内存中的存储方式

 3. 动态内存申请一维数组 (长度可变)

三、一维数组的遍历方式

1、下标引用操作符访问

2、通过指针自增或者指针+偏移量访问

四、二维数组的定义及在内存中的存储方式

1、二维数组的定义及初始化

2.  动态申请二维数组

3、二维数组在内存中的存储方式

4. 指针与二维数组

五、二维数组的遍历方式

六、数组的函数封装

七、数组常见操作

7.1 计算数组中的特征值

7.2 数组的复制

7.3 数组元素的反转

 7.4 数组的排序-冒泡排序算法

7.5 二分查找/折半查找的递归实现与非递归实现算法


一、数组的基本概念

       为方便对大量相同数据类型的数据进行处理,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=&num;   *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 是序列的长度。相比于线性搜索,二分查找通常更快,尤其在大型有序序列中。但要注意,二分查找要求序列是有序的,如果序列无序,需要先进行排序。 因此,二分查找算法的使用前提是数据完全有序,二分查找常常作为笔试题的变形考察,需要重点掌握。

       以上便是一维数组和二维数组需要全面掌握内容,掌握基本的理论,遇到实际问题需要具体分析,才不会出现语法使用错误,提高开发效率, 更多精彩内容见下期!欢迎交流分享! 

  • 33
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来可期,静待花开~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值