第五章 数组、广义表

本章的数据结构是一个扩展的线性结构,表中的数据结构本身也是一个复合类型,如二维数组,可以将每个元素都看做一个一维数组的一维数组,又如广义表,表元素本身也可以是广义表。

数组

两种数组的逻辑表示

  1. 一维数组:\((a_0,a_1,a_2,a_3,a_4,...,a_{n-1})\)
  2. 二维数组:\([(a_{0,0},a_{0,1},a...,a_{0,n-1}),(a_{1,0},a_{1,1},...,a_{1,n-1}),...,(a_{n-1,0},a_{n-1,0},...,a_{n-1,n-1})]\)

可见二维数组是元素为一维数组的一维数组。

数组一般采取顺序存储,涉及最多的就是数组元素下标的计算问题。一维数组比较简单,知道\(a_0\)的位置,可以根据相对于\(a_0\)的偏移量求出其后任一元素\(a_i\)的位置。例如:\(a_0\)存放在内存100的位置,则\(a_i\)存放在\(100+i\)的位置。二维数组元素的位置计算稍微复杂,要考虑行优先和列优先两种情况,下面详细介绍。

二维数组的行优先和列优先存储

定义二维数组a:int a[4][5],对于行优先,a[2][3]是第几个元素的求法:行标2之前的行已填满元素,每行元素有5个,每行元素有5个,行标2所指的行的元素个数由列标指示出来,因此a[2][3]是第\(2 \cdot 5+3+1\)个元素,a[2][3]之前有13个元素,列优先的情况类似。下面看一个例题。

例:设二维数组A[6][10],每个数组元素占4个存储单元,若按行优先顺序存放的数组元素A[3][5]的存储地址是1000,求A[0][0]的存储地址。

分析:A数组按行优先存储,则对于A[3][5],行标3之前的元素都已经填满,每行元素有10个,一共有3行,行标3所指的行中元素个数由列标指示,有6个元素,因此,A[3][5]之前一共有\(10\cdot3+5\)个元素,A[3][5]的地址为1000,则A[0][0]的地址为\(1000-35\cdot4=860\)

矩阵的压缩存储

矩阵

\[\mathbf { A } _ { \mathrm { mn } } = \left( \begin{array} { c c c c } \mathrm { a } _ { 0,0 } & \mathrm { a } _ { 0,1 } & \ldots & \mathrm { a } _ { 0 , \mathrm { n } - 1 } \\ \mathrm { a } _ { 1,0 } & \mathrm { a } _ { 1,1 } & \ldots & \mathrm { a } _ { 1 , \mathrm { n } - 1 } \\ \vdots & \vdots & & \vdots \\ \mathrm { a } _ { \mathrm { m } - 1,0 } & \mathrm { a } _ { \mathrm { m } - 1,1 } & \ldots & \mathrm { a } _ { \mathrm { m } - 1 , \mathrm { n } - 1 } \end{array} \right) \]

\(A_{mn}\)即为一个矩阵的逻辑表示,在C语言中,可以用一个二维数组存放,假设元素都是int类型。

int A[m][n]

其中,m和n必须为常量,或者预先定义的宏常量。因此完整的定义如下:

#define m 4
#define n 5
int A[m][n]

矩阵的相关操作方法定义和实现

#define maxSize 100

//对于一个mxn的矩阵A[m][n],转置矩阵之后的矩阵B[n][m],且B[i][j] = A[i][j]
void trsmat(int A[][maxSize],int B[][maxSize],int m,int n);
//矩阵相加,cij = aij + bij
void addmat(int C[][maxSize],int A[][maxSize],int B[][maxSize],int m,int n);
//矩阵乘法,A乘B结果为C
void mutmat(int C[][maxSize],int A[][maxSize],int B[][maxSize],int m,int n,int k);
void trsmat(int A[][maxSize],int B[][maxSize],int m,int n){
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            B[j][i] = A[i][j];
        }
    }
}

void addmat(int C[][maxSize],int A[][maxSize],int B[][maxSize],int m,int n){
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            C[i][j] = A[i][j] + B[i][j];
        }
    }
}

