• 编程
  • 信奥中关于数组使用需要注意的几点讨论

  • @ 2023-12-4 22:52:43
//以下为信奥中标准的数组定义模式 
#include <bits/stdc++.h>
using namespace std;
const int N=1e6;//定义int类型常量N,N取值大小为题目中最大需求范围,1e6=10^6;
int a[N+5];	//定义数组在全局变量区域,而且适当多一点,防止数组访问溢出。
//数组或者变量定义在这里,编译时候会默认初始化为0; 
int main() {
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		a[i]=i;
	}
	cout<<a[n];
	return 0;
}

先上标准模板代码,然后我们详细说明几点对于信奥初学者学习数组经常容易犯错的地方:

1、数组定义初始化的补充说明

一般情况下数组初始化写法(推荐用这种标准写法):

int a[6]={0,1,2,3,4,5};

上述数组定义[ ]内可以省略,系统自动根据后面实际数字的个数分配内存空间;

在新版本C++标准中"="也可省略,所以上述数组也可以定义为:

int a[]{0,1,2,3,4,5};
int a[5]{0};//全部初始化为0;

以上省略写法这里写出来,仅供了解即可,好处无非少打两个字符而已,但万一有些比赛不支持反而得不偿失,所以我还是要求大家用标准写法

2、数组赋值

正如第1点说明,数组只可以初始化时候使用"=" 号赋初值,在运行时候只能对单个元素进行赋值,但不能对整个数组使用"=" 赋值 。

#include <bits/stdc++.h>
using namespace std;
const int N=1e6;
int a[N+5],b[N+5];
int main() {
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		a[i]=i;
	}
	//b=a;这样写是不允许的;
	//只能采用循环单个赋值
	for(int i=1;i<=n;i++) {
		b[i]=a[i];
	}
	//或者用 memcpy(b,a,sizeof(b)) ;
	for(int i=1;i<=n;i++){
		cout<<b[i]<<" ";
	}
	return 0;
}

3、数组定义不要用变量限定范围;

对于信奥初学者经常会这样写代码:

#include <bits/stdc++.h>
using namespace std;

int main() {
	int n;
	cin>>n;
	int a[n];//不建议这样定义
	for(int i=0;i<n;i++)
		a[i]=i;
	for(int i=0;i<n;i++)
		cout<<a[i]<<" ";
	return 0;
}

这样写代码一般情况下也能正常编译,运行也不会报错,大部分情况下提交OJ系统好像也都AC了,貌似没有错误,

但是在C++标准中不允许使用变量作为数组范围定义内容,一定要使用常量来定义数组范围。

因为用变量定义数组长度,由于在编译和输入前,变量n是不确定的,那么这样可能会造成一些不可预知的内存访问错误。

所以正确的定义数组范围是根据题目给定的要求,尽量定义为一个能满足题意的最大范围的常数; 为了防止某些意外情况下数组溢出错误,所以一般数组定义范围会适当比实际需要的大一点。

例如题目要求最多存储N个数,N=106N=10^6,那么我们定义数组为(N+xN+x),xx为一个较小的正整数,例如: int a[N+5],b[1000010];

4、数组定义在全局变量位置还是mai() 里面?

在不同系统和编译器的情况下,结果会有不同,例如在win系统,请你用Dev-c++编译运行以下两段代码试试:

#include <bits/stdc++.h>
using namespace std;
int a[1000005];//数组定义为全局变量	
int main() {
	for(int i=1;i<=1000005;i++){
		a[i]=i;
	}
	cout<<a[100000]<<endl;
	return 0;
}

//以上代码能正确运行,并输出答案100000;
#include <bits/stdc++.h>
using namespace std;
int main() {
	int a[1000005];//数组定义为局部变量	
	for(int i=1;i<=1000005;i++){
		a[i]=i;
	}
	cout<<a[100000]<<endl;
	return 0;
}
// 以上代码不能正确运行,并未输出答案异常退出;

这是因为程序运行时候,电脑操作系统会分配不同的内存区域来运行代码,内存主要分为四个区域:

①:代码区:存放执行代码区域,程序被加载至内存时,该区域运行且期间不可修改。

②:静态区 :也称数据区,存放静态变量和全局变量等东西,内存比较大,在程序加载时就分配好的,程序结束后消失。该区域分配空间一般为几G字节。

③:栈区:操作系统自动分配,存放函数中局部变量值,函数参数值,函数的返回值,系统自动释放,编译器自动完成,内存较小。类似数据结构中的栈,栈的大小一般为几M字节;

