数组

1、如何存储 n 个数

输入 n 个数,把这 n 个数按某种顺序输出。

#include <cstdio>
int main() {
	//3 个数按序输出
	int a, b, c;
	scanf("%d%d%d", &a, &b, &c);
	printf("%d %d %d", a, b, c);
	return 0;
}

当输入的数据量非常巨大时(比如:几百万),我们是无法挨个进行变量命名的。这个时候,我们需要给这一组数整体命名,然后具体每个数,给排一个编号即可。例如:当需要输入 100 位同学的语文成绩时,我们可以给这一组数起一个统一的名字,叫 score。具体的每位同学的成绩,根据班级序号进行排列即可,若小郁同学在班里的序号为 39,他的语文成绩为 98 分,则 score[39] = 98;

score[39] = 98;//score就叫做数组,score[39]代表小郁同学的语文分数
score[22] = 99;//score[22]代表小孟同学的语文分数

2、数组的声明

和变量一样,一个数组的所有元素都只能是一个相同的数据类型,比如,都为 int,或者都为 char。声明的时候,在名字后面要用一对中括号,里面填写这个数组要包含的数据个数。

int score[100];//声明一个语文分数的数组,共可以存储 100 个数据,编号为 0 ~ 99

注意:编号是从 0 开始的,最大编号比声明的数据个数要少 1。

类似的,我们还可以声明其他类型的数组

//声明了一个可以存储 55 个 double 类型数据的数组,编号为 0 ~ 54
double len[55];
//声明了一个可以存储 70 个 char 类型数据的数组,编号为 0 ~ 69
char ch[70];

多维数组在内存中存储时,根据不同的处理器架构,分为行优先存储列优先存储两种方式:

  • 相邻两个元素是同一行的,称为“行优先”;
  • 相邻两个元素是同一列的,称为“列优先”。

我们日常使用的电脑,均为行优先存储

上图中,红色点为第一维,绿色点为第二维,蓝色点为第三维。

3、数组的输入、输出

数组由于数据量通常巨大,因此需要借助 for 循环来完成输入、输出,以及其他对数组中某些元素的处理任务。

#include <cstdio>
int main() {
	//10 个数按序输出
	int a[10];
	//使用数组,我们可以借助循环变量 i,来对数组的第 i 个数进行输入、输出及其他操作
	for (int i = 0; i < 10; ++i)
		scanf("%d", &a[i]);
	for (int i = 0; i < 10; ++i)
		printf("%d ", a[i]);
	return 0;
}

通常,题目中要求输入的数量都是万级为单位的,这时候,我们需要把数组声明到 main 函数的外面,作为“全局变量”。“全局变量”才可以声明 50 万 以上级别的数组,最大不要超过 2000万。

#include <cstdio>
//const int 是声明一个不能改变的常量 N,它在这个程序里永远都是 10 万
const int N = 1e5;
int a[N];
int main() {
	//先输入一个正整数 n,接着连续输入 n 个数
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; ++i)
		scanf("%d", &a[i]);
	//将这 n 个数按输入顺序依次输出
	for (int i = 0; i < n; ++i)
		printf("%d ", a[i]);
	return 0;
}

数组名

在定义数组之后,根据数组中元素的类型及个数,在内存中分配一段连续存储单元用于存放数组中的各个元素。

数组定义后,数组名表示该数组所分配连续内存空间中第一个单元的地址,即首地址

数组定义后,数组名的值是一个地址,不可以被修改

数组定义后,只能引用单个的数组元素,而不能一次引用整个数组,数组名单独出现的时候,仅仅代表首地址。

1e5 就是\(1\times {10^5}\)

练习:double 类型数组练习

与 int 型数组一样,不论数组类型是什么样的,下标始终是整型数。

#include <cstdio>
const int N = 1e3;
double a[N];
int main() {
	//先输入一个正整数 n,接着连续输入 n 个数
	int n;
	scanf("%d", &n);
	for (int i = 0; i < n; ++i)
    	//scanf("%lf", a + i); a数组里的第 i 个元素的地址
		scanf("%lf", &a[i]);
	//将这 n 个数按输入顺序依次输出
	for (int i = 0; i < n; ++i)
		printf("%.3f ", a[i]);
	return 0;
}

两个数组整体赋值函数

  1. memset()函数:逐个字节地给数组赋值,需要 #include <cstring>

  2. fill()函数:逐个元素地给数组赋值,需要 #include <algorithm>

bool vis[107];// 每个元素占 1 个字节

char a[107];// 每个元素占 1 个字节

int b[107];// 每个元素占 4 个字节

double c[107];// 每个元素占 8 个字节