void mutmat(int C[][maxSize],int A[][maxSize],int B[][maxSize],int m,int n,int k){
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < k; ++j) {
            C[i][j] = 0;
            for (int h = 0; h < n; ++h) {
                C[i][j] += A[i][h] * B[h][j];
            }
        }
    }
}

特殊矩阵和稀疏矩阵

相同的元素或零元素在矩阵中的分布存在一定规律的矩阵称为特殊矩阵,反之称为稀疏矩阵。

特殊矩阵

  1. 对称矩阵:矩阵中的元素满足\(a_{i,j}=a_{j,i}\)的矩阵称为对称矩阵。

例:假设有一个\(n \times n\)的对称矩阵,第一个元素为\(a_{0,0}\),请用一种存储效率较高的存储方式将其存储在一维数组中。

分析:由对称矩阵\(a_{i,j}=a_{j,i}\)的矩阵称为对称矩阵,其主对角线上下方元素对称相等,所以相同的元素只需要保存一份即可。所需要的存储空间为\((1+n) \times n /2\)。需要保存的元素为

\[\begin{array} { l } a _ { 0,0 } \\ a _ { 1,0 } a _ { 1,1 } \\ a _ { 2,0 } a _ { 2,1 } a _ { 2,2 } \\ \vdots \\ a _ { n - 1,0 } \quad a _ { n - 1,1 } \cdots a _ { n + 1 , n ^ { - 1 } } \end{array} \]

按行优先存储,保存在一维数组中的结果

\(a_{0 , 0}\) \(\cdot \cdot \cdot\) \(a_{n-1,0}\) \(a_{n-1,1}\) \(a_{n-1,n-1}\)
下标 0 \(\cdot \cdot \cdot\) \(n \times (n-1) /2\) \(n \times (n-1) /2 +1\) \((1+n) \times n /2 -1\)
  1. 三角阵

上三角阵为矩阵下三角部分元素全为c的矩阵。

下三角阵为矩阵上三角部分元素全为c的矩阵。

三角矩阵的存储方式和对称矩阵类似。以下三角为例,只需存储对角线及其以下部分的元素和其上三角中的一个元素c即可。

\[\left( \begin{array} { c c c c c c } a _ { 0,0 } & a _ { 0,1 } & c & \cdots & \cdots & c \\ a _ { 1,0 } & a _ { 1,1 } & a _ { 1,2 } & c & \cdots & c \\ c & a _ { 2,1 } & a _ { 2,2 } & a _ { 2,3 } & \cdots & c \\ \cdots & & \cdots & \cdots & \cdots & \cdots \\ c & & & \cdots & \cdots & a _ { n - 2 , n - 1 } \\ c & & \cdots & c & a _ { n - 1 , n - 2 } & a _ { n - 1 , n - 1 } \end{array} \right) \]

上图三角阵的存储在一维数组中的表示如下

\(a_{0,0}\) \(a_{1,0}\) \(a_{n-1,0}\) \(a_{n-1,1}\) \(a_{n-1,n-1}\) \(c\)
下标 0 1 \(n \times (n-1)/2\) \(n\times (n-1)/2+1\) \((1+n)\times n/2-1\) \((1+n) \times n /2\)

稀疏矩阵顺序存储

稀疏矩阵中的相同元素c(假设c为0)在矩阵中的分布不像在特殊矩阵中那么有规律可循,因此必须为其设计一些特殊的存储结构。

稀疏矩阵的顺序存储及相关操作:常用的稀疏矩阵顺序存储方法用三元组表示法和伪地址表示法。

三元组表示法

三元组数据结构为一个长度为n,表内每个元素都有3个分量的线性表,其3个分量分别为值、行下标和列下标。元素结构体定义如下:

typedef struct trimat{
    int val;
    int i;
    int j;
}Trimat;

在程序中如果要定义一个含有maxterms个非零元素的稀疏矩阵,则只需写如下代码:

Trimat trimat[maxterms];//maxterms是已经定义好的常量

语句trimat[k].val;表示取第k个非零元素的值,trimat[k].i;trimat[k].j;表示取第k个非零元素在矩阵的行下标和列下标。

为了简便起见,可以不使用上述方式来定义三元组,直接申请一个数组即可:

int trimat[maxterms][3];

