在学习了循环之后,我们来学习一个数据结构,当然,它很简单()
一维数组
数组的创建
我们先来看一个需求。如果你想要存储一些 int
变量,你会怎么做?
如果个数少的话,我们可以选择 int a, b, ...;
。
但是如果变量很多(比如有 $10^5$ 个),这样定义是永无止境的,所以我们应该如何解决呢?于是,我们引入一种数据类型——数组。
int a[114514];
我们使用中括号 [ ]
来定义一个数组。其中,int
代表这个数组中存储的变量类型为整数,$a$ 是数组的名字,$114514$ 是数组的大小(也就是里边包含多少个变量,我们将数组里的变量称为元素)。
类似地,如果我们想要大量存储其他类型的变量,也可以使用数组。例如,我们可以使用 double b[10];
创建一个大小为 $10$ 的浮点数数组 $b$。
如果我们想要获取数组中的元素,我们同样使用中括号 [ ]
,使用 varname[index]
。其中,varname
是数组的名称,index
是这个元素在数组中的位置(也就是数组的第几个元素),被称为下标。这样获取数组中元素的行为被称为访问。
值得注意的是,数组的下标从 $0$ 开始,也就是说,一个大小为 $N$ 的数组的下标为 $0\sim N-1$。当我们访问的位置超出这个范围时(比如访问负数或者 $\ge N$ 的下标),就会出现一些奇奇怪怪的错误,这样的行为被称为数组越界。
在 OI 中,为了方便,我们一般会将数组的大小开的比数据范围略大一些,舍弃下标为 $0$ 的位置。
数组的初始化
虽然我们已经创建了一个数组,但是如果数组是在函数(比如主函数)中创建的,直接访问这个数组将会得到一些奇奇怪怪的结果,比如:
int a[5];
for (int i = 0; i < 5; ++i) cout << a[i] << endl;
一种可能得到的结果如下:
4283280
1
1876947504
1
-2113433532
可以发现,这似乎与我们的预期相差甚远。这是因为,当我们访问一个未初始化的数组时,计算机并不知道我们要访问的元素的值时多少,于是就可能会随机给出一个值。
对于上述操作,甚至在不同的时间或不同的电脑上得到的结果也可能不相同。这样不知道会有什么结果的行为被称为未定义行为,也常简写为 UB(Undefined Behavior),比如把答题卡的定位点之间的空隙全都涂黑就是一种 UB。我们要在代码中尽可能避免 UB,以避免出现各种奇奇怪怪的错误。
此时,我们就需要对这个数组进行初始化,也就是给它赋一个初值。
我们可以在创建数组时,将我们要初始化的值放在大括号 { }
内,并将其赋值给数组来进行初始化。例如:
int a[5] = {0};
for (int i = 1; i < 5; ++i) cout << a[i] << endl;
0
0
0
0
0
可以发现,此时数组的每个元素就都有了初值。那么如果我们在大括号里写 $1$,数组会被全部初始化为 $1$ 吗?
答案是否定的。这样做只会给数组最开头的元素初始化为 $1$,而其他元素仍然为 $0$。也就是说,大括号中可以初始化的是数组的前几个值,剩下的会初始化为 $0$。例如下面这段代码和输出:
int a[5] = {114, 514, 1919810};
for (int i = 1; i < 5; ++i) cout << a[i] << endl;
114
514
1919810
0
0
当然,初始化的过程中,同样要避免数组越界。
数组的应用
我们来看一个实例:
向数组 $a$ 中输入 $n$ 个整数,$n \le 10^5$。
由于 $n$ 很大,我们很难将 $n$ 个数字一个个手动输入,但可以发现的事,程序是在做同样的事情——「输入」。这个时候,就要联系到前几周学过的循环结构上来了,使用循环输入即可很好的解决这一问题。
int a[100010]; // 稍微开大一点,防止越界
for (int i = 1; i <= n; ++i) cin >> a[i];
求数组中的 $n$ 个元素的和。
int sum = 0;
for (int i = 1; i <= n; ++i) sum += a[i];
多维数组
如果我们想存储一个矩阵,又该如何操作呢?于是我们引入了二维数组。
typename varname[N][M];
typename
指数组的数据类型。这段代码表示我们创建了一个 $N\times M$ 的二维数组,也就存储了 $N \times M$ 个整数。二维数组的本质是将一个数组的下标指向另一个数组,也就是「数组的数组」。
类比一维数组,当我们想访问第 $x$ 行第 $y$ 列的数,可以使用 varname[x][y]
进行操作。
以此拓展,还可以有三维数组、四维数组……
给出 $n$ 行,每行 $m$ 个整数,向二维数组中输入。
值得注意的是,尽管我们学过循环,但只写一层循环,或是只有一个循环变量,似乎怎么也实现不了。
这个时候,我们就需要使用循环嵌套了。我们将两层循环嵌套在一起,外层循环用于读入一整行的内容,一共执行 $n$ 次,内层循环用于读入每行的每一个元素,一共执行 $m$ 次。代码如下:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) cin >> a[i][j];
当然,循环也是可以无限嵌套的,因此,更高维度的数组也可以类比上述操作。
数据范围与空间
不知道各位在做题的时候是否注意到,每道题都有一个的时间限制与空间限制,当然,并不知道这是什么意思。
计算机的存储单位
从学过数组开始,我们经常需要存储大量的数据。然而,计算机存储数据也是需要花费代价的,也就是占用存储空间。此时,题目给出的空间限制就变成了我们需要考虑的东西——我们需要保证存储的这些数据不超过空间限制。
具体地说,计算机存储的最小单位是位(bit, b),一位只可以表示 $0$ 和 $1$ 两个值,也就构成了一个二进制位。
每 $8$ 位就可以构成一个字节(Byte, B),接下来以 $1024$(也就是 $2^{10}$)为进制,依次有千字节(Kibibyte, KiB)、兆字节(Mebibyte, MiB)、吉字节(Gibibyte, GiB)等等。也就是说,它们之间有如下的关系:
1B = 8b,1KiB = 1024B,1 MiB = 1024KiB,1GiB = 1024MiB……
值得注意的是,我们平时经常看到 KB、MB、GB 等写法,在大多数时候它们指的就是我们刚才提到的 KiB、MiB、GiB 等,但事实上这是一套以 $1000$ 为进制的存储单位,这两种写法完全是两种体系,请各位不要混淆。
数据类型的空间与范围
了解计算机存储的计算方式后,接下来我们给出常用的一些数据类型的占用空间。除去我们之前介绍的几种,还会介绍更多的数据类型,方便解决更多题目。
数据类型 | 范围 | 近似范围 | 空间 |
---|---|---|---|
布尔 bool | $[0, 1]$ | $[0, 1]$ | 1B |
字符 char | $[-2^7, 2^7 - 1]$ | $[-128, 127]$ | 1B |
整数 int | $[-2^{31}, 2^{31} - 1]$ | $[-2 \times 10^9, 2 \times 10^9]$ | 4B |
无符号整数 unsigned int | $[0, 2^{32} - 1]$ | $[0, 4 \times 10^9]$ | 4B |
64 位整数 long long | $[-2^{63}, 2^{63} - 1]$ | $[-9 \times 10^{18}, 9 \times 10^{18}]$ | 8B |
无符号 64 位整数 unsigned long long | $[0, 2^{64} - 1]$ | $[0, 1.8 \times 10^{19}]$ | 8B |
双精度浮点数 double | $[-1.8 \times 10^{308}, 1.8 \times 10^{308}]$, 精度 $15 \sim 16$ 位 | 8B | |
扩展精度浮点数 long double | $[-1.2 \times 10^{4932}, 1.2 \times 10^{4932}]$, 精度 $18 \sim 19$ 位 | 16B |
另外,除了空间,每种数据类型可以表示的数据的范围也都不一样,我们也给出了常用数据类型的范围,今后做题就需要同时考虑数据范围和空间限制了。
数组是我们接触的第一个数据结构,也是之后学习绝大多数数据结构的基础,请各位熟练掌握,并着重理解与循环的关系。