数据结构之线性表7:数组和广义表

852 阅读10分钟

数组

实际上是一组有固定个数的元素的集合。看成一般线性表的扩充,一维数组就是线性表,二维数组定义为:每个数据元素都是一个一维数组的的线性表。 所以一般操作只有两类:获取和修改特定位置的值,所以主要采用顺序结构。

元素总数=上限-下限+1

存储和实现

由于多维数组还是要存在线性区域内,所以先存行还是列的存储分为两种

  • 行序存储:BASIC、COBOL、C语言和PASCAL等高级语言
  • 列序存储:FORTRAN等高级语言 若三维数组: 行序就是一层一层楼,列序就是一片一片切菜

一维数组的地址计算

数组中每个元素占size个存储单元,则元素ai存储地址为:

Loc(a[i])=Loc(a[1])+(i-1)*size

所求地址是基地址+该下标变量与基地址之间序号。 由于同类型,所以size确定。

二维数组的地址计算

例如二维数组Amn(m行n列),以行序存储,此处注意设首元素下标从1开始:a[1][1]。

所求位置=基地址(数组名A[1][1])+前i-1行所占空间+不足一行的列零头。

设每个元素占一个存储单元:

Loc(a[i][j])=Loc(a[1][1]) + n*(i-1) + (j-1)

设每个元素占size个存储单元:

Loc(a[i][j])=Loc(a[1][1]) + n*(i-1)*size + (j-1)*size

三维数组的地址计算

  • 特殊情况

例如三维数组Amnr(m行n列r层),以行序存储,下限从首元素:a111开始,地址为LOC[1,1,1]。

每个元素占size个存储单元(1≤i≤r,1≤j≤m,1≤k≤n),i是层数,j是行数,k是列数

所求位置=首地址+(i-1)整层数+(j-1)整行数+不足一行的列零头(从1,1,1开始的特殊情况)

Loc[i][j][k]=Loc[1,1,1]+(i-1)*m*n*size+(j-1)*n*size+(k-1)*size

  • 普遍情况

将三维数组推广到一般情况

j1,j2,j3的下限为c1,c2,c3上限为d1,d2,d3j1是层数,j2是行数,j3是列数。每个元素占size个存储单元a(j1,j2,j3)的地址为:

Loc(a[j1][j2][j3])=Loc(a[c1][c2][c3])+((j1-c1)*(d2-c2+1)*(d3-c3+1)+(j2-c2)*(d3-c3+1)+(j3-c3))*size

这里+1是因为包含上下限

其实就是某一块不规则体积

压缩存储

原则:对有规律的元素和值相同的元素只分配一个存储空间,对于零元素不分配空间

三角矩阵(分界线左上到右下)

下三角矩阵

  • 当i<j时,aij=0,上面都是0

  • 开辟一个B[k]数组,k=n*(n+1)/2(等差数列)。分别存a11,a21,a22,a31...  

需要将aij和B[i]联系起来:

对于下三角矩阵,按“行序为主序”进行存储,得到的序列 为:a11,a21,a22,a31,a32,a33...an1,an2...ann由于下三角矩阵的元素个数为n(n+1)/2,所以可压缩存储到一个大小为n(n+1)/2的一维数组中。下三角矩阵中元素aij(i>j),在一维数组A中的位置为:

LOC[i,j]=LOC[1,1]+i(i-1)/2+j-1 == 基地址+整行数+不足的列数

上三角矩阵

若i>j时,有aij=0,下面都是0

  • 对称矩阵:所有元素满足aij=aji,对角线轴对称

LOC[i,j]=LOC[1,1]+j(j-1)/2+i-1

基地址+所求左上三角形+所求列左边的

带状矩阵(对角线对称分布)

所有非零元素都集中在以对角线为中心的带状区域。(下图是常见的三对角带状矩阵)

三对角带状矩阵下标特征:

i=1 , j=1,2;
1<i<n , j=i-1,j=i,j=i+1;
i=n , j=i-1,i;

因此三对角带状矩阵所需要的空间大小为:3n-2

计算列数:若j-i=-1,元素是一行最左边,需要+1;若j-i=0,元素是一行中间,需要+1;若j-i=-1,元素是一行最右边,需要+1;

LOC[i,j]=LOC[1,1]+(i-1)-1+j(j-1)/2+i-1=LOC[1,1]+2(i-1)+j+1

稀疏矩阵

非零元素个数一般低于总数的1/4到1/3

三元组表表示

将一个元素的位置和值储存在一个结构体内:

而三元组表形式的一个重要操作就是对稀疏矩阵进行转置操作:

最简单的矩阵转置算法:

void TranMatrix(ElementType source[n][m],ElementType dest[n][m])
{int i,j;
 for(i=0;i<m;i++)
 { for(j=0;j<n;j++)
    dest[i][j]=source[j][i];
 }
}

默认A矩阵是源矩阵,B矩阵是转置后的目标矩阵。

