上次我们已经了解了串的顺序定长实现和串的两种模式匹配算法的实现。此次,我们一起来看看数组的顺序存储实现。
还是老规矩:
程序在码云上可以下载。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git
数组是也是一种特殊的线性表,它的限制比串、栈和队列都要严格——不仅限制了操作的类型(只能做存取和定位操作),而且限制了一个数组中只能存储同种类型的元素。数组的存储空间是固定的,不允许我们在运行过程中动态改变其存储空间的大小。
首先看看数组的ADT定义,了解一下数组可以进行哪些操作:
ADT Array{
数据对象:ji = 0, …, bi-1, i = 1,2,…,n,
D = {aj1j2…jn|n(>0)称为数据的维数, bi是数组第i维的长度,
ji是数组元素的第i维下标,aj1j2…jn ∈ ElemSet}
数据关系:R = {R1, R2, … ,Rn}
Ri = {1…jn >|
0<=jk<=bk-1, 0<=k<=n,且k!=i,
0<=ji<=bi-2,
aj1…ji…jn, aj1…ji+1…jn ∈ D, i = 1,2,…,n }
基本操作:
InitArray(&A, n, bound1, … , boundn)
操作结果:若维数 n 和各维长度合法,则构造相应的数组A,并返回OK。
DestroyArray(&A)
操作结果:销毁数组A;
Value(A, &e, index1, … , indexn)
初始条件:A是n维数组,e为元素变量,随后是n个下标值。
操作结果:若各下标不超界,则e赋值为所指定的A的元素值,并返回OK。
Assign(&A, e, index1, … , indexn)
初始条件:A是n维数组,e为元素变量,随后是n 个下标值。
操作结果:若下标不超界,则将e的值赋给所指定的A的元素,并返回OK。
} ADT Array
在看代码之前我们需要了解几个非常重要的知识点:
1.可变参数
书上P93页增加了一个头文件:stdarg.h,这个头文件引入了一项重要功能——可变参数。书中的程序也大量使用了可变参数作为形参。我们在写程序之前需要了解清楚可变参数是做什么的,怎么用才可顺利完成程序的实现。
首先要搞明白为什么书的作者要使用可变参数的方式实现数组?
作者的想法是在一个线性的存储空间上实现一个n维数组,将n维数组按照一定的映射方式(映射就是对应的意思)存储到一维数组中,使用数组的时候想要找到某个元素或者修改某个元素只需要提供n维数组对应的下标,程序就会按照事先设置的映射规则(书中称之为映像函数)计算出这个元素相对与存储它的一维数组是在哪个位置上,然后再去一维数组里面找。使用者只需要知道n维数组的下标,不需要了解n维数组的实现原理,也不需要知道指定下标的元素在一位数组中怎样映射就可以实现对数组的操控了。
可是要实现这样的数组,我们需要确定数组是几维数组(维数)以及每一维的长度(维界),才可能知道下标传几个数字才是正确的,比如:一维数组只传一个数字就可以唯一确定一个元素,二维数组传两个数字可以唯一确定一个元素,三维数组传三个数字可以唯一确定一个元素。。。以此类推。那也就是说每次在参数列表中传递的数字个数是不确定的,因为我们想要写出的代码可以适应任意维数的数组,调用处维数修改时不需要修改数组的源代码就可以适应任意维数的情况,提高代码的灵活性,但是在不使用可变参数的情况下我们目前掌握的C语言技术还不足以实现在参数列表中添加任意多个参数的功能,因为按照我们的认知:形式参数和实际参数必须保证位置上一一对应,而且参数类型也必须匹配才可以正确的编译和运行。
想要实现形式参数和实际参数的个数不相等的情况下仍然可以按照实际传入的参数对应形参就必须要靠可变参数了。可变参数可以实现我们的设想:一维数组传在实参列表中写1个下标值,二维数组传在实参列表中写2个下标值,三维数组传在实参列表中写3个下标值。。。以此类推。所以我们要使用可变参数实现n维数组。
百度百科如是说:https://baike.baidu.com/item/stdarg.h/10239382?fr=aladdin
stdarg.h是C语言中C标准函数库的头文件,stdarg是由standard(标准) arguments(参数)简化而来,主要目的为让函数能够接收可变参数。C++的cstdarg头文件中也提供这样的功能;虽然与C的头文件是兼容的,但是也有冲突存在。
可变参数函数(Variadic functions)是stdarg.h内容典型的应用,虽然也可以使用在其他由可变参数函数调用的函数(例如,vprintf)。
也就是说想要使用可变参数必须导入stdarg.h这个头文件。
推荐大家看一篇文章,我也是看这篇文章了解了可变参数的使用方法,非常感谢作者的分享:
http://blog.csdn.net/ddppqq/article/details/17332161
从这篇文章中提炼的对我们有用的信息如下:
VA_LIST的用法:
函数原型:
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va在这里是variable-argument(可变参数)的意思.
使用步骤
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,
这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。
(4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。
如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。
使用VA_LIST应该注意的问题:
(1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,
可变参数的类型和个数完全在该函数中由程序代码控制,
它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现
智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,
对编程查错不利.不利于我们写出高质量的代码。
在搞清楚作者的意图的可变参数的使用方法之后,我们需要了解一下上溢OVERFLOW和下溢UNDERFLOW:
2.上溢和下溢
对于计算机中的上溢和下溢,有一种简单的解释:
上溢:运算结果超出所能表示的最大正数
下溢:运算结果超出所能表示的最小负数
但是我们说的并不是这个含义,作者在程序中更想要表示的是数组下标越界的含义:
在math.h中定义了一些表示运算异常的常量,其中有几个我们经常见到:
/*
* Types for the _exception structure.
*/
#define _DOMAIN 1 /* domain error in argument */
#define _SING 2 /* singularity */
#define _OVERFLOW 3 /* range overflow */
#define _UNDERFLOW 4 /* range underflow */
#define _TLOSS 5 /* total loss of precision */
#define _PLOSS 6 /* partial loss of precision */
/*
* Exception types with non-ANSI names for compatibility.
*/
#ifndef __STRICT_ANSI__
#ifndef _NO_OLDNAMES
#define DOMAIN _DOMAIN
#define SING _SING
#define OVERFLOW _OVERFLOW
#define UNDERFLOW _UNDERFLOW
#define TLOSS _TLOSS
#define PLOSS _PLOSS
#endif /* Not _NO_OLDNAMES */
#endif /* Not __STRICT_ANSI__ */
其中就提到了OVERFLOW和UNDERFLOW。
程序中的OVERFLOW多用在内存分配失败的时候,因为此时的可分配内存空间过小,再分配就会超出限定的最大内存大小(向上溢出,内存满了再分配就溢出了),所以分配不成功。
程序中的UNDERFLOW用在检查数组下标越界的时候,如果下标的值小于0,也就是传入的可变参数包含负数下标的时候,我们认为下标超过了数组所能接受的最小值0,变成了负数,所以认为程序发生了向下溢出(下溢)。
3.映像函数是干啥的,为啥非得搞那么多数学表达式
我们在解释可变参数时已经提到,作者想在一个线性的存储空间上模拟一个n维数组,这个数组和我们在C语言的数组不一样,它是我们手工实现的,与C语言已有的数组没有任何关系,我们需要手工完成数组的创建,初始化,元素定位,取值,修改值和销毁。最终目的是让实现的数组对外表现用起来就像C语言的数组一样,对使用者屏蔽掉数组的实现细节。
但是如果我们必须清楚地意识到,多维数组和一维数组是有区别的,所以不可能按照一维数组的存取方式直接操纵多维数组。所以我们必须找到一种方法,让我们可以通过某些特定的接口发出指令来操纵多维数组,而且这些指令可以翻译成在一维数组上的等效操作,这种方法就是一维数组和多维数组元素之间的对应规则,也就是我们所说的映像函数。
映像函数在书上P93页有公式。
为什么非得把多维数组通过特定的方式存到线性的一位数组中?
为了使模拟的数组尽可能贴近计算机真实的运行方式。因为计算机中的内存就是一维线性的,我们想要模拟C语言中真正在使用的数组就要明白它在内存中的映射方式。我们的一维数组模拟的就是内存,多维数组对应的是我们实际使用的数组。我们就是靠操作这个多维数组来间接操纵它下面封装的一维数组。
在解释清楚了这些问题之后,就可以看程序了,本次的程序虽然简短,却使用了许多编程技巧:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>引入头文件<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#include //使用了标准库函数
#include //使用了动态内存分配函数
#include //标准头文件,提供宏va_start、va_arg和va_end,用于存取变长参数表
#include //使用了其中的两个符号常量OVERFLOW和UNDERFLOW
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>自定义符号常量<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#define OK 1 //表示操作正确的常量
#define ERROR 0 //表示操作错误的常量
#define TRUE 1 //表示逻辑真的常量
#define FALSE 0 //表示逻辑假的常量
#define MAX_ARRAY_DIM 8 //假设数组维数最大值为8
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>自定义数据类型<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
typedef int Status; //状态标志
typedef int ElemType; //元素类型
//---------------------数组的顺序存储表示----------------------
typedef struct{
ElemType * base; //数组元素基址,由InitArray分配
//这个指针指向的内存空间用于存放数组中的元素。
//其大小由数组的维数和维界决定。
int dim; //数组维数
int * bounds; //数组维界基址,由InitArray分配
//维界是指数组每一维的长度,比如A[3][4][5]
//那么需要三个int变量分别存储3、4、5这3个值
//由于数组维数是不固定的,所以这块内存需动态分配
int * constants; //数组印象函数常量基址,由InitArray分配
//数组印象函数常量是配合数组的映像函数来定位数组元素的
//映像常量的值在确定数组维数时就可以确定,提前将它们算出来
//并储存在一段连续的内存空间中有利于元素定位操作时计算
//元素的相对位置。
}Array;
//----------------------数组的基本操作------------------------
/*
函数:InitArray
参数:Array &A 数组引用
int dim 数组维数
... 可变参数,数组每一维的大小
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:若维数dim和随后的各维长度合法,则构造相应的数组A,并返回OK
*/
Status InitArray(Array &A, int dim, ...){ //初始化数组
//ap指向可变参数列表
va_list ap;
//检查参数维数dim是否合法
if(dim < 1 || dim > MAX_ARRAY_DIM) { //维数参数非法
//操作失败
return ERROR;
}//if
//确定数组的维数
A.dim = dim;
//根据维数dim分配数组维界基址的空间
//if(!(A.bounds = (int *)malloc(dim * sizeof(int))))
//相当于以下两行代码:
//A.bounds = (int *)malloc(dim * sizeof(int))
//if(!A.bounds) <=> if(A.bounds == NULL)
if(!(A.bounds = (int *)malloc(dim * sizeof(int)))) {
printf("内存分配失败!\n");
//math.h头文件中定义OVERFLOW的值为3
//OVERFLOW表示上溢错误
exit(OVERFLOW);
}//if
//若各维长度合法,则存入A.bounds,并求出A的元素总数elemtotal
int elemtotal = 1;
//接下来需要根据可变参数传递的每一维的大小计算出数组中的元素总数
//在这个过程中需要多次读取可变参数的值,所以要调用va_arg宏来
//循环获取数组每一维的大小bounds,所以va_start宏要放在循环的外面。
//注:调用va_arg宏获取可变参数之前要调用一下va_start宏。
//ap为va_list类型,是可变参数的前一个参数,也就是存放数组维数的dim。
//在使用可变参数后,对函数的参数列表的声明顺序就有要求了
//可变参数必须声明成最后一个参数,否则没办法确定传入的值
//是否应该传给可变参数。而dim必须是可变参数的前一个参数
//中间不可以有其他参数。
//第二个参数就是我们将要在各个操作中使用的可变参数
va_start(ap, dim);
//根据数组的维数初始化每一维的大小(维界)
for(int i = 0; i < dim; ++i){
//调用va_arg宏从可变参数中取出每一维的大小bounds,类型是int
A.bounds[i] = va_arg(ap, int);
//A.bounds[i] < 0表示出现了下溢错误,此时应立即终止程序执行
//可变参数传入的值要进行检查后才可以使用
if(A.bounds[i] < 0){
//math.h头文件中定义UNDERFLOW的值为4
//UNDERFLOW表示下溢错误
return UNDERFLOW;
}//if
//元素总数累乘,比如数组A[3][4][2]的元素总数就是
//3*4*2 = 24 所以算数组元素总数就是每一维大小相乘。
elemtotal *= A.bounds[i];
}//for
//获取可变参数结束调用va_end宏
va_end(ap);
//计算出数组元素总数之后就要申请相同大小的存储数组元素的内存空间,
//以便容纳数组中的全部元素
//if(!(A.base = (ElemType *)malloc(elemtotal * sizeof(ElemType))))
//相当于以下两行代码:
//A.base = (ElemType *)malloc(elemtotal * sizeof(ElemType));
//if(!A.base) <=> if(A.base == NULL)
if(!(A.base = (ElemType *)malloc(elemtotal * sizeof(ElemType)))){
printf("内存分配失败!\n");
//math.h头文件中定义OVERFLOW的值为3
//OVERFLOW表示上溢错误
exit(OVERFLOW);
}//if
//申请映像函数常量的存储空间,申请存储单元个数等于数组维数
if(!(A.constants = (int *)malloc(dim * sizeof(int)))){
printf("内存分配失败!\n");
//math.h头文件中定义OVERFLOW的值为3
//OVERFLOW表示上溢错误
exit(OVERFLOW);
}//if
//求映像函数的常数ci,并存入A.constants[i-1],i=1,...,dim
//映像函数的常数ci的作用是方便的给元素定位,具体公式推导见书上P93
//只要算出ci的值,就可以轻松得到某个元素在A.base指示的一维存储空间
//中的相对地址off,然后就可以通过A.base[off]获取元素的值。
A.constants[dim - 1] = 1; //L=1,指针的增减以元素大小为单位
for(int i = dim - 2; i >= 0; --i) {
A.constants[i] = A.bounds[i + 1] * A.constants[i + 1];
}//for
//操作成功
return OK;
}//InitArray
/*
函数:DestroyArray
参数:Array &A 数组引用
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:销毁数组A
*/
Status DestroyArray(Array &A){
//1.释放存储数组元素的A.base指示的内存空间
//检查此段内存区域是否存在
if(!A.base) { //if(!A.base) <=> if(A.base == NULL)
return ERROR;
}//if
//释放A.base指向的存储区域的内存空间
free(A.base);
//A.base指针置空,释放掉指针变量本身的内存空间
A.base = NULL;
//2.释放存储数组维界(每一维大小)的A.bounds指示的内存空间
//检查此段内存区域是否存在
if(!A.bounds) { //if(!A.bounds) <=> if(A.bounds != NULL)
return ERROR;
}//if
//释放A.bounds指向的存储区域的内存空间
free(A.bounds);
//A.bounds指针置空,释放掉指针变量本身的内存空间
A.bounds = NULL;
//3.释放存储数组映像函数常量的A.constants指示的内存空间
//检查此段内存区域是否存在
if(!A.constants) { //if(!A.constants) <=> if(A.constants == NULL)
return ERROR;
}//if
//释放A.constants指向的存储区域的内存空间
free(A.constants);
//A.constants指针置空,释放掉指针变量本身的内存空间
A.constants = NULL;
//操作成功
return OK;
}//DestroyArray
/*
函数:Locate
参数:Array A 数组A
va_list ap 指向保存待定位元素各个维下标的可变参数的指针
int &off 带回待定位元素在A.base指示的一维存储空间的相对位置
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:对ap指示的下标对应的元素进行定位,由off带回元素在A.base
指示的一维存储空间的相对位置。
实际上就是使用映像函数配合求得的映像常量完成元素的相对定位,
得到一个相对位置。
*/
Status Locate(Array A, va_list ap, int &off){
//ind临时保存从可变参数中取得的的待定位元素的每一维的下标
int ind;
//off保存求得的待定位元素的相对位置
off = 0;
//根据元素每一维的下标使用映像函数进行定位
for(int i = 0; i < A.dim; ++i){
//从可变参数中获取待定位元素在某一维的下标
ind = va_arg(ap, int);
//检查下标ind是否越界
if(ind < 0 || ind >= A.bounds[i]) { //数组越界
return OVERFLOW;
}//if
//根据映像函数求出某一维的偏移量,并且累加到off上
off += A.constants[i] * ind;
}//for
//操作成功
return OK;
}//Locate
/*
函数:Value
参数:Array A 数组A
ElemType &e 获取指定下标位置元素的值保存到e
... 可变参数,传递的是待查找元素各维的下标
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:(按下标从数组A中取值)A是n维数组,e为元素变量,
随后的可变参数是n个下标值。若各下标不超界,
则e赋值为所指定的数组A的元素值,并返回OK。
*/
Status Value(Array A, ElemType &e, ...){
//声明指向可变参数的指针ap
va_list ap;
//off保存了指定下标元素的定位结果,也就是这个元素
//在A.base指示的一维数组中的位置
int off;
//保存定位操作的结果,只要这个结果<=0就说明定位操作失败
Status result;
//开始获取e后面的可变参数,并使ap指向可变参数,供定位函数使用
va_start(ap, e);
//对指定下标的元素进行定位,获取这个元素在A.base指示的
//一位数组中的相对位置,如果返回值result<=0,定位失败
if((result = Locate(A, ap, off)) <= 0) { //数组越界
return result;
}//if
//根据定位操作计算出的相对位置,直接将其作为一位数组的下标
//从A.base中按照一维数组的取法取出对应位置的元素(注意下标从0开始) ,
//也可以用指针的取法来取得对应位置的元素
//e = *(A.base + off); <=> e = A.base[off];
e = *(A.base + off);
//结束可变参数的获取。书上没有此行代码,但是建议写上它
va_end(ap);
//操作成功
return OK;
}//Value
/*
函数:Assign
参数:Array &A 数组引用
ElemType e 修改指定下标位置元素的值为e
... 可变参数,传递的是待修改元素各维的下标
返回值:状态码,操作成功返回OK,操作失败返回ERROR
作用:A是n维数组,e为元素变量,随后是n个下标值
若各下标不超界,将e的值赋给所指定的A的元素,并返回OK。
*/
Status Assign(Array &A, ElemType e, ...){
//ap指向存储下标的可变参数
va_list ap;
//off存储了定位操作获得的元素在A.base一维数组中的相对位置
int off;
//开始获取参数e后面的可变参数,并使ap指向可变参数
va_start(ap, e);
//result存储了定位操作的结果,如果这个值<=0表示定位操作失败
Status result;
//按可变参数指示的下标执行定位操作,获取元素在A.base指示的
//一维数组中的相对位置off,操作结果作为返回值赋值给result
if((result = Locate(A, ap, off)) <= 0) { //数组越界
return result;
}//if
//结束可变参数的获取。书上没有此行代码,但是建议写上它
va_end(ap);
//根据定位操作获取的相对位置off,将A.base指示的内存空间中
//对应位置的值修改为e,也可以使用一维数组的写法:
//*(A.base + off) = e; <=> A.base[off] = e;
*(A.base + off) = e;
//操作成功
return OK;
}//Assign
int main(int argc, char **argv){
Array A;
printf("\n--------------------------------数组顺序存储表示-------------------------------\n\n");
//i,j,k是临时变量
int i, j, k;
//p是工作指针
int *p;
//设置数组的维数为3
int dim = 3;
//设置数组每一维的大小分别为3,4,2,也就是:A[3][4][2]
int bound1 = 3, bound2 = 4, bound3 = 2;
//e保存从键盘输入的元素值和带回的元素值
ElemType e, *p1;
//构造一个3*4*2的三维数组
InitArray(A, dim, bound1, bound2, bound3);
//输出数组A的维界基址
p = A.bounds;
printf("数组A的维界(每一维大小)基址:A.bounds=[");
for(int i = 0; i < dim; i++){
printf(" %d ", *(p + i));
}//for
printf("]\n");
//输出数组A印象函数常量基址
p = A.constants;
printf("数组印象函数常量基址:A.constants=[");
for(int i = 0; i < dim; i++){
printf(" %d ", *(p + i));
}//for
printf("]\n");
//数组元素赋初值
for(int i = 0; i < bound1; i++){
for(int j = 0; j < bound2; j++){
for(int k = 0; k < bound3; k++){
//调用赋值函数Assign初始化数组A
//第一个参数是数组A
//第二个参数是初始化的元素值
//从第三个参数开始到最后的是3个下标(可变参数)
Assign(A, i * 100 + j * 10 + k, i, j, k);
}//for-k
}//for-j
}//for-i
//输出矩阵的所有元素
printf("A[%d][%d][%d]矩阵元素如下:\n", bound1, bound2, bound3);
for(int i = 0; i < bound1; i++){
for(int j = 0; j < bound2; j++){
for(int k = 0; k < bound3; k++){
//取出A[i][j][k]位置的元素
Value(A, e, i, j, k);
//输出
printf("A[%d][%d][%d]=%2d\t", i, j, k, e);
}//for-k
printf("\n");
}//for-j
printf("\n");
}//for-i
printf("A.dim=%d\n", A.dim);
DestroyArray(A);
}//main
以下是程序运行时的输出:
--------------------------------数组顺序存储表示-------------------------------
数组A的维界(每一维大小)基址:A.bounds=[ 3 4 2 ]
数组印象函数常量基址:A.constants=[ 8 2 1 ]
A[3][4][2]矩阵元素如下:
A[0][0][0]= 0 A[0][0][1]= 1
A[0][1][0]=10 A[0][1][1]=11
A[0][2][0]=20 A[0][2][1]=21
A[0][3][0]=30 A[0][3][1]=31
A[1][0][0]=100 A[1][0][1]=101
A[1][1][0]=110 A[1][1][1]=111
A[1][2][0]=120 A[1][2][1]=121
A[1][3][0]=130 A[1][3][1]=131
A[2][0][0]=200 A[2][0][1]=201
A[2][1][0]=210 A[2][1][1]=211
A[2][2][0]=220 A[2][2][1]=221
A[2][3][0]=230 A[2][3][1]=231
A.dim=3
--------------------------------
Process exited with return value 0
Press any key to continue . . .
总结:
本次编程使用了可变数组完成了对计算机中n维数组操作的模拟。
数组的操作很有限,不可以在运行期间改变它的容量。一个数组中只能存储相同类型的元素。
映像函数是模拟的多维数组和底层的一维数组之间的桥梁,定位,设置值,取值都要用到映像函数。
映像函数常量是根据数组的维数算出来的,其作用是辅助映像函数的计算。
下次我们将会介绍稀疏矩阵存储程序的实现。希望大家继续关注我的博客,下次再见!