(C++)数据结构课程笔记5/9 - 数组和广义表

174 阅读6分钟

§5 - 数组和广义表

1 - 数组

抽象数据类型

ADT Array {
    数据对象:
        D={a[j_1][j_2]...[j_i]...[j_n]|j_i=0,...,(b_i)-1, i=1,2,...,n, j_i是第i维索引值, b_i是第i维长度}
	数据关系:
		R={R_1,R_2,...,R_i,...,R_n}
    	R_k={<a[j_1][j_2]...[j_k]...[j_i]...[j_n], a[j_1][j_2]...[(j_k)+1]...[k_i]...[jn]>|j_k=0,...,(b_k)-2, j_i=0,...,(b_i)-1, i=1,2,...,k-1,k+1,...,n, j_i是第i维索引值, b_i是第i维长度, k是特定的i}
    基本操作:
    	InitArray(&A,n,bound1,...,boundn) // 初始化,n是维数,boundi是维界
    	DestroyArray(&A)                  // 销毁
    	Value(A,&e,index1,...,indexn)     // 读值
    	Assign(&A,e,index1,...,indexn)    // 赋值
} ADT Array

顺序存储表示

类型特点
  1. 没有增删操作,只有改查操作;数组建立后,元素个数与元素之间的关系不会发生变动。故可以用顺序存储表示数组;
  2. 数组是多维结构,而存储空间是一维结构。故用一组连续存储单元存放数组元素存在次序约定的问题。
两种次序约定
  1. 以行序为主序(低下标优先),C 语言采用该次序约定

    • 二维数组 a[b1][b2] 中任一元素 a[i][j] 的存储位置 LOC(i, j) = LOC(0, 0) + (b2 * i + j) * L

    • 三维数组 a[b1][b2][b3] 中任一元素 a[j1][j2][j3] 的存储位置 LOC(j1, j2, j3) = LOC(0, 0, 0) + (b2 * b3 * j1 + b3 * j2 + j3) * L

    • LOC(j1, j2, ..., jn) = LOC(0, 0, ..., 0) + (b2 * b3 * ... * bn * j1 + b3 * ... * bn * j2 + ... + j3) * L

  2. 以列序为主序(高下标优先)

    计算顺序由自左向右转换为自右向左即可,意义不大

矩阵的压缩存储

当阶数很高的矩阵中有许多值相同的元素或零元素时,为了节省空间,要对矩阵进行压缩,即把二维数组的数据元素压缩到一维数组上。这里压缩的含义是:对多个值相同的元素只分配一个存储空间零元素不分配存储空间

特殊矩阵

特点:值相同的元素或零元素在矩阵中的分布有一定规则

  1. 对称矩阵

    • 特点:n 阶方阵满足 a[i][j] = a[j][i] (0 ≤ i, j ≤ n-1)

    • 方法:对每一对对称元素只分配一个存储空间,将 n2n^2 个元素压缩存储到 n(n+1)/2n*(n+1)/2 个元素的空间中

    • 图示:

    • 公式:

      存下三角:LOC(i, j) = LOC(0, 0) + [i * (i + 1) / 2 + j] * L

      存上三角:LOC(i, j) = LOC(0, 0) + [i * (2 * n - i + 1) / 2] * L

  2. 三角矩阵

    • 特点:n 阶方阵满足下(上)三角矩阵的上(下)三角(不包括对角线)中的元均为常数
    • 方法:除了和对称矩阵一样,只存储其下(上)三角中的元之外,再加一个存储常数 C 的存储空间即可
  3. 对角矩阵

    • 特点:n 阶方阵满足所有的非零元都集中在以主对角线为中心的带状区域中,即除了主对角线上和其上下方紧邻的若干条对角线上的元之外,所有其它的元皆为零
    • 方法:以行的顺序或以对角线的顺序将其压缩存储到一维数组上,并找出每个非零元在一维数组中的对应关系
稀疏矩阵