语句 作用
memset(vis, true, sizeof vis); 给数组 vis 每个元素都赋值为 true
memset(vis, true, 3); for (int i = 0; i < 3; ++i) vis[i] = true;
memset(a, 'a', sizeof a); 给数组 a 每个元素都赋值为 'a'
memset(a, -1, sizeof a); 给数组 a 的每个元素的每个字节赋值为 -1(-1的补码为 \(11111111_2\)
memset(b, 0, sizeof b); 给数组 a 的每个元素的每个字节赋值为 0
memset(b, 0x7F, sizeof b); 给数组 a 的每个元素赋值约为21亿,表示正无穷 INF,不可用于加法,会溢出
memset(b, 0x3F, sizeof b); 给数组 a 的每个元素赋值约为10亿,也表示正无穷 INF,可用于加法,不会溢出
memset(b, 0x80, sizeof b); 给数组 a 的每个元素赋值约为-21亿,表示负无穷 INF,不可用于加法,会溢出
memset(b, 0xC0, sizeof b); 给数组 a 的每个元素赋值约为-10亿,也表示负无穷 INF,可用于加法,不会溢出
memset(c, 0x7F, sizeof c); 给数组 a 的每个元素赋值约为\(10^{306}\),表示正无穷 INF,可用于加法,不会溢出
memset(c, 0xFE, sizeof c); 给数组 a 的每个元素赋值约为\(-10^{303}\),表示负无穷 INF,可用于加法,不会溢出
fill(a, a+10, 4); for (int i = 0; i < 10; ++i) a[i] = 4;

全局变量、static变量与局部变量

4、标记数组(桶)的应用

在有些题目中,我们需要对一些状态进行标记,我们称之为“标记数组”。

用数组中每个下标的位置当作一个桶标记,用来存储与下标号对应含义的数据。

  • 应用1:

    标记一个数是否出现,v[2]=1 表示标记 2 在数列中出现过,v[5]=0标记 5 没在数列之中;

  • 应用2:

    标记一个数出现的次数,v[2]=6表示 2 出现了 6 次。(常用)

  • 应用3:

    标记一个数的前后关系,例如:nx[i] 表示 i 后面出现的数的下标。

注意:

  • 作为下标的数字必须是非负整数!
  • 下标是数列中的数字,而非它们的位置!
  • 标记数组的大小取决于数列中的数值的取值范围,而不是数列中的数字个数!

练习:校门外的树

点击查看代码
#include <stdio.h>
int a[10007];
int main () {
	int L, M, ct = 0, s, t, i;
	scanf("%d%d", &L, &M);
	while (M--) {// 循环M次
		scanf("%d%d", &s, &t);
		// i从s变到t,每次加1
		for (i = s; i <= t; ++i) {
			a[i] = 1;//标记第i个位置的树被移走了
		}
	}
	for (i = 0; i <= L; ++i) {
		if (a[i] == 0) {// 如果第i个位置的树没被移走,ct加1
			++ct;
		}
	}
	printf("%d", ct);
	return 0;
}

练习:有趣的跳跃

点击查看代码
#include <stdio.h>
#include <math.h>
int a[3007];
int main () {
	int n, last, x, i, d;// diff
	scanf("%d%d", &n, &last);
	for (i = 2; i <= n; ++i) {
		scanf("%d", &x);
		d = abs(x - last);
		if (!d || d>=n || a[d]) {
			puts("Not jolly");
			return 0;
		}
		a[d] = 1;
		last = x;
	}
	puts("Jolly");
	return 0;
}

练习:初级桶排序

点击查看代码
#include <stdio.h>
int a[10007];
int main () {
	int n, i, x, mn = 10000, mx = 0;
	scanf("%d", &n);
	for (i = 1; i <= n; ++i) {
		// 标记数组的输入方式
		// 输入的数值作为下标使用
		scanf("%d", &x);
		if (mn > x) mn = x;
		if (mx < x) mx = x;
		a[x] = 1;
	}
	// 标记数组统计结果时,循环的方式不是1~n
	// 而是整个数列的取值范围
	for (i = mn; i <= mx; ++i) {
		if (a[i]) {
			// 标记数组的下标才是数列的值,而不是a[i]
			printf("%d ", i);
		}
	}
	return 0;
}

前缀和

已知给定的 n 个整数,求任意的第 x 个数到第 y 个数的和。

例如:

第一行输入两个整数 \(n\)(输入的数的个数:\(1\le n \le 10^6\)),\(m\)(询问次数,\(1\le m \le 10^6\))

第二行连续输入 \(n\) 个数(所有的数均在 \(int\) 范围内)

接下来 \(m\) 行,每行输入两个整数 \(x\)\(y\),代表一次询问,第 x 个数到第 y 个数的和。

5 3
9 1 2 0 6
2 4
1 3
2 5
3
12
9

声明一个前缀和数组 s[N]

在输入 a[i] 的同时,计算前缀和 s[i] = s[i-1] + a[i];

求第 x 个元素到第 y 个元素的区间和:s[y] - s[x-1];

#include <cstdio>
const int N = 1e6 + 7;
int a[N], n, m, x, y;
long long s[N];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i);
        s[i] = s[i-1] + a[i];
    }
    for (int i = 1; i <= m; ++i) {
        scanf("%d%d", &x, &y);
        printf("%lld\n", s[y]-s[x-1]);
    }
    return 0;
}

