为方便对于数组的复习和掌握,本篇博客对于一维数组/二维数组进行全面的梳理,主要包括以下内容 :一维数组的定义及在内存中的存储方式、一维数组的访问方式/使用、二维数组的定义及在内存中的存储方式、二维数组的访问方式/使用、数组的函数封装(数组作为函数参数)、冒泡排序算法(一组数-数组)、二分查找/折半查找的递归实现与非递归实现算法(一组数-数组)。这里复习的是静态数组,即数组的长度固定,无法动态改变,实际开发常用的是通过动态内存申请的顺序表,可以根据实际需要动态的改变数组的长度。
为方便对大量相同数据类型的数据进行处理,C语言引入数组,数组从形式上来看,是指一组数目固定,数据类型相同的若干个元素的有序集合,它们按照一定的先后顺序排列,使用统一的数组名和不同的下标来唯一标识数组中的每一个元素,这一组数称为一个数组,每个数据称为一个数组元素。
更加深刻的理解如下:数组是一组相同数据类型变量的集合,并且局部数组存储在栈区的一块连续的内存空间,这使得可以使用指针来对数组的元素进行操作(增删改查),这也正是访问数组元素的下标从0开始的原因,这是由指针的访问方式是由偏移量决定的。并且数组元素的地址随着下标的增大而增大,即低地址向高地址开辟内存空间。需要注意的是:数组名是数组首元素的起始地址,因此数组名可以看作是一个指针变量,即arr等价于&arr[0];这意味着我们可以通过数组名加偏移量的方式来访问数组元素,此外数组作为函数参数传递的时候,传递的是数组的指针,便可以通过传递数组名即可。
利用数组来取代同类型的多个变量,将数组与循环结构结合起来,利用循环对数组中的元素进行操作,可以使算法大大简化,程序更加容易实现。
一维数组的定义:数组的元素类型 数组名 [数组的元素个数/长度],需要注意的是:数组创建,在C99标准之前, [] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
方式一:全部初始化,对数组的所有元素赋初值,直接用花括号括起来,元素之间用逗号隔开,数组的维度可以省略。
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
数组在内存中是连续存放的,并且数组的地址随着下标的增大而增大,这使得可以通过指针来访问数组元素。
#include
int main()
{
int arr[10] = {0};
int i = 0;
int len = sizeof(arr)/sizeof(arr[0]);
for(i=0; i
由于数组在内存中是连续存放的,这样在对数组元素进行大量移动或者拷贝时,可以考虑使用内存函数memcpy()、memmove(),更加简单高效,提高开发效率,后续总结内存函数。
数组元素的访问,是通过下标引用操作符操作的,使用方式同变量的使用,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
int main()
{
int arr[10]={0};
int len=sizeof(arr)/sizeof(arr[0]);
//通过for循环为数组赋初值
for(int i=0;i
由于数组在内存是连续存放的,因此可以通过指针对其进行访问,这是由于指针的自增运算和指针加偏移量是具有意义的,这与指针加1的能力有关,更与指针所指向的数据元素的数据类型有关,通过指针可以找到数据存放的内存地址,然后解引用便可以访问到这块内存空间,修改所存储的数据,或者遍历。使用指针自增运算和指针加偏移量的方式访问数组还是有区别的,根据实际开发需要选择。
一、 通过指针加偏移量的方式,此时循环变量可以作为指针的偏移量的次数,从而实现对整个数组元素的访问。需要注意的是:
*(p+i)等价于*(arr+i)也等价于p[i] ,还等价于arr[i] , 第一种是先对指针进行偏移,在进行解引用访问这块内存空间,第三种底层实现本质上和第一种是一样的,数组名是数组首元素的起始地址,因此,这四种使用方式本质一样,常用的是p[i]和arr[i]。
因此,从下述代码可以得出:数组名arr、指针变量p、&arr[0]这三种写法等价,都指向数组的第一个元素。
#include
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
#include
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
二、通过指针的自增运算符,访问数组元素 ,指针自增1,代表指针偏移所指向的数据类型大小的字节数。
#include
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
指针自增或者指针+偏移量访问数组元素的区别:数组名代表数组首元素的地址(十六进制的数),是一个常量,而数组指针p是变量(除非特别指明它是常量)它的值可以任意改变,即数组名只能指向数组的开头,而数组指针可以指向数组的开头,再指向其他元素,这对于自增运算符是不同的,因为自增运算符针对的是变量,而数组名arr是一个常量地址,不可以进行改变,因此不能使用自增运算符,而指针变量p是一个变量,保存的是数组元素的地址,是可以改变的,故它可以使用自增运算符。
即*p++可以使用,但是*arr++不可以使用。
二维数组能够体现矩阵中数据之间的关系,简化以行和列方式组织的一组数据的处理过程。二维数组的定义如下:数据类型 数组名[行数][列数]; 注意:行和列只能用常量表达式,二维数组的元素个数等于行数乘以列数,并且每一维的下标都从开始。
方式一:对二维数组元素全部进行初始化。一个花括号代表一行;
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}};
从表面看二维数组的逻辑结构为一个类似表的结构,而从物理存储结构看: 二维数组在内存中也占用一片连续的存储区域,在C语言中二维数组元素的存放顺序是按照行存放的,即在内存中先顺序存放第一行元素,在接着存放第二行元素。
#include
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|
二维数组的逻辑结构:按照矩阵的方式存储。
二维数组的物理存储结构:连续的按照行优先存储。
C 语言中的多维数组基本的定义是以数组作为元素构成的数组,二维数组的数组元素是一维数组,三维数组的数组元素是一个二维数组,依此类推。也就是说,多维数组用的是一个嵌套的定义。
比如,在C语言中,可以把一个二维数组看作是特殊的一维数组,int arr[3][4];可以看作是由3个元素arr[0]、arr[1]、arr[2]组成的一个一维数组,而arr[0]、arr[1]、arr[2]又分别包含了各自的四个元素;即可将arr[0]、arr[1]、arr[2]分别看作为包含4个元素的一维数组。
arr[0]、arr[1]、arr[2]便是第一行元素、第二行元素、第三行元素的起始地址。
二维数组元素的访问,同样是通过下标引用操作符[][]操作的,使用方式同变量的使用,通过下标定位二维数组中的每一个元素。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
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|
打印杨辉三角
#include
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
#include
//函数封装,以下三种写法相同,底层实现都是退化成整形指针
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=0 && res
以下四种形式,在底层传递的都是指针(地址),来传递整个数组,但是不论是哪一种方式都不能在函数封装的内部求数组的长度,因为仅仅是一个指针而不是真正的数组,所以必须在额外增加一个参数来传递数组的长度!
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);
冒泡排序(Bubble Sort)是一种简单的排序算法,其基本思想是多次遍历待排序的序列,每次遍历都比较相邻的两个元素,如果它们的顺序不正确就交换它们,这样一轮遍历之后,最大(或最小)的元素就会移动到序列的末尾。重复这个过程,直到整个序列有序。
基本思想如下:
1. 比较相邻元素:从序列的第一个元素开始,依次比较相邻的两个元素,比较它们的大小。
2. 交换元素位置:如果发现顺序不对(比如前面的元素大于后面的元素),就交换这两个元素的位置,使得较大的元素向后移动。
3. 一轮遍历后的效果: 一轮遍历之后,最大的元素就会移动到序列的末尾。
4. 重复遍历:重复以上步骤,每一轮遍历都会使得未排序部分的最大元素移动到正确的位置。为了确保序列完全有序,需要进行 n-1 轮遍历(n 是序列的长度)。
5. 优化:可以在每一轮遍历中加入一个标志位flag,若一轮遍历中没有发生交换操作,说明序列已经有序,可以提前结束排序。
假设数组的长度为N(有N个元素),则需要比较(N-1)趟,每趟需要比较(N-1-i)对数据
冒泡排序是一种简单但效率较低的排序算法,其时间复杂度为O(n^2),其中n是序列的长度。在实际应用中,在大规模数据排序时,更常使用效率更高的排序算法,如快速排序、归并排序等。
#include
#include
void bubblesort(char* arr, int len)
{
int temp, 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;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (flag == false)
{
break;
}
}
for (int j = 0; j < len; j++)
{
printf("%5d", 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;
}
二分查找(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
#include
#include
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
#include
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 是序列的长度。相比于线性搜索,二分查找通常更快,尤其在大型有序序列中。但要注意,二分查找要求序列是有序的,如果序列无序,需要先进行排序。 因此,二分查找算法的使用前提是数据完全有序,二分查找常常作为笔试题的变形考察,需要重点掌握。