基本操作:

  • CreateSMatrix(&M)
  • DestroySMatrix(&M)
  • PrintSMatrix(M)
  • CopySMatrix(M,&T)
  • AddSMatrix(M,N,&Q)(若稀疏矩阵 M 和 N 的行数与列数对应相等,求稀疏矩阵的和)
  • SubSMatrix(M,N,&Q)(若稀疏矩阵 M 和 N 的行数与列数对应相等,求稀疏矩阵的差)
  • MultSMatrix(M,N,&Q)(若稀疏矩阵 M 的列数等于 N 的行数,求稀疏矩阵的乘积)
  • TransSMatrix(M,&T)(求稀疏矩阵的转置)

用二维数组存储稀疏矩阵存在的问题:

  • 零值元素占了很大空间 → 期望尽可能少存或不存零值元素
  • 将进行很多和零值的运算 → 期望尽可能减少没有实际意义的运算
  • 还期望操作方便,即:尽可能快地找到对应下标值 i, j 的元素,尽可能快地找到同一行或同一列的非零元素

方法:

  1. 用三元组表示:用一个线性表来表示稀疏矩阵,表中结点对应非零元素(包括三个域:行下标、列下标和值),结点间的先后顺序按矩阵的行优先顺序排列(跳过零元素)

    #define MAXSIZE 100 // 非零元素个数上限
    
    typedef struct {
        int i,j;
        T v;
    } Triple;
    
    typedef struct {
        Triple data[MAXSIZE]; // 非零元素三元组表
    	int mu,nu,tu; // 矩阵的行数、列数和非零元素个数
    } TSMatrix;
    

    矩阵转置算法

    用二维数组表示稀疏矩阵时转置算法:

    for (int row=1;row<=mu;++row)
    	for (int col=1;col<=nu;++col)
    		N[col][row]=M[row][col];
    // 时间复杂度:O(mu*nu)
    

    用三元组表示稀疏矩阵时转置算法:

    交换矩阵的行数和列数→交换每个三元组的 i 和 j →重排三元组之间的次序(以行序为主序),实现重排有两种方法:

    • 方法1:按矩阵 M 的列序进行转置,即按对应三元组表中三元组的第二个字段值(列下标)由小到大的顺序进行转置

      void TransSMatrix(TSMatrix M,TSMatrix &T) {
      	T.mu=M.nu,T.nu=M.mu,T.tu=M.tu;
          if (M.tu) {
              int q=0; // 目标矩阵T三元组表的索引
              for (int col=1;col<=M.nu;col++) // 按矩阵M的列序进行转置
                  for (int p=0;p<M.tu;p++) // 扫描矩阵M三元组表
                      if (M.data[p].j==col) {
                          T.data[q].i=M.data[p].j;
                          T.data[q].j=M.data[p].i;
                          T.data[q].v=M.data[p].v;
                          q++;
                      }
          }
      }
      // 时间复杂度:O(tu*nu)
      
    • 方法2:按矩阵 M 的行序进行转置,即按对应三元组表的本身次序进行转置,然后按三元组表中三元组的第一个字段值(行下标)由小到大的顺序将三元组置入矩阵 T 对应三元组表中恰当的位置

      预处理:如果能够预先确定矩阵 M 中每一列(即 T 中每一行)的第一个非零元素在矩阵 T 对应三元组表中应有的位置,那么在对矩阵 M 对应三元组表中的三元组进行转置时,便可直接将其放到矩阵 T 对应三元组表中恰当的位置;附设两个向量:

      • num[col] 表示矩阵 M 中第 col 列非零元素的个数

      • cpot[col] 表示矩阵 M 中第 col 列第一个非零元素在矩阵 T 对应三元组表中恰当的位置(column position)

      • cpot[1]=0;
        for (int col=2;col<=M.nu;++col)
        	cpot[col]=cpot[col-1]+num[col-1];
        
      void TransSMatrix(TSMatrix M,TSMatrix &T) {
          T.mu=M.nu,T.nu=M.mu,T.tu=M.tu;
      	if (M.tu) {
              // 预处理
              int num[MAXCOL];
              int cpot[MAXCOL];
              for (int col=1;col<=M.nu;++col)
                  num[col]=0;
              for (int t=0;t<M.tu;++t)
                  num[M.data[t].j]++;
              cpot[1]=0;
              for (int col=2;col<=M.nu;++col)
                  cpot[col]=cpot[col-1]+num[col-1];
              
              for (int p=0;p<M.tu;p++) {
                  int col=M.data[p].j;
                  int q=cpot[col];
                  T.data[q].i=M.data[p].j;
                  T.data[q].j=M.data[p].i;
                  T.data[q].v=M.data[p].v;
                  cpot[col]++;
              }
          }
      }
      // 时间复杂度:O(tu+nu)
      
  2. 用三元组附加行指针表示:相比用三元组表示,在稀疏矩阵的结构定义中增加指示矩阵 M 中每一行的第一个非零元素在对应三元组表中的位置的向量 rpot(row position)

    typedef struct {
        int i,j;
        T v;
    } Triple;
    
    typedef struct {
        Triple data[MAXSIZE];
        int rpot[MAXROW]; // 若某行无非零元素,则该行行指针照样在其本应在的位置,即该行之前的所有行的非零元素在data中的最大下标加1
    	int mu,nu,tu;
    } TSMatrix;
    

    矩阵相乘算法

    用二维数组表示稀疏矩阵时相乘算法:

    // Q=M*N,M是m1×n1矩阵,N是m2×n2矩阵,n1=m2
    for (int i=1;i<=m1;++i)
        for (int j=1;j<=n2;++j) {
            Q[i][j]=0;
            for (int k=1;k<=n1;++k)
                Q[i][j]+=M[i][k]*N[k][j];
        }
    // 时间复杂度:O(m1*n1*n2)
    

    用三元组附加行指针表示稀疏矩阵时相乘算法:

    // 若稀疏矩阵M的列数等于N的行数,则用Q返回稀疏矩阵的乘积,并返回true;否则返回false
    bool MultSMatrix(TSMatrix M,TSMatrix N,TSMatrix &Q) {
        if (M.nu!=N.mu)
            return false;
        Q.mu=M.mu,Q.nu=N.nu,Q.tu=0; // Q.tu初值为0
        if (M.tu*N.tu!=0) {
            int sum[MAXROW],top;
            for (int row_M=1;row_M<=M.mu;++row_M) {
                Q.rpot[row_M]=Q.tu+1;
                
                for (int col=1;col<=N.nu;++col)
                    sum[col]=0;
                
                if (row_M<M.mu) // 顶部
                    top=M.rpot[row_M+1]-1;
                else
                    top=M.tu-1;
                
                for (int j=M.rpot[row_M];j<=top;++j) {
                    int row_N=M.data[j].j,t;
                    if (row_N<N.mu) // 顶部
                        t=N.rpot[row_N+1]-1;
                    else
                        t=N.tu-1;
                    for (int k=N.rpot[row_N];k<=t;++k) {
                        /*
                         * 示意图:
                         *         col 
                         * O O O O	- - -
                         * - O O O	O O O
                         * O O O O	O O O
                         *          O O O
                         *    M       N
                         * 由此可见,完全避免了与0有关的乘法
                         */
                        int col=N.data[k].j;
    					sum[col]+=M.data[j].v*N.data[k].v;
                    }
                }
                
                for (int col=1;col<=N.nu;++col) // 抄答案
                    if (sum[col]) {
                        Q.data[Q.tu].i=row_M;
    					Q.data[Q.tu].j=col;
    					Q.data[Q.tu].v=sum[col];
                        ++Q.tu;
    				}
            }
        }
        return true;
    }
    
  3. 用带行指针向量的单链表表示:将稀疏矩阵中每一行的非零元素分别链接成一个单链表,表中结点对应该行非零元素(包括三个域:列下标、值和指针),附设一个作为各单链表头指针的行指针向量

    类似地,也可以用带列指针向量的单链表表示

  4. 用带行指针向量和列指针向量的十字链表表示:结合了带行指针向量的单链表表示法和带列指针向量的单链表表示法,表中结点可以包括五个域:行下标、列下标、值、行指针和列指针

