指针变量与应用——动态数组
在C++中,有一种神奇的变量,它不可以表示一个值,但是可以表示某个元素的地址,通过地址来访问这个元素。
打个比方:你有一张地图和一个坐标,你就可以通过访问这个坐标来达到你访问坐标所表示的元素的目的。指针变量就是这个“坐标”。
下面我们来具体看看指针变量的应用。
1、指针变量的性质
正如上面所说,指针变量不可以表示一个值,但是可以指向别的元素的地址,通过这个地址来间接访问这个元素的值。
由于它的性质,指针变量不可以直接=一个元素,赋值时要注意。
具体操作下面会讲到。
2、指针变量的声明
如何声明一个指针变量? 有如下表达式:
数据类型+“*”+指针名
通常我们这样赋值:
int main() { int *p=NULL; return 0; }
这样我们就定义了一个指针类型的变量p,NULL是空内存,这个内存里什么元素都没有,你可以之后再给p赋一个元素的地址。(可以不用=NULL,但是这是个人习惯,类似于return 0好习惯这种……)
这个语句的意义是:定义一个int类型的指针p,指向空地址。
那么怎么把一个元素的地址赋给一个指针变量呢?
有如下语句:
#include<cstdio> using namespace std; int main() { int a; int *p=NULL; p=&a; return 0; } /*int main() { int a; int *p=&a; return 0; }*/
上面两个主函数的效果是一样的。
我们说说这两段代码的意义:
相信大家都用过scanf( ),在我们输入变量名前要加一个符号“&”,这就是地址符,表示变量名的地址。
我们的指针要指向一个地址,当然就是:指针名=&变量名啦!
3、用指针变量调用元素值
既然我们会赋值了,下一步就是调用元素值,但是指针指向的是一个地址,不能直接参与运算,这时候我们要用到间接运算符“*”。(就是定义的时候那个星号)
如果我们有一个元素a,需要用指针来输出它,怎么操作?
对于这个问题,有如下程序:
#include<cstdio> using namespace std; int main() { int a; scanf("%d",&a); int *p=&a;//定义一个指针变量p指向元素a printf("%d",*p);//间接运算符+指针名表示指针所指元素 return 0; }
代码注释已经很详尽了,我们的指针指向一个元素,用“*”+指针名即可访问指针所指元素
注意:通过指针操作元素值和直接操作元素值是一样的,比如下面这段代码:
#include<cstdio> using namespace std; int main() { int a,b,s,t,*pa=NULL,*pb=NULL; pa=&a,pb=&b; a=10,b=20; s=*pa+*pb; t=*pa**pb; printf("%d %d",s,t); return 0; }
程序给出的结果是30 200。
4、指针的+、-运算
首先我们给出一个基本的定义:
当我们定义数组的时候,系统会给出连续的地址,比如a[5],假设a[0]的地址是0,那么a[1]的地址就是1……以此类推。
此时,我们直接把地址+1(指针+1),就可以访问数组的下一个元素。
#include<cstdio> using namespace std; int main() { int a[5],*p=&a[0]; for(int i=0;i<5;i++) scanf("%d",&a[i]); for(int i=0;i<5;i++) { printf("%d ",*p); p++; } return 0; }
对于p--,同理。这个语句输出了a[ ]中的所有变量。
5、指针与数组
指向数组的指针叫数组指针,众所周知,一个数组的地址是连续的,首地址就是他所占有的几个单元的首地址,我们可以把数组名赋给指针,也可以把数组中某个元素的地址赋给它。
有以下语句:
int a[5],*p=a;
则以下三个语句
&a[0],a,*p,均指向同一个单元——数组的首地址。
那么可以推导:&a[i]、a+i、p+i,均指向数组a中a[i]的地址。
有如下代码:
#include<cstdio> using namespace std; int main() { int a[5],*pa=a; for(int i=0;i<5;i++) scanf("%d",&a[i]); for(int i=0;i<5;i++) printf("%d %d %d %d ",*(pa+i),pa[i],a[i],*(a+i)); return 0; }
我们输如5个数:1 2 3 4 5
系统给出了5行:
1 1 1 1
2 2 2 2
3 3 3 3
4 4 4 4
5 5 5 5
这说明上面4个语句:*(pa+i),pa[i],a[i],*(a+i)是等价的。
代码说明和注意事项:
1、a(数组名)可以加“*”变为常量指针,a是开始元素,根据指针的加减原理,a+i是第i个元素的地址。
2、a是常量名,不能直接进行指针的+、-操作(这里指的是p++、p--这种赋值操作非法,但是a+i这种是合法的),但是pa是指针变量,可以进行加减操作。
6、指针申请系统空间
我们用如下语句可以申请一个系统空间给指针p:
int *p=new(int);
此时*p的内容不确定。
这个语句是动态数组的基础。
7、用指针当数组名
1、原理:之前说过,如果我们一次申请多个空间,系统会发给我们连续的新地址,可以当做数组用。
2、具体操作
有如下代码:
#include<cstdio> using namespace std; int main() { int n,*p; scanf("%d",&n); p=new int[n+1];//申请连续的n+1个空间给指针p for(int i=1;i<=n;i++) scanf("%d",&p[i]); for(int i=1;i<=n;i++) printf("%d ",p[i]); return 0; }
如果我们输入:
5
1 2 3 4 5
系统给出
1 2 3 4 5
上面的代码你可以理解有一个数组,数组名就是指针名,其余操作和第5个板块中提到的一样。(通过数组名+下标访问)
我们还可以改成这个样子:
#include<cstdio> using namespace std; int main() { int n,*p; scanf("%d",&n); p=new int[n+1];//申请连续的n+1个空间给指针a for(int i=1;i<=n;i++) scanf("%d",&p[i]); for(int i=1;i<=n;i++) { p++;//由于p默认指向第0个元素,所以先++ printf("%d ",*p); } return 0; }
这里使用指针访问而不使用数组名访问,和上面的代码是等价的。当然你也可以写成这样:printf("%d ",*(p+i));在上面提到过,这几种写法是等价的。
8、动态数组与空间复杂度优化
前面扯了那么多指针的基本定义和写法,终于到了今天的正题了——利用指针建立动态数组。
我们给出一个情景:现在有一个巨大(行列<=10000000)但是稀疏(大部分元素是0)的矩阵,我们要对这个矩阵进行操作,怎么办呢?
显然,这样的代码是绝对行不通的。
#include<cstdio> #define N 10000100 using namespace std; int n[N][N];
如果这么写,你的空间复杂度是绝对过不了的。
我们要进行优化才行。
记得指针可以申请空间吗?我们可以利用这个特性,避免存储无效数据(0),我们为每一次输入的有效数据开一个新的内存单元,这样就不会爆内存啦!
我们看下面这个例题:
一本通例题8.7:
【问题描述】
矩阵可以认为是N*M的二维数组。现在有一个巨大但稀疏的矩阵。
N,M的范围是1<=N,M<=100000,有K个位置有数据,1<=K<=100000。
矩阵输入的方式是从上到下(第1行到第N行),从左到右(从第1列到第M列)扫描,记录有数据的坐标位置(x,y)和值(v)。这是按照行优先的方式保存数据的。
现在要求按照列优先的数据,即从左到右,从上到下扫描,输出有数据的坐标和位置。
【输入格式】
第1行:3个整数N,M,K,其中1<=N,M,K<=100000;下面有K行,每行三个整数:a,b,c,表示第a行第b列有数据c。数据在int范围内,保证是行优先的次序。
【输出格式】
1行,K个整数,是按照列优先次序输出的数
【样例输入】
4 5 9
1 2 12
1 4 23
2 2 56
2 5 78
3 2 100
3 4 56
4 1 73
4 3 34
4 5 55
【样例输出】
73 12 56 100 34 23 56 78 55
【样例解释】
0 | 12 | 0 | 23 | 0 |
0 | 56 | 0 | 0 | 78 |
0 | 100 | 0 | 56 | 0 |
73 | 0 | 34 | 0 | 55 |
对于这个矩阵,我们可以这样存:
73 | 12 | 34 | 23 | 78 |
—— | 56 | —— | 56 | 55 |
—— | 100 | —— | —— | —— |
—— | —— | —— | —— | —— |
注:标记“------”的都是没有使用的内存,这样我们就节省了11个内存单元,对于大数据的时候,我们还能节省更多的内存,保证不会超出空间限制。
这个思路的大体意思就是:忽略x的值,把第y列第一次输入的数据当做第y列的第一个数据,然后是第二个……
下面来看代码实现:
#include<cstdio> using namespace std; const int LP=100001; int n,m,k; int x[LP],y[LP],d[LP],c[LP];//记录数据,记录第n个数据在x行,y列,c是第y列的数据总数。 int *a[LP];//最多有LP列,所以我们开LP长度的指针数组 int main() { scanf("%d%d%d",&n,&m,&k); for(int i=1;i<=k;i++) { scanf("%d%d%d",&x[i],&y[i],&d[i]);//输入x,y,d c[y[i]]++;//第y[i]列的数据个数++ } for(int i=1;i<=m;i++) a[i]=new int[c[i]];//为每一列申请空间来存数据 for(int i=1;i<=k;i++) { *a[y[i]]=d[i];//收集数据到第y列中 a[y[i]]++;//第y列的指针指向下一个地址,准备下一次收集 } for(int i=1;i<=m;i++)//列优先 { a[i]-=c[i];//因为前面收集数据的时候每一列的指针都指向了该列的最后一个元素,所以要先减去该列的元素数,让它指向第一个元素 for(int j=1;j<=c[i];j++,a[i]++)//从第1列开始输出,j用来统计输出到第i列第几个元素,如果输出到最后一个元素,跳出循环 printf("%d ",*a[i]);//指针每次+1,指向下一个元素并输出它 } return 0; }
a[i]=new int c[[i]];这一句的意思是给a[i]这个指针新申请c[i]个空间,等同于我们开了LP个一维的指针数组,这些数组每一个都有一个专用的指针a[i],每个数组有c[i]个元素。
到这里,我们已经讲完了利用指针开动态数组数组的具体做法,这样可以很有效率的优化你的程序,赶紧用起来吧!!!