课程采用教材《数据结构(C语言版)》严蔚敏,吴伟民,清华大学出版社。
本系列博文用于自我学习总结和期末复习使用,同时也希望能够帮助到有需要的同学。如果有知识性的错误,麻烦评论指出。
本次实现多维数组的初始化、索引和赋值。
本次实验中抽象数据类型的定义如下:
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={
aj1…ji…jn,aj1…ji+1,…jn∈D,i=2,…,n}
基本操作:
InitArray(&A, n,bound1,…,boundn)
操作结果:若维数n和各维度长度合法,则构造相应的数组A,并返回OK。
DestroyArray(&A)
操作结果:销毁数组A。
Locate(A, index1,…,indexn, &off)
操作结果:若各下标值合法,则求出该元素在A中相对地址off。
Value(&e, A, index1,…,indexn)
初始条件:e为元素变量,A是n维数组,随后是n个下标值。
操作结果:若各下标不超界,则e值赋为所指定的A的元素值,并返回OK。
Assign(&A, e, index1,…,indexn)
初始条件:A是n维数组,e为元素变量,随后是n个下标值。
操作结果:若各下标不超界,则将e的值赋给所指定的A的元素,并返回OK。
}ADT Array
将所有的数组视为一个线性表,按照一定的规律,表示不同维度的元素。本次实验采用顺序表来实现多维数组。由于存储单元是一维结构,而数组是个多维的结构,则用一组连续存储单元存放数组的数据元素就有个次序约定问题。对于二维数组来说,有两种存储方式:一种是以列序为主序,一种是以行序为主序。在C语言中,二维数组是以行序为主序的存储结构。
由此,对于数组,一旦规定了它的维数和各维的长度,便可以为它分配存储空间。反之,只要给出一组下标便可求得相应数组元素的存储位置。
在以行序为主序的存储结构中,假设每个数据元素占L个存储单元,bi为各维度的边界,则n维数组的数据元素存储位置的计算公式为:
L O C ( j 1 , j 2 , . . . , j n ) = L O C ( 0 , 0 , . . . , 0 ) + ( b 2 × . . . × b n × j 1 + b 3 × . . . × b n × j 2 + . . . + b n × j n − 1 + j n ) L LOC(j_1,j_2,...,j_n)=LOC(0,0,...,0)+(b_2\times...\times b_n\times j_1+b_3\times...\times b_n\times j_2+...+b_n\times j_{n-1}+j_n)L LOC(j1,j2,...,jn)=LOC(0,0,...,0)+(b2×...×bn×j1+b3×...×bn×j2+...+bn×jn−1+jn)L
可缩写成
L O C ( j 1 , j 2 , . . . , j n ) = L O C ( 0 , 0 , . . . , 0 ) + ∑ i = 1 n − 1 c i j i LOC(j_1,j_2,...,j_n)=LOC(0,0,...,0)+\sum_{i=1}^{n-1}c_ij_i LOC(j1,j2,...,jn)=LOC(0,0,...,0)+i=1∑n−1ciji
其中 c n = L , c i − 1 = b i × c i , 1 < i ≤ n c_n=L,c_{i-1}=b_i\times c_i,1< i\leq n cn=L,ci−1=bi×ci,1<i≤n。
上式称为n维数组的映像函数。容易看出,数组元素的存储位置是其下标的线性函数,一旦确定了数组各维的长度,ci就是常数。由于计算各个元素存储位置的时间相等,所以存取数组中任一元素的时间也相等。具有这一特点的存储结构为随机存储结构。
本次实验采用顺序结构表示和实现多维数组。在结构体中,定义了数组元素的基址,数组维数,各维度的边界基址,映像函数中的常数基址。接下来讲解其中的基本操作。
目标:获得初始化的n维数组A。
具体过程:
向初始化函数InitArray(Array& A, int dim, …)中传入未初始化的数组A,维度dim,以及各维度的边界,采用变长参数列表的方式传入。
先为各维度边界分配dim个空间,从变长参数列表中获取各个维度的边界,同时获得数组中元素的总个数,即各个维度边界之积。按照元素总个数,为基址分配空间。需要注意的是,如果基址从1开始,则需要分配元素总个数加1个空间;如果从0开始,直接分配元素总个数个空间即可。计算映像函数的常数ci,便于数组索引。其计算方法如下: c i = b i + 1 × c i + 1 c_i=b_{i+1}\times c_{i+1} ci=bi+1×ci+1
目标:销毁数组A。
具体过程:
逐一释放数组中的指针,并将指针指空。
目标:获取下标位置元素在数组A中的相对地址off。
具体操作:
采用变长参数列表,向函数Locate(Array A, va_list index, int& off)传入下标值。在获取偏移量之前,先判断各下标值的合法性,之后根据映像函数,计算其中不含LOC(0,0,…,0)的部分,也即下标位置元素相对于起始位置的偏移量。计算方法是: o f f = ∑ i = 1 n − 1 c i j i off=\sum_{i=1}^{n-1}c_ij_i off=∑i=1n−1ciji。
目标:通过下标值,获取下标位置的元素值。
具体过程;
通过上面的Locate函数,获取下标位置的偏移量,在基址上加上偏移量,即为下标位置,带星号访问元素值,并用e传出。
目标:将e的值赋给数组A下标位置的元素。
具体过程;
通过上面的Locate函数,获取下标位置的偏移量,在基址上加上偏移量,即为下标位置,带星号访问元素值,并赋以e的值。
本次实验中会用到头文件stdarg.h,提供宏va_start、va_arg和va_end,以及va_list类型,用于存储和操作变长参数列表。这里简单介绍一下这些宏。
va_start是获取边长参数列表,并存放在va_list类型中的宏。它可以将参数列表中非引用参数之后的变长参数列表以字符指针的形式存放在va_list类型中,该类型中的第一个参数是起引导作用的非引用参数,第二个参数起,就是变长参数列表部分。再通过va_arg依次取出va_list类型中的参数。它在取出一个参数后,指针后移,指向下一个参数,所有的参数应按相应的类型取出。最后,通过va_end关闭va_list类型。
//1、用到的头文件、命名空间和函数执行结果状态代码
#include
#include // 标准头文件,提供宏va_start、va_arg和va_end,用于存储变长参数列表
using namespace std;
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status是函数的类型,其值是函数结果状况代码
typedef int Status;
typedef int ElemType;//将ElemType定义为int类型
//2、采用的存储结构
//-----数组的顺序存储表示-----
#define MAX_ARRAY_DIM 8// 假设数组维数的最大值为8
struct Array {
ElemType *base;// 数组元素基址,由InitArray分配
int dim;// 数组维数
int* bounds;// 数组维界基址,由InitArray分配
int* constants;// 数组映像函数常量基址,由InitArray分配
};
//3、基本操作函数原型说明及实现
//-----基本操作的函数原型说明-----
Status InitArray(Array& A, int dim, ...);
// 初始条件:维数dim和随后的各维长度合法。
// 操作结果:构造相应的数组A,并返回OK。
Status DestroyArray(Array& A);
// 初始条件:数组A存在。
// 操作结果:销毁数组A。
Status Locate(Array A, va_list ap, int& off);
// 初始条件:A是n维数组,ap为各下标值,各下标不超界。
// 操作结果:用off返回该元素在A中相对地址。
Status Value(ElemType& e, Array A, ...);
// 初始条件:A是n维数组,e为元素变量,随后是各下标值,各下标不超界。
// 操作结果:将e值赋为所指定的A的元素值,并返回OK。
Status Assign(Array& A, ElemType e, ...);
// 初始条件:A是n维数组,e为元素变量,随后是各下标值,各下标不超界。
// 操作结果:将e的值赋给所指定的A的元素,并返回OK。
//-----基本操作的算法描述-----
Status InitArray(Array& A, int dim, ...) {
// 若维数dim和随后的各维长度合法,则构造相应的数组A,并返回OK。
if (dim<1 || dim>MAX_ARRAY_DIM)return ERROR;// 维度数合法
A.dim = dim;// 取维度
A.bounds = new int[dim];// 分配dim个维度边界
if (!A.bounds)exit(OVERFLOW);// 分配失败
// 若各维长度合法,则存入A.bounds,并求出A的元素总数elemtotal
int elemtotal = 1;// 元素总数从1开始计
va_list bound;// 定义va_list类型的指针bound,用于存放各维度的边界
va_start(bound, dim);// bound为va_list类型,是存放变长参数表信息的数组
for (int i = 0; i < dim; i++)
{// 遍历每个维度边界
A.bounds[i] = va_arg(bound, int);// 从bound中取出第一个指针指向的维度边界,并将指针后移
if (A.bounds[i] < 0)return UNDERFLOW;// 如果维度边界小于0,则边界下溢
elemtotal *= A.bounds[i];// 每次累乘维度边界,获取元素总数
}
va_end(bound);// 销毁bound指针
A.base = new ElemType[elemtotal];// 为基址分配elemtotal个空间,如果基址从1开始,则需要分配elemtotal+1个空间,否则容易造成内存泄漏
if (!A.base)exit(OVERFLOW);// 内存分配失败
// 求映像函数的常数Ci,并存入A.constants[i-1],i=1,…,dim
A.constants = new int[dim];// 分配dim个数组映像函数常量,用于索引元素
if (!A.constants)exit(OVERFLOW);// 分配失败
A.constants[dim - 1] = 1;// 每个元素占L=1个存储单元,指针的增减以元素的大小为单位
for (int i = dim - 2; i >= 0; --i)// c(i)=b(i+1)*c(i+1)
A.constants[i] = A.bounds[i + 1] * A.constants[i + 1];
return OK;
}// InitArray
Status DestroyArray(Array& A) {
// 销毁数组A。
if (!A.base)return ERROR;
delete[] A.base;// 注意向A.base中赋值时,不能越界
A.base = NULL;
if (!A.bounds)return ERROR;
delete[] A.bounds; A.bounds = NULL;
if (!A.constants)return ERROR;
delete[] A.constants; A.constants = NULL;
return OK;
}// DestroyArray
Status Locate(Array A, va_list index, int& off) {
// 若index指示的各下标值合法,则求出该元素在A中的相对地址off
off = 0;// 偏移量初始值为0
for (int i = 0; i < A.dim; i++)
{// 计算偏移量
int ind = va_arg(index, int);// 从index中取出第一个指针指向的维度下标,并将指针后移
if (ind<0 || ind>A.bounds[i])return OVERFLOW;// 维度下标不合法
off += A.constants[i] * ind;// LOC(j(1),j(2),...,j(n))=LOC(0,0,...,0)+SUM(c(i)j(i))
}
}// Locate
Status Value(ElemType& e, Array A, ...) {
// 将e值赋为所指定的A的元素值,并返回OK。
va_list index;// 定义va_list你类型的指针index
va_start(index, A);// 需要注意,va_start无法获取引用参数后面的变长参数列表
int off;// 下标元素相对基址的偏移量
if (!Locate(A, index, off))return ERROR;// 获取元素的偏移量
e = *(A.base + off);// 取下标对应地址的存储值
}// Value
Status Assign(Array& A, ElemType e, ...) {
// 将e的值赋给所指定的A的元素,并返回OK。
va_list index;// 定义va_list你类型的指针index
va_start(index, e);// 需要注意,va_start无法获取引用参数后面的变长参数列表
int off;// 下标元素相对基址的偏移量
if (!Locate(A, index, off))return ERROR;// 获取元素的偏移量
*(A.base + off) = e;// 对下标对应地址的存储值赋值
}// Assign
int main()
{
Array A; ElemType e;
InitArray(A, 3, 1, 2, 3);// 初始化一个3维,各维度边界为1,2,3的数组A
Assign(A, 1, 0, 1, 2);// 注意索引的范围,把A(1,2,3)赋值为1
Value(e, A, 0, 1, 2);// 获取A(1,2,3)的值
cout << e << endl;// 输出e
Assign(A, -1, 0, 1, 2);// 注意索引的范围,把A(1,2,3)赋值为-1
Value(e, A, 0, 1, 2);// 获取A(1,2,3)的值
cout << e << endl;// 输出e
if (DestroyArray(A)) cout << "数组已销毁" << endl;// 销毁数组
return 0;
}
通过本次实验,我们能够更好地了解编程语言中多维数组的初始化、索引、赋值过程,理解列主序和行主序的区别,理解多维数组的映像函数,特别是二维数组的映像函数。此外,还了解了变长参数列表的获取和操作方法,提升编程能力。
在编程实现的过程中,遇到了数组越界的问题,通过多方查找之后,才得以解决。数组越界与内存泄漏,需要再加深理解,避免出现这样的问题。同时,在使用va_arg时,不能获取引用参数之后的参数,所以在Value(ElemType& e, Array A, …)函数中,调换了参数A和e的位置。