④:堆区:亦称动态内存分配,由程序员使用new/delete与malloc/free人为手动分配内存和释放,不由得编译器管,也是比较大的内存容器,程序结束后,若没有手动释放,系统会自动回收。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

上述第一部分代码数组定义在main()上面为全局变量,数组存储在②静态区域,内存空间足够使用,所以能正常运行。

第二段代码,数组定义在main()函数中,运行时候被存储在③栈区域,当数组太大时栈空间不够用,所以这就是为什么上面代码会爆空间闪退的原因。

关于第③:栈区内存分配的大小,可能存在的情况是win系统是2M,Liunx系统是8M,但是在一般信奥竞赛中,程序可使用的栈空间大小和题目要求的内存空间限制一致(CSP竞赛为所占内存总和不超过512M),这也是为什么你在本地Devc++中闪退,提交到OJ系统却可以正常AC的原因)。

综上,一般在信奥竞赛中,大家都会习惯性的把数组和变量定义为全局变量。

5、数组定义的最大范围

我们的数组到底定义多大比较合适?

首先根据题目意思,尽可能估算出数组的最大范围数,但是数组也不是越大越好,信奥中要求所有变量空间总和不能超过题目给定的最大内存范围。

比如csp竞赛中规定最大内存空间为512M512M,那么以int数据类型(每个int型占4Byte,其他数据类型根据定义计算)为例:

$N=512\times1024\times1024 Byte/4 Byte\approx 1.3\times10^8$,

也就是512M512M内存空间,最多存储约N=1.3e8N=1.3e8个int类型的数据,所以:

一维数组最大定义到 int array[N]

二维数组最大定义到 int array[a][b];其中a*b<=N;

如果定义多个数组和变量,那么所有数组和变量的总和不能超过限制空间要求。

其他竞赛一般内存限制为256M,请赛前需要注意规则要求

6、数组访问出界

以下代码是初学者最容易犯的错误。

#include <bits/stdc++.h>
using namespace std;
int a[10];	
int main() {
	for(int i=1;i<=10;i++){
		a[i]=i;
	}
	cout<<a[10] ;
	return 0;
}

上述代码能正常编译运行,也貌似没有错误。

但是其实我们只是定义了a[0]a[0] ~a[9]a[9]1010个位置,并不存在a[10]a[10],这里却访问和修改了a[10]a[10],这就叫数组上界访问溢出。虽然在很多时候好像代码运行不会出错, 但对于程序这是一件很危险的事情,因为a[10]a[10] 那部分内存你不知道是谁在用,结果你却改变了他的数值,那就有可能造成系统崩溃等严重错误发生。

上界溢出,一般情况下用采用上面第3点(N+xN+x)的方式来定义,基本上就可以防止上界溢出了。

再看下面这段代码,先不着急往下看,先复制代码去DevC++中运行试试,并看能否找出错误的地方:

#include <bits/stdc++.h>
using namespace std;
int a[10],b[10];
int main() {
	for(int i=0;i<10;i++){
		a[i]=i;
	}
	for(int i=0;i<10;i++){
		b[i]=a[i-1]+a[i];
	}
	for(int i=0;i<10;i++){
		cout<<b[i]<<" ";
	}
	return 0;
}

上述代码,一般情况下也都能正常运行,而且错误非常隐蔽,就是一些老手也经常会一不小心就犯错了。上面的错误就在b[i]=a[i-1]+a[i],当i=0的时候,出现了a[0-1]=a[-1],显然这是数组下出界了,但是编译运行的时候并不会报错,因为实际运行时候会访问a[0]的前面一个位置的内存。所以请各位同学务必记住,任何时候当数组的a[ i-x]内出现“-”的时候,一定要考虑是否会出现负数的情况,请特判之。

7、关于数组的几个标准库函数(STL)

*数组初始化:memset(),

*数组复制:memcpy(),

*数组填充:fill(),

*求最值:max_element()/min_element(),

*数组反转:reverse(),

以上带“*”几个函数功能,基本上可以通过简单的for循环即可实现

#数组排序:sort(),

#二分查找:lower_bound()/upper_bound(),

#第k大值:nth_element(),

#数组循环:rotate();

#全排列 :next_permutation();

以上带“#”的函数,虽然很好用,但是在使用它之前建议你一定要先掌握他们的原理,并能做到自己编程实现。就像你有了汽车这么好的快捷工具,但是你一定要学会奔跑,否则你就是残疾人了。

0 条评论

目前还没有评论...