为了保证转置后的矩阵的三元组表B也是以“行序为主序”进行存放,则需要对行、列互换后的三元组B,按B的行下标(即A的列下标)大小重新排序,但排序时间开销较大。

列序递增转置法

默认A矩阵是源矩阵,B矩阵是转置后的目标矩阵。且转置后的列序其实就是转置前的行序。

思想:按照三元组表A的列序递增的顺序转置,当B中行号为1时扫描A三元组,从小到大找出A中列号为1的所有元素;然后当B中行号为2...

算法主体:

void TransposeTSMatrix(TSMatrix A,TSMatrix *B)
{/*矩阵A转置到矩阵B中,矩阵使用三元组表表示,长宽分别存在m,n中,行数row,列数col,值v*/
int i,j,k;		//iA表中元素的位置,j是B表中元素的位置,k是A的外部循环
B->m=A.n;		//B的行数=A的列数
B->n=A.m;		//B的列数=A的行数
B->len=A.len;		//B的三元组表长=A的表长
if(B->len>0)
	{j=1;				//三元组表B的行号从1开始
     	 for(k=0;k<=A.n;k++)		//A的列数123...n列
        	{
      		   for(i=1;i<=A.len;i++)	//从A的表头找到表尾,看谁等于k
            		{    if(A.data[i].col==k)	//当A的列数等于k时,把这个元素存入B  
            			{	B->data[j].row=A->data[i].col;	//Bi个元素的行数等于A[j]的列数,i是断的,j是一个一个接着的
                   			B->data[j].col=A->data[i].row;	//B的列数等于A的行数
                    			B->data[j].v=A->data[i].v;	//B的值等于A的值
                    			j++;				//B三元组表进入下一个元素
                 		}
            		}
            	}
	}
}
时间复杂度=O(A.n x A.len) 

未深入讨论快速转置算法,相关代码及讲解:

www.bilibili.com/video/BV1kx… www.bilibili.com/video/BV1kx…

算法思想:

插入一个“统计选票”的例子:20000张选票,都是有效票,投给10个候选人,统计各人所得票数
思路:给十个人编号并将选票数分别存在c[10]10个元素中
主体:
for(i=0;i<20000;i++)
{
	scanf("%d",&x);		//投给编号为x的人一票
  	c[x]++;			//直接给该编号内数值+1
}

通过一重循环完成转置,即对A中所有非零元“一次定位”直接放到B三元组表的正确位置。故需要设置position[ ]和num[ ]两个数组,存放如下预先计算的值,以实现一次定位。

position[col] 存放A三元组第col列中第一个非零元素的位置,即A三元组中第某列在B三元组中第一个元素的位置。

num[col] 存放A三元组第col列非零元素个数,即1列有多少个元素,多少个元素后换列。

类似之前的统计选票问题,若循环到某列的元素,就给num[某]的值加1,最后循环存入的时候每存一个就给position[某]的值加1

时间复杂度=O(A.n + A.len)

十字链表

该结构除了和三元组一样的 行,列,值 三个内容,还存储两个指针,分别名叫down和right,意为链接同一行的下一个非零元素/链接同一列的下一个非零元素

举个例子!

其最终十字链表形式为下图:

结点结构声明

typedef struct OLNode
{int row,col;			//行下标和列下标
 ElementType value;
 struct OLNode *right,*down;	
}OLNode,*OLink;

链表结构声明

typedef struct
{OLink *row_head,*col_head;		//分别为行链表头指针和列链表头指针
  int m,n,len;				//存储行数,列数,非零元素个数
}CrossList;

存入(插入)算法描述