2 - 广义表

相关概念

  1. 广义表(Generalized list)是 n 个单元素(或称原子)或子广义表 α1, α2, …, αn 的有限序列,记作 LS = (α1, α2, …, αn),约定用小写字母表示原子、用大写字母表示广义表
  2. 广义表是线性表的推广,线性表是广义表的特例;广义表是递归定义,其运算通常也用递归函数完成
  3. 广义表的长度为最外层的元素个数,深度为层数即所含括弧的最大重数
  4. 当广义表非空时,称第一个元素 α1 为表头(Head),其余元素组成的广义表 (α2, α3, …, αn) 为表尾(Tail)

例子:

  • A = ()

    A 是空表,长度为 0,深度为 1

  • B = (a, b, c, d)

    B 是由四个原子组成的广义表,长度为 4,深度为 1

    Head(B) = a,Tail(B) = (b, c, d)

  • C = (a, (b, c, d))

    C 是由一个原子和一个子表组成的广义表,长度为 2,深度为 2

    Head(C) = a,Tail(C) = ((b, c, d))

  • D = (A, B, C) = ((), (a, b, c, d), (a, (b, c, d)))

    D 是由三个子表组成的广义表,长度为 3,深度为 3

    Head(D) = A,Tail(D) = (B, C)

  • E = (a, E) = (a, (a, (a, (...))))

    E 是由一个原子和一个子表组成的广义表,长度为 2,深度为无穷,它是一个无限递归的广义表

    Head(E) = a,Tail(E) = (E)