记录的意义在于:可以去掉大量的重复工作,使速度得到巨大的提升!

记录本身不应该占用过大的工作量

6、差分

  • 差分算法介绍

现有一个有 11 个元素的整型数组 d,所有元素都为 0。下面我们来看 2 步操作(从1号下标开始存储数据):

d[0] d[1] d[2] d[3] d[4] d[5] d[6] d[7] d[8] d[9] d[10]
0 0 0 0 0 0 0 0 0 0 0

(1)给 d[3] 加上 2

d[3] = 2;

(2)求 d 数组的前缀和

for (int i = 1; i <= 10; ++i)
    d[i] += d[i-1];

这时候,d 数组变成了这样

d[0] d[1] d[2] d[3] d[4] d[5] d[6] d[7] d[8] d[9] d[10]
0 0 0 2 2 2 2 2 2 2 2

以上两个操作合到一起,实现了将数组中从第3个往后的所有元素均赋值为 2 的效果。

由上可见,一个全 0 的数组,只需要给起点的元素加上一个值,即如同“记录”下——对该起点到终点的所有元素统一加上这个值。不过真正的修改,需要通过求前缀和来实现

注意:数组的初值为 0 非常重要!

那么,如何实现修改一个区间内(不再是到数组最后元素)的值呢?例如,将 d 数组的 [5, 8] 的元素都加上 3,怎么办?

这时候,我们实际上需要做两次刚才的操作:

(1)给 d[5] 加上 3

d[0] d[1] d[2] d[3] d[4] d[5] d[6] d[7] d[8] d[9] d[10]
0 0 0 0 0 3 0 0 0 0 0

(2)给 d[9] 加上 -3

d[0] d[1] d[2] d[3] d[4] d[5] d[6] d[7] d[8] d[9] d[10]
0 0 0 0 0 3 0 0 0 -3 0

(3)求 d 数组的前缀和(前缀和一定要在最后做

d[0] d[1] d[2] d[3] d[4] d[5] d[6] d[7] d[8] d[9] d[10]
0 0 0 0 0 3 3 3 3 0 0

我们做 2 次记录:先将 5~最后 的所有元素都加上 3,再将 9~最后 的所有元素都加上 -3。这样,9~最后 的所有元素 +3 和 -3 就抵消了,相当于没变。最终,我们成功实现了将 [5, 8] 的元素全部加上 3。

练习:差分

如果我们把每个区间都挨个加一遍,那么最多需要 \(10^6\times 10^6=10^{12}\)次操作,就会 \(TLE\)

那么,能否把所有的区间操作合并到一起,只执行一遍呢?当然可以

1. 将每个区间要做的操作快速地记录下来;

2. 统一对区间执行一次求前缀和操作。

以上就是刚才演示的 d 数组的 2 步操作。由于 d 数组要求初值必须为 0,所以,我们不能在原数组上直接进行操作,需要再单独声明一个 d 数组,求完前缀和之后,再将其与原数组对应位置相加即可。

#include <cstdio>
typedef long long LL;//注意取值范围,需要用long long
const int N = 1e6 + 7;
LL a[N], d[N];
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%lld", a+i);
    for (int i = 1, x, y, z; i <= m; ++i) {
        scanf("%d%d%d", &x, &y, &z);
        d[x] += z, d[y+1] -= z;
    }
    for (int i = 1; i <= n; ++i) {
        d[i] += d[i-1];
        printf("%lld ", a[i] + d[i]);
    }
    return 0;
}

7.二维数组

二维数组通常被理解为数组的数组,例如:int a[5][3]; 可以看作是一个由 5 个一维数组(包含 3 个元素的数组)构成的另一个一维数组(包含 5 个元素的数组),其中,每个元素又是一个一维数组。

其中,所有的元素地址按从左到右,从上到下的顺序依次排列。

