考虑这样一个问题:读入一些整数,逆序输出到一行中。已知整数不超过100个。如何编写这个程序呢?首先是循环读取输入。读入每个整数以后,应该做些什么呢?思来想去, 在所有整数全部读完之前,似乎没有其他事可做。换句话说,只能把每个数都存下来。那么存放在哪里呢?答案是:数组。
(一)一维数组的定义和引用
定义:数组是有序的元素序列,是有限个类型相同的变量的集合。
1.一维数组的定义
类型标识符 数组名[常量表达式];
类型标识符 | 数组名 | 常量表达式 |
---|---|---|
int float double char bool string… | 必须是合法的标识符,C++语言规定只能由字母或下划线开头,可以由字母或数组组成 | 常量表达式的值即为数组元素的个数,下标从0~n-1。对于传统的C数组,要求是整型常量表达式。 |
如:int num[10];
就定义了一个一维整型数组,数组名为num,数组中的元素为10个,分别为num[0]、num[1] 、num[2]… num[9]。同一数组的所有数组元素在内存中占用一片连续的存储单元。
注:
(1)数组中的变量称为数组元素,数组元素都有下标,数组元素也称下标变量。元素下标从0开始,使用数组时下标不能越界,否则会造成内存的混乱,可导致不可预测的错误。如下面这段程序:
#include<bits/stdc++.h>
using namespace std;
int main() {
int a[10];
for(int i=1;i<15;i++){
a[i]=0;
cout<<i;
}
return 0;
}
(2)数组中的每个元素都属于同一类型,数组元素可以是基本数据类型或是构造类型。如:
float score[100];
bool sex[100];
string name[100];
(3)用方括号括起来的常量表达式的值是一个非负整数,表示数组元素的个数即数组长度,如下面的写法是合法的:
int a[10];//a[0]是第一个元素,a[9]是最后一个元素
char sum[10000+5];
int n=100;
int sum[n*2];
(4)局部数组默认值为野值,全局数组默认值为0。大数组须定义为全局数组,在局部定义大数组会爆栈(详见文档最后解析)
(5)根据2022年CSP比赛内存512M,我们可以算出可定义的不同类型的最大数组大小:
512
M
=
512
∗
1024
∗
1024
B
y
t
e
=
536870912
c
h
a
r
=
134217728
i
n
t
=
67108864
d
o
u
b
l
e
512 M = 512*1024*1024 Byte = 536870912 char= 134217728 int=67108864 double
512M=512∗1024∗1024Byte=536870912char=134217728int=67108864double
2.一维数组元素的引用
数组与变量一样必须先定义,然后才可使用。只能逐个引用数组元素的值,而不能一次引用整个数组中全部元素的值。
数组元素的表示形式为:
数组名[下标]
注意:下标的值必须为整型,值从0开始,最大值为数组长度减1。
例如,可以像下面这样引用:
a[0]、sum[i]、a[b[i]]、score[i+1]
当然表达式i、b[i]、i+1的值必须在对应的数组下标范围之内。
如果int a[100],b[100];
表达式“a>b"在C++中是不支持的,C++中不能一次引用整个数组;但可以比较a[2]和b[2]的大小,我们可以写成:a[2] >b[2]。如执行这样的语句:“cout <<a[100]<<endl;”,虽然我们没有定义a[100]元素,但C++不会检测下标越界问题,所以能通过编译,运行时会导致不可预料的结果。这种错误我们在编程时要注意避免。
3.一维数组的存锗
数组定义后,对应着一块连续的内存单元,其地址与容量在程运行后到程序结束前保持不变,数组在计算机内存单元中是连续存储的。例如,定义数组bool vis[20];
如果数组vis的起始地址为P,由于bool类型占用1个字节,则vis[1]的起始地位为P+1,vs[2]的起始地位为P+2…,vs[19]的起始地位为P+19。如果定义数组int vis[20];
,vis定义为int类型,由于int类型占用4个字节,则vs[1]的起始地址为P+4,vis[2]的起始地址为P+8…,vis[19]的起始地址为 P+76。
数组定义后,就可以计算出整个数组所占的存储空间大小。数组所占用的空间为:单个数组元素所占用的空间(如int类型为4个字节,char类型为1个字节)乘以数组元素个数,这样算出来的存储空间的单位是字节。当然我们也可以直接用函数sizeof(数组名)来求出整个数组所占用的存储空间。如“int a[100];”,可以这样计算: 4 ∗ 100 = 400 字节 4*100=400字节 4∗100=400字节,也可以用 sizeof(a)来计算。
(二)一维数组的赋值
数组定义后,需要给数组元素赋值,我可用下种方法给数组元素赋值
1、定义数组时赋值
C++中,单个变量的值可以在定义时同时赋值,数组也可以在定义时赋值。
表5.2-1定义时给数组元素赋值
语句 | 作用 |
---|---|
int a[1005] | 如果这条语句定义在主函数之上,则数组a所有元素的值赋为0 |
int a[5]={1,2,3,4.5}; | a[0]、a[1]、a[2]、[3]、a[4]的值分别赋为1,2,3,4,5 |
int a[5]={1,2,3}; | a[0]、a[1]、a[2]、[3]、a[4]的值分别赋为1,2,3,0,0 |
int a[5]={0}; | 将数组a中5个元素的值全赋为0 |
int a[]={1,2,3}; | a[0]、a[1]、a[2]的值分别赋为1,2,3,且将数组的长度定义为3 |
2、用函数赋值
C语言的数组并不是“一等公民”,而是“受歧视”的。例如,数组不能够进行赋值操作:
如声明“int a[maxn],b[maxn]”,是不能赋值b=a的。
C++提供的memset、memcpy、fill函数都可以给数组元素赋值。
memset函数按字节对内存块进行初始化,通常用来将数组每个元素的值初始化为0或-1。memcpy函数可以将指定的一段内存地址中储存的内容,拷贝到另一段连续的地址中,所以通常用来将一个数组的值赋给另一个数组。这两个函数使用前都必须引用头文件。fill 函数也可以用来对数组元素赋值,使用前需引用头文件。
语句 | 作用 |
---|---|
memset(a,true,sizeof(a)); | 给bool类型数组 a 每个元索都赋值为true |
memset(a,true,3); | 相当于语句for(int i=0;i<3;i++) a[i]= true; |
memset(ch,97,sizeof(ch)); | 给字符数组ch每个元索赋值为字符’a’ |
memset(a,-1,sizeof(a)); | 即给数组a的每个元素的每个字节赋值为-1的补码为(11111111)2即-1 |
fill(a,a+4,4); | 相当于语句for(int i=0;i<4;i++) a[i]= 4; |
memcpy(b,a,sizeof(int)*k); | 从数组a复制k个元素到数组b;当然,如果数组a和b都是浮点型double,复制时要写成memcpy(b,a,sizeof(double)*k) |
memcpy(b,a,sizeof(a)); | 把数组a全部复制到数组b |
3、用输入语句逐个赋值
例如:
int x[100],y[100];
for(int i=0;i<100;i++)
cin>>x[i];
for(int i=0;i<100;i++)
scanf("%d",&y[i]);
注意:数组元素输出时,也只能逐个输出。如:
int x[100];
for(int i=0;i<100;i++)
cout<< x[i];//用循环语句批量输出数组元素
cout<<x[50]<<endl;//输出单个数组元素
不能写为:cout << x<< endl;
习题1:逆序输出
回到我们上课开始的问题:
读入一些整数,逆序输出到一行中。已知整数不超过100个。
#include<stdio.h>
#define maxn 105//在空间够用的情况下数组一般会声明得稍大一些防止越界
int a[maxn];
int main(){
int x,n=0;
while(scanf("%d",&x)==1) //scanf的返回值为读入元素的个数,以ctrl+z两个字符结束输入
a[n++]=x; //相当于a[n]=x;n++;
for(int i=n-1;i>=1;i--)
printf("%d ",a[i]);
printf("%d\n",a[0]);
return 0;
}
注:现在的题目中一般会说明输入元素的个数,故无需使用scanf("%d",&x)==1
4、在程序中用赋值语句逐个赋值
例如:
int x[100],y[100];
for(int i=0;i<100;i++)
x[i]=i*i;
for(int j=0;j<100;j++)
y[j]=j*j*j;
习题2:斐波那契数列
运行下面程序思考该程序运行后的输出结果。
#include<bits/stdc++.h>
using namespace std;
int a[40]={1,1};//定义时给a[0]、a[1]赋值为1,其他数组元素赋为0
int main(){
int n;
cin>>n;
for(int i=2;i<n;i++)
a[i]=a[i-1]+a[i-2];//用赋值语句给数组元素逐个赋值
for(int i=0;i<n-1;i++) //顺序输出数组中元素的值
cout<<a[i]<<" ";
cout<<a[n-1]<<endl;
return 0;
}
程序输出:1 1 2 3 5 8
【问题分析】
数组opt第1个元素为1,第2个元素为1,从第3个元素开始,每个元素皆为前两个元素之和。所以数组 opt 保存的就是斐波那契数列:1、1、2、3、5、8…
习题3:进制转换
【例2】进制转换
【问题描述】
给定一个十进制正整数N,求其对应的二进制数。
【输入格式】
仅一行,包含一个正整数N。
【输出格式】
共一行,包含一个正整数,表示N对应的二进制数。
【输入样例】
10
【输出样例】
1010
【数据范围】
1≤N≤30000
【问题分析】
十进制整数转换为二进制整数采用“除2倒序取余”法。具体做法是用2除十进整 数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为0时为止,然后把得到的余数倒序输出。
【参考程序】
#include<bits/stdc++.h>
using namespace std;
int n,num[100],t;
int main(){
cin>>n;
while(n){
t++;
num[t]=n%2;//用数组num 保存每次的余数
n/=2;
}
for(int i=t;i>=1;i--)//例序输出数组中元素
cout<<num[i];
cout<<endl;
return 0;
}
课后练习
习题4:走楼梯
【题目描述】
一个楼梯有 n 级,小文同学从下往上走,每一步可以跨一步,也可以跨两步。问:走到第 n 级台阶有多少种走法?
【输入格式】
一个一个整数 n,0<n<=30。
【输出格式】
一行 n 个整数,之间用一个空格分隔,表示走到第1级、第2级…第n级台阶分别有多少种走法。
【输入样例】
2
【输出样例】
1 2
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,f[31];
scanf("%d",&n);
f[1]=1;f[2]=2;
for(int i=3;i<=n;i++)
f[i]=f[i-1]+f[i-2];
for(int i=1;i<n;i++)
cout<<f[i]<<" ";
cout<<f[n];
return 0;
}
习题5:火柴数字
【题目描述】
火柴数字如下图:
现用n根火柴摆数字,请列出所有能摆出的自然数,要求每个数火柴全用上,不多不少。
【输入格式】
一个整数 n,0<n<=10。
【输出格式】
所有能摆出的自然数,用空格分隔
【输入样例】
6
【输出样例】
0 6 9 14 41 77 111
【问题分析】
0~9每根所用的火柴棒数量:
数字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
火柴棒 | 6 | 2 | 5 | 5 | 4 | 5 | 6 | 3 | 7 | 6 |
可以看出:1
最少,用了2根,8
最多,用了7根。不难分析出数字范围是[0,11111]之间。
思考7根火柴棒的范围?[8,711] 18根火柴?
知道了数据范围
,且每个数需使用n根火柴的条件
明确,此时我们可以考虑使用穷举法
:将[0,11111]的每个数的所使用的总火柴数统计,若为n则输出。
算法:通过循环列出 [0,11111] 的每个数,再写一个内层循环判定每个数所使用的的火柴个数(与上面的数字统计类似),整体上是一个双层循环。外层从0取到11111,内层把每个数的每一位所使用火柴数的和算出来,0~9十个数字的火柴数可以用数组来表示。参考程序如下:
#include<bits/stdc++.h>
using namespace std;
int a[]={6,2,5,5,4,5,6,3,7,6};//0~9分别需要多少根火柴棒
int main(){
int n;
cin>>n;
if(n==6)
cout<<0<<" ";
for(int i=1;i<=11111;i++){//穷举范围
int s=0; //统计总数目
int ti=i; //因i会在内层循环时发生改变,在此对i进行保存以便后续使用
while(ti){ //计算ti需要多少根火柴棒,ti非0才会执行循环,所以0不在计算范围
s=s+a[ti%10]; //个位所用的数量
ti=ti/10; //整除10,除去个位数
}
if(s==n)
cout<<i<<" ";
}
return 0;
}
(三)一维数组的查询、统计
1、查询
顺序查找:本质即从数组的第一个位置找到最后一个位置,判断是否有所需特定数值x。
【参考代码】
for(int i=1;i<=n;i++)
if(a[i]==x){
ans=i;
break;
}
习题6:二分查找
如果数组中元素是有序的(从小到大或从大到小),我们可以用二分查找来提高效率。
二分查找:假定数组有小到大排列,首先将数组中间位置上的元素与x进行比较,如果x大于中间位置元素,则在数组右半部分继续进行二分查找;否则在左半部分继续二分查找。
【参考代码】
int ans=-1;//假定数组中没有x这个数
int l=1,r=n;//l和r表示查找范围,初始范围为数组1~n元素
while(l<=r){//有区域可查找
int mid=(l+r)/2;
if(a[mid]==x){//找到x
ans=mid;
break;
}
if(g<a[mid])
r=mid-1;//在左半部分查找
else
l=mid+1;//在右半部分查找
}
还可以通过C++中的函数方便地实现查找
lower_bound(begin,end,num)
upper_bound(begin,end,num)
binary_search(begin,end,num)
2、统计
通过访问数组每一个元素,统计某一个值或数组元素符合某一条件的次数
桶排序
当数组元素的值范围较小时我们可以使用桶排序。
方法:定义一个数组a,用a[i]表示值为i的元素有多少个。输出时依次访问a[1]到a[n],当a[i]不为0时输出a[i]个i,从而实现排序功能。
习题7:桶排序
for(int i=1;i<=n;i++){
cin>>x;
a[x]++;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=a[i];j++)
cout<<i<<" ";
(四)一维数组元素的移动
1、数组部分元素整体左移
习题8:删除元素
【输入样例】
10
100 200 150 140 129 134 167 198 200 110
【输出样例】
100 150 140 129 134 167 198 110
【样例解释】
删除最大值
【参考程序】
#include<bits/stdc++.h>
using namespace std;
int n,a[10000],m,cnt;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
m=max(m,a[i]);
}
for(int i=1;i<=n;i++){
while(a[i]==m){
for(int j=i;j<=n;j++)
a[j]=a[j+1];//从i位开始,用后一个元素值覆盖前一个元素的值,相当于删除第i个元素
cnt++;//删除后数组长度缩短,需记缩短的长度,在输出时调整数组长度
}
}
for(int i=1;i<=n-cnt;i++)//注意数组最后两个元素的值完全相同
cout<<a[i]<<" ";
return 0;
}
注意:我们在编程时也可以将要删除的元素设置一个删除标志(通常赋值为一个特殊的数),访问时遇到标志则跳过,就不需要左移数组删除元素了。
【参考代码】
#include<bits/stdc++.h>
using namespace std;
int n,a[10000],m;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
m=max(m,a[i]);
}
for(int i=1;i<=n;i++){
if(a[i]==m){
a[i]=-1;
}
}
for(int i=1;i<=n;i++)
if(a[i]!=-1)
cout<<a[i]<<" ";
return 0;
}
2、数组部分元素整体右移
习题9:插入元素
【输入样例】
10
100 200 150 140 129 134 167 198 200 110
3 120
【输出样例】
100 200 120 150 140 129 134 167 198 200 110
【样例解释】
在第3个位置插入120
【参考程序】
#include<bits/stdc++.h>
using namespace std;
int n,a[10000],b,c;
int main(){
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
cin>>b>>c;
for(int i=n;i>=b;i--)
a[i+1]=a[i];
a[b]=c;//在b处插入c
n++;//修改数组长度
for(int i=1;i<=n;i++)
cout<<a[i]<<" ";
return 0;
}
习题10:插入排序
根据数组下标由小到大依次给数组赋值时,第一个元素直接赋值;给后面的元素x赋值时首先找到第一个比它大的元素y的位置,从y起所有元素右移一位,将x插入到原来y的位置。
【输入样例】
10
100 200 150 140 129 134 167 198 200 110
【输出样例】
100 110 129 134 140 150 167 198 200 200
#include<bits/stdc++.h>
using namespace std;
int n,j,a[10000],b,c;
int main(){
cin>>n;
cin>>a[1];
for(int i=2;i<=n;i++){
cin>>a[0];//输入下一个元素准备插入
for(j=1;j<i;j++)//从前往后找到第一个比a[0]大的元素,位置存在j中
if(a[j]>=a[0])
break;
for(int k=i;k>=j;k--)//从i到j位置的元素右移一位
a[k+1]=a[k];
a[j]=a[0];//插入新的元素
}
for(int i=1;i<=n;i++)
cout<<a[i]<<" ";
return 0;
}
3、两个数组元素相互移动
swap(a,b)
可以实现两个变量值的交换
(1)选择排序
有数组int a={5,2,4,3,1}
,现对其进行从小到大排序:
第1轮:用a[0]与a[1]~a[4]进行比较,如a[0]大于后面的元素则swap交换
第2轮:用a[1]与a[2]~a[4]进行比较,如a[1]大于后面的元素则swap交换
第3轮:用a[2]与a[3]~a[4]进行比较,如a[2]大于后面的元素则swap交换
第4轮:用a[3]与a[4]进行比较,如a[3]大于后面的元素则swap交换
则代码如下:
习题11:选择排序
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[5]={5,2,4,3,1};
for(int i=0;i<4;i++){ //i除了表示比较的轮次,亦可以表示“擂主”
for(int j=i+1;j<5;j++){ //j除了表示比较每一轮比较的次数,亦可以表示“打擂者”
if(a[i]>a[j])
swap(a[i],a[j]);
}
}
for(int i=0;i<5;i++)
cout << a[i] << " ";
return 0;
}
(2)冒泡排序
有数组int a={5,2,4,3,1}
,现对其进行从小到大排序:
第1轮:用a[4]与a[3]、a[3]与a[2]、a[2]与a[1]、a[1]与a[0]进行比较,如后者小于前者则swap交换
第2轮:用a[4]与a[3]、a[3]与a[2]、a[2]与a[1]进行比较,如后者小于前者则swap交换
第3轮:用a[4]与a[3]、a[3]与a[2]进行比较,如后者小于前者则swap交换
第4轮:用a[4]与a[3]进行比较,如后者小于前者则swap交换
则代码如下:
习题12:冒泡排序
#include <bits/stdc++.h>
using namespace std;
int main()
{
int a[5]={5,2,4,3,1};
for(int i=1;i<5;i++){ //i表示轮次
for(int j=4;j>=i;j--){ //j除了表示比较每一轮比较的次数,亦可以表示“后者”
if(a[j]<a[j-1])
swap(a[j],a[j-1]);
}
}
for(int i=0;i<5;i++)
cout << a[i] << " ";
return 0;
}
快速排序
习题13:约瑟夫问题
n个人围成一圈,初始编号从1~n排列,从约定编号为1的人开始报数,数到第m个人出圈,接着又从1开始报数,报到第m个数的人又退出圈,以此类推,最后圈内只剩下一个人,这个人就是赢家,求出赢家的编号。
数组模拟
模拟思路简单,但是编码却没那么简单,临界条件特别多,每次遍历到数组最后一个元素的时候,还得重新设置下标为 0,并且遍历的时候还得判断该元素时候是否是 -1。用这种数组的方式做,千万不要觉得很简单,编码这个过程还是挺考验人的。
这种做法的时间复杂度是 O(n * m), 空间复杂度是 O(n);
#include<algorithm>
#include<iostream>
using namespace std;
int main(){
int a[1001]={0}; //初始化化数组作为环
int n,m;//n代表总的人数,m代表报数到几退出
cin>>n>>m;
int count=0;//记录退出的个数
int k=-1;//这里假定开始为第一个人,下标为0,编号为1,如需从编号x开始,则k=x-2
while(count<n-1){ //总共需要退出n-1个人
int i=0;//记录当前报数编号
while(i<m){
k=(k+1)%n; //循环处理下标
if(a[k]==0){
i++;
if(i==m){
a[k]=-1;
count++;
}
}
}
}
for(int i=0;i<n;i++){
if(a[i]==0){
printf("%d\n",i+1);
break;
}
}
return 0;
}
课后练习:
习题14:陶陶摘苹果(NOIP2005普及组)
习题15:校门外的树(NOIP2005普及组)
习题16:开灯
习题17:质因子分解
(附)数组放在main函数内外的区别
区别一:函数内数组初始值为野值,全局数组初始值为0。
区别二:
我们首先来看一个问题
【问题描述】
给定数列 1, 1, 1, 3, 5, 9, 17, …,从第 4 项开始,每项都是前 3 项的和。求第 20221102 项的最后4位数字。
显然,这题目思路明确清晰,就是不断计算然后对10000进行模运算得到最后4位整数
【错误代码】
#include<iostream>
using namespace std;
const int MOD=10000;
int main(){
int a[20221110]={0,1,1,1};//a[0]为0,使得下标与序号一致
for(int i=4;i<=20221102;i++){
a[i]=(a[i-3]+a[i-2]+a[i-1])%MOD;
}
cout<<a[20221102];
return 0;
运行后会报错,原因是:
大数组不能放在main函数里面,要定义在main函数外面成为全局变量!
【正确代码】
#include<iostream>
using namespace std;
const int MOD=10000;
int a[20221110]={0,1,1,1};//a[0]为0,使得下标与序号一致
int main(){
for(int i=4;i<=20221102;i++){
a[i]=(a[i-3]+a[i-2]+a[i-1])%MOD;
}
cout<<a[20221102];
return 0;
}
为什么大数组一定要放在main函数外面而不能放在里面呢?
原因在于开设数组的区域不同,在运行代码的时候,操作系统会分配不同的内存区域来运行代码
栈区:由操作系统自动分配释放,存放函数的参数值,局部变量的值,不需要时系统会自动清除,内存较小
堆区:由new分配的内存块,也就是说在代码中new一个数组,内存由堆区分配;堆区不由编译器管,由应用程序控制,相当于程序员控制。如果程序员没有释放掉,程序结束后,操作系统会自动回收
数据区:也称全局区或者静态区,存放全局的东西,比如全局变量,内存较大
代码区:存放执行代码的地方
简而言之,在main函数外面开设一个数组,它的内存分配在数据区里;而如果在main函数内部开设一个数组,它的内存分配在栈区内。一般来说栈区的内存是比较小的,所以平常开一些小一点的数组是完全没问题的;但如果题目要求的数组比较大,那就会出现爆满溢出的情况,程序将无法访问内存而出错;相反,数据区的内存较大,就不会出现这样的问题。这就是为什么开设大数组一定要放在main函数之外的原因。
约瑟夫问题
#include<bits/stdc++.h>
using namespace std;
int main(){
int m,n,i=0,j=0,t;
bool p[100];
cin>>m>>n;
for(int i=0;i<100;i++) p[i]=true;
t=m;
while(t>0){
i++;
if(i==m+1) i=1;//实现圈的效果
if(p[i]){
j++;
if(j==n){
cout<<i<<endl;
p[i]=false;
j=0;
t--;
}
}
}
return 0;
}