CreateCrossList (CrossList*M)
{ scanf(&m,&n,&t);		//输入M的行数,列数和非零元素的个数
  M->m=m;M->n=n;M->len=t;
  if(!(M->row_head=(OLink*)malloc((m+1)sizeof(OLink)))) exit(OVERFLOW); 	//申请了m+1个OLink结构的链表并赋给m的行头指针,若不成功则退出
  if(!(M->col_head=(OLink*)malloc((n+1)sizeot(OLink)))) exit(OVERFLOW);		//与上同理,意为创建相应个数的表头结点
  M->row_head[]=M->col_head[]=NULL;						//初始化行、列头指针,使各行列链表为空
  for(scanf(&i,&j,&e);i!=O;scanf(&i,&j,&e))					//若输入i不为0,就不断输入
  {
     if(!(p=(OLNode *) malloc(sizeof(OLNode)) exit(OVERFLOw);
     p->row=i;		//生成结点并赋值
     p->col=j;
     p->value=e;
     
     /*以下为分别插入行和列*/
     if(M->row_head[i]==NULL)		//如果行表是空的就直接插
     	M->row_head[i]=p;
     else
     {/*若行表不是空的就需要寻找行表中的插入位置*/  
       for(q=M->row_head[i];q->right&&q->right->col<j;q=q->right)			//若右边仍有值并且小于列下标j,则右移
       p->right=q->right;q->right=p;			//完成插入
     }
     
      if(M->col_head[j]==NULL)		//如果列表是空的就直接插
     	M->col_head[j]=p;
     else
     {/*若列表不是空的就需要寻找行表中的插入位置*/  
       for(q=M->col_head[j];q->down&&q->down->row<i;q=q->down)			//若下边仍有值并且小于行下标i,则下移
       p->down=q->down;q->down=p;			//完成插入
     }
  }
}    
时间复杂度= O(t*s)  s=max(m,n)

广义表

广义表的概念

广义表也是线性表的一种推广。广义表也是n个数据元素(d1,d2,d3,....dn)的有限序列,但不同的是,广义表中的di,既可以是单个元素,还可以是一个广义表,通常记作: GL=(d1,d2,d3,....dn),可以像套娃一样一个元素里面塞一个表。

广义表中默认:大写ABC...是表名,小写abc...是元素名

GL是广义表的名字,通常用大写字母表示。n是广义表的长度,这个广义表的表头是d1,表尾是除了表头以外其余元素构成的表,一定是个表。 若 di 是一个广义表,则称表 di 是广义表GL的子表。

一个练习题,其中每句话并不独立: 补充:head(C)=a,tail(C)=空

  • 广义表的元素可以是子表,而子表还可以是子表....由此,广义表是一个多层的结构。
  • 广义表可以被其他广义表共享。如:广义表B就共享表A。在表B中不必列出表A的内容,只要通过子表的名称就可以引用该表。
  • 广义表具有递归性,如上图的广义表C。

广义表的存储结构

广义表中有两种结点:单个元素结点子表结点。任何一个非空广义表都可以分解成表头和表尾,同样,一对确定的表头和表尾可唯一确定一个广义表。

  • 元素结点 需要两个域:标志域值域其中标志域=0

  • 表结点 需要三个域:标志域指向表头的指针域指向表尾的指针域,其中标志域=1

  • 广义表的长度定义为最外层所包含元素个数

  • 广义表的深度定义为该表展开后括号的层数,A= (b, c)只有1层括号,深度为1。B=(A, d=((b,c)c)有2层括号,深度为2。C=(f,B,h)=(f,((b,c)c),h)有3层括号,深度为3。D=()深度为0

  • 递归表深度无限,长度是有限值

  • 优先满足表头,表尾更可以空

举个例子:

  • A(a,(b,c)); 解读:A先是一个表,表头指向元素a,表尾指向一个表,然后这个表内又有表头和表尾,表头是b,表尾必是一个表,然后表头指针指向c,表尾指针指向空。长度为2

  • B(A,A,D); D( ); D是空表 解读:B先是一个表,表头指向表A,表尾指向一个表,这个表的表头指向A,表尾必指向一个表,指向的表D是空表,所以剩下两个域都是空。 长度为3

  • C=(a,C);

解读:这个广义表中包含了递归操作,首先C是一个表,表头指向元素a,表尾必指向一个表,这个表的表头是C,实现嵌套内含a和C的表,表尾无穷尽,所以表尾为空。 长度为2

E=(( )) 这是长度为1,表头和表尾均为()

头尾链表存储结构:

扩展线性链表存储结构:

以上为广义表结构,不详细展开。 www.bilibili.com/video/BV1kx…

广义表的操作(了解)

表头指针hp,表尾指针tp

求广义表表头

GList Head(GList L)
{ if(L==NULL)
	return(NULL);			//若L空,则返回空
  if(L->tag==ATOM)			
  	exit(0);			//标志位等于元素结点,说明本身就是单元素,返回0
   else 
   	return (L->atom_htp.htp.hp);	//若不是这些,则返回表头指针
}

复制广义表 求广义表的长度

int Length(GList L)
{ int n=0; GLNode *s;
  if(L==NULL)
     return(0);
  if(L->tag==ATOM)
     exit(O);
 
 /*并非一个元素结点的时候*/
  s=L;
  while(s!=NULL)
  { k++; 
  s=s->atom_htp.htp.tp;
  }

return(k);
}

总结

数组

  • n维数组可以看成是每个数据元素均是一个n-1维数组的线性表。
  • 数组是一组有固定个数元素的集合。给出维数和每一维的上下限,数组中的元素个数就固定了。
  • 数组采用顺序存储结构,主要操作是随机存取,即给定元素的下标,得到该元素在计算机中的存放位置。

特殊矩阵:

  1. 元素分布有规律的矩阵。只需找到对应规律的函数,就可由二维矩阵A中元素aij的下标计算出一维内存空间地址值K,实现二维矩阵到压缩存储后的一维数组的存储映射。
  2. 非零元素很少的稀疏矩阵,只存非零元素所在的行号,列号及元素值来实现压缩存储。

广义表:

  • 广义表是n个元素(dt,d,d3,...,dn)的有限序列,d既可以是单个元素,也可以是广义表。广义表的定义具有递归性,其操作通常采用递归实现。
  • 一个非空的广义表GL可以看成是由表头和表尾构成。第一个元素称为表头,除表头以外的其余元素构成表尾。
  • 常用的存储方式有:头尾链表存储结构和扩展线性链表存储结构。