#include <iostream>
//内部的每个大括号代表对一行元素从左到右依次赋值
//如果元素数不够,后面的元素不赋值
int a[5][3] = {{1, 2}, {3, 4}, {5, 6, 7}, {8}, {9}};
int main() {
	int *p = a[0];
	for (int i = 0; i <= 14; ++i) printf("%d ", p[i]);
	return 0;
}
//1 2 0 3 4 0 5 6 7 8 0 0 9 0 0

(1)int、double等数值类型的输入与输出

直接采用 2 层循环,外层对循环,内层对循环,枚举每个元素 [i][j] 进行赋值。

#include <iostream>
int a[5][3];
int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			scanf("%d", &a[i][j]);//a[i]+j也可以
	//输出需要注意换行和空格
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= m; ++j)
			printf("%d ", a[i][j]);
		puts("");
	}
	return 0;
}

有时候,我们会使用如下的一个技巧:

我们都知道一对双引号内的内容会被看成一个字符串,也就是字符数组,只不过其中的内容是不可更改的。我们在输出的时候,可以利用这个特点。

#include <iostream>
int a[5][3];
int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			scanf("%d", &a[i][j]);//a[i]+j也可以
	//" \n"[0]代表空格," \n"[1]代表回车
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			printf("%d%c", a[i][j], " \n"[j==m]);
	return 0;
}

(2)多行字符串输入

有时候,我们会遇到如下的输入:

输入的第一行包含一个整数 \(n(1\le n\le 1000)\) 和一个字符 \(c\),中间用一个空格隔开;

第二行起,连续输入 \(n\) 行只包含小写字符的长度在 50 以内的字符串。

3 y
abcd
efgh
ijkl
#include <iostream>
using std::cin;
const int N = 1007;
char a[N][57];
int main() {
	int n;
	char c;
	cin >> n >> c;
	for (int i = 1; i <= n; ++i) cin >> a[i];
	//注意在输出的时候,"\n"比endl要快很多
	for (int i = 1; i <= n; ++i) std::cout << a[i] << "\n";
	return 0;
}
#include <iostream>
const int N = 1007;
char a[N][57];
int main() {
	int n;
	char c;
	scanf("%d %c", &n, &c);
	for (int i = 1; i <= n; ++i) scanf("%s", a[i]);
	for (int i = 1; i <= n; ++i) printf("%s\n", a[i]);
	return 0;
}

如果输入格式改成这样:

输入的第一行包含一个整数 \(n(1\le n\le 1000)\) 和一个字符 \(c\),中间用一个空格隔开;

第二行起,连续输入 \(n\) 行只包含小写字符和空格的长度为 \(n\) 的字符串。

5 y
ab cd
e fgh
ijk l
e fgh
ijk l

如果使用 scanf 进行读入,就容易出现问题:

#include <iostream>
const int N = 1007;
char a[N][N];
int main() {
	int n;
	char c;
	scanf("%d %c", &n, &c);
	for (int i = 0; i < n; ++i)
		for (int j = 0; j < n; ++j)
			scanf("%c", a[i] + j);
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < n; ++j)
			printf("%c", a[i][j]);
		puts("");
	}
	return 0;
}
/*
5 y
ab cd
e fgh
ijk l
e fgh

ab c
d
e f
gh
ij
k l
e
 fgh

*/

造成上面结果的原因,正是循环里的 scanf("%c", a[i]+j);。它会把上一行的回车也作为输入存入二维数组之中,从而造成混乱的局面。

解决的方法:把字符作为字符数组输入。

#include <iostream>
const int N = 107;
char a[N][N];
int main() {
	int n, i;
	char c;
	// %c 后面要把作为格式\n输入,方便下面scanf直接输入字符串
	scanf("%d %c\n", &n, &c);
	for (i = 0; i < n; ++i) {
		scanf("%[^\n]", a[i]);
		// 除了最后一行,每行后面都有一个换行符
		if (i < n-1) getchar();
	}
	for (i = 0; i < n; ++i)
		puts(a[i]);
}
#include <iostream>
using namespace std;
const int N = 1007;
char a[N][N];
int main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);//cin,cout加速
	int n;
	char c;
	cin >> n >> c;
	//多加一句cin.getline,将上一行的回车处理掉
	cin.getline(a[0], N);
	for (int i = 0; i < n; ++i)
		cin.getline(a[i], N);
	//注意在输出的时候,"\n"比endl要快很多
	for (int i = 0; i < n; ++i)
		cout << a[i] << "\n";
	return 0;
}
posted @ 2024-07-22 10:31  飞花阁  阅读(406)  评论(0)    收藏  举报
//雪花飘落效果