抽象数据类型

ADT Generalized list {
	数据对象:
		D={e_i|e_i为原子且e_i∈AtomSet,或e_i为广义表,i=1,2,...,n}
	数据关系:
		R={<e_i-1,e_i>|e_i-1,e_i∈D,i=2,...,n}
	基本操作:
		创建和销毁操作
		InitGList(&L)
		DestroyGList(&L)
		CopyGList(&T,L)
		
		插入和删除操作
		InsertFirst(&L,e) // 插入e,作为第一个元素
		DeleteFirst(&L,&e) // 删除第一个元素,用e返回
		
		查询函数
		GListEmpty(L)
		GListLength(L)
		GListDepth(L)
		GListHead(L)
		GListTail(L)
		
		遍历函数
		TraverseGList(L,visit())
} ADT Generalized list

广义表的链式存储表示

由于广义表中数据元素可以是原子或广义表,因此难以用顺序存储结构表示;通常采用链式存储结构,并设置表结点原子结点两类结点,每个数据元素用一个结点表示;广义表的链式存储结构常见的两种表示方法为:

表头表尾表示法
  • 表结点包括三个域:结点类型标记 tag(为 1)、指向表头的指针 hp 和指向表尾的指针 tp

    原子结点包括三个域:结点类型标记 tag(为 0)和值 value

  • 除空表的表指针为空外,表指针均指向一个表结点,该结点的 hp 域指示该表表头、tp 域指示该表表尾(除非表尾为空,否则必为表结点)

  • 例子

    • A = ()

      表示为:A = NULL

    • B = (a, b, c, d)

      表示为:

    • C = (a, (b, c, d))

      表示为:

    • D = (A, B, C) = ((), (a, b, c, d), (a, (b, c, d)))

      表示为:

    • E = (a, E)

      表示为:

  • 优点:容易分清层次;广义表的长度为最高层中表结点的个数,深度为最大层次数减 1;因此,对于求长度、求深度、求表头、求表尾等相关操作很方便

    缺点:表结点个数多,空间效率低;表结点个数和括号对数不匹配

子表表示法
  • 表结点包括三个域:结点类型标记 tag(为 1)、指向子表中第一个元素的指针 head 和指向下一个元素的指针 next

    原子结点包括三个域:结点类型标记 tag(为 0)、值 value 和指向下一个元素的指针 next

  • 一个表结点对应一个括号

  • 例子

    • A = ()

      表示为:

    • B = (a, b, c, d)

      表示为:

    • C = (a, (b, c, d))

      表示为:

    • D = (A, B, C) = ((), (a, b, c, d), (a, (b, c, d)))

      表示为:

    • E = (a, E)

      表示为:

  • 优点:表结点个数少;表结点个数和括号对数一致

    缺点:写递归算法不方便,例如求深度:

    int GListDepth(GList L) {
        if (!L)
            return 0;
        int max=0;
        for (GList p=L;p;p=p->next) {
            int dep=0;
            if (L->tag==1)
                dep=GListDepth(p->head);
            if (dep>max)
                max=dep;
        }
        return max+1;
    }