第一列存放的是第k个非零元素的值,第二列表示第k个非零元素的行,第三列表示第k个非零元素的列。

void createtrimat(int A[][maxSize],int m,int n,int B[][3]){
    int k=1;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if(A[i][j] != 0){
                B[k][0] = A[i][j];
                B[k][1] = i;
                B[k][2] = j;
                ++k;
            }
        }
    }
    B[0][0] = k-1;
    B[0][1] = m;
    B[0][2] = n;
}
void printMatrix(int B[][3]){
    int k=1;
    for (int i = 0; i < B[0][1]; ++i) {
        for (int j = 0; j < B[0][2]; ++j) {
            if(i == B[k][1] && j == B[k][2]){
                printf("%d ",B[k][0]);
            } else{
                printf("0 ");
            }
        }
        printf("\n");
    }
}

伪地址表示法

伪地址即元素在矩阵中按照行优先或列优先存储的相对位置。用伪地址方法存储稀疏矩阵和三元组方法相似,只是三元组每一行中有两个存储单元存放地址,而伪地址只需要一个,因此伪地址法每一行只有2个存储空间,一个用来存放伪地址,另一个存放矩阵的元素值。这种方法需要2N个存储单元,N为非零元素的个数。对于一个\(m \times n\)的稀疏矩阵A,元素A[i][j]的伪地址计算方法为\(n \cdot (i-1) +j\)。根据这个公式,不仅可以计算出一个矩阵给定元素的伪地址,还可以反推出给定元素在原矩阵的真实位置。

稀疏矩阵链式存储

最常用的方法有两种:邻接表表示法和十字链表表示法。

邻接表表示法

邻接表表示法将矩阵中每一行的非零元素串成一个链表,链表节点有2个分量,分别表示该节点对应的元素值和列号。

graph LR; A(0)-->B(1); C(1)-->D(2); D(2)-->F(4); G(2)-->H(1); Q(3)-->w(2);

十字链表表示法

在稀疏矩阵的十字链表存储结构中,矩阵的每一行用一个带头结点的链表,每一列也用一个带头结点的链表表示,这种存储结构的链表节点都有5个分量:行分量,列分量,数据域分量,指向下方节点的指针,指向右方节点的指针。

十字链表的存储结构比较复杂。十字链表是由一些单链表纵横交织而成的,其中最左边和最上边是头结点数组,不存放数据信息,左上角的节点可以为整个十字链表的头结点,它有5个分量,分别存储矩阵的行数,列数,非零元素个数以及指向两个节点数组的指针,十字链表节点中除头结点以外的节点就是存储矩阵非零元素相关信息的普通节点。

  1. 普通节点的定义
typedef struct OLNode{
    int row,col; //行号和列号
    struct OLNode *right,*down;//指向右边节点和下方节点的指针
    int val;
}OLNode;
  1. 头结点的定义
typedef struct CrossList{
    OLNode *rhead,*chead; //指向两头节点数组的指针
    int m,n,k; //矩阵行数,列数,非零节点的个数
}CrossList;

广义表

广义表:表元素可以是原子或广义表的一种线性表的扩展结构。

下面举例说明广义表:

  1. A=(),A是一个空表,长度为0,深度为1。
  2. B=(d,e),B的元素全是原子,即d和e,长度为2,深度为1。
  3. C=(b,(c,e)),C有两个元素,分别是原子b和另一个广义表(c,d),长度为2,深度为2。
  4. D=(B,C),D的元素都是广义表,即B和C,长度为2,深度为3,由此可见,一个广义表的子表可以是其他已经定义的广义表的引用。
  5. E=(a,E),E有两个元素,分别是a和它本身,长度为2,由此可见一个广义表可以是递归定义的,展开E会得到(a,(a,(a,(a,...)))),是一个无限深的广义表。

广义表的深度:为表中最上层元素的个数,如广义表C的长度为2。

广义表的深度:为表中括号的最大层数。注意,求深度时需要将子表展开,如广义表D应该展开为((d,e),(b,(c,d))),深度为3。

表头和表尾:当广义表非空时,第一个元素为广义表的表头,其余元素组成的表示广义表的表尾。

posted @ 2021-12-19 22:01  笑忘书丶  阅读(229)  评论(0)    收藏  举报