数据结构——第3章线性表

第3章 线性表

内容来自书籍——大话数据结构第三章。

线性表(List):零个或多个数据元素的有限序列。

3.1线性表的定义

它是一个序列,元素之间是有顺序的;

若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。

线性表强调是有限的,元素个数是有限的。事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。

如果用数学语言来进行定义:

在这里插入图片描述
在这里插入图片描述
在较复杂的线性表中,一个数据元素可以由若干个数据项组成。

3.2线性表的抽象数据类型

常用操作:

  • 线性表的创建和初始化过程
  • 线性表重置为空表
  • 根据位序得到数据元素
  • 查找某个元素是否存在
  • 获得线性表的长度
  • 加入一个新的元素到队列中(插入数据)
  • 移除队列中的一个元素(删除数据)

线性表的抽象数据类型定义如下:

ADT 线性表(List)
Data
	线性表的数据对象集合为(a1,a2,...,an),每个元素的类型均为DataType。其中,除第
	一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个
	元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。

Operation
	InitList(*L);	初始化操作,建立一个空的线性表L
	ListEmpty(L);	若线性表为空,返回true,否则返回false
	ClearList(*L);	将线性表清空
	GetElem(L,i,*e);	将线性表L中的第i个位置元素值返回给e
	LocateElem(L,e);	在线性表L中查找与给定值e相等的元素,如果查找成功,返回
						该元素在表中序号表示成功;否则,返回0表示失败 
	ListInsert(*L,i,e);		在线性表L中的第i个位置插入新元素e
	ListDelete(*L,i,*e);	删除线性表L中第i个位置元素,并用e返回其值
	ListLength(L);		返回线性表L的元素个数
endADT 

对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的。
对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。
在这里插入图片描述
只要循环集合B中的每个元素,判断当前元素是否存在A中,若不存在,则插入到A中即可。

假设La表示集合A,Lb表示集合B,实现代码如下:

/* 将所有的在线性表Lb中但不在La中的数据元素插入到La中 */
void union(List *La,List Lb)
{
    
    
	int La_len,Lb_len,i;
	ElemType e;				/* 声明La和Lb相同的数据元素e */
	La_len = ListLength(La);	/*求线性表的长度*/
	Lb_len = ListLength(Lb);
	for(i=1;i<=Lb_len;i++)
	{
    
    
		GetElem(Lb,i,e);	/* 取Lb中第i个数据元素赋给e*/
		if(!LocateElem(La,e,equal))	/*La中不存在和e相同数据元素*/
			ListInsert(La,++La_len,e);	/*插入*/	
	}
}

对于union操作,用到了前面线性表基本操作ListLength、GetElem、LocateElem、ListInsert等,可见对于复杂的个性化操作,其实就是把基本操作组合起来实现的。

3.3线性表的顺序存储结构

3.3.1顺序存储定义

线性表的两种物理结构的第一种——顺序存储结构。

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素

在这里插入图片描述

3.3.2顺序存储方式

线性表的顺序存储结构,就是在内存中找了块地址,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地上。

线性表的每个数据元素的类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。

为了建立一个线性表,要在内存中找一块地,于是这块地的第一个位置就非常关键,它是存储空间的起始位置
线性表中,我们估算这个线性表的最大存储容量,建立一个数组,数组的长度就是这个最大存储容量。

线性表已经有了起始的位置,也有了最大的容量,于是我们就可以在里面增加数据了。随着数据的插入,线性表的长度开始变大,不过线性表的当前长度不能超过存储容量,即数组的长度

线性表的顺序存储的结构代码:

#define MAXSIZE 20		/*存储空间初始分配量*/
typedef int ElemType;	/*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct
{
	ElemType data[MAXSIZE];		/*数组存储数据元素,最大值为MAXSIZE*/
	int length;					/*线性表当前长度*/
}SqList;

描述顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
  • 线性表的最大存储容量:数组长度MaxSize
  • 线性表的当前长度:length

3.3.3数据长度与线性表长度区别

数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。(数组的大小不一定不可以变,动态分配的一维数组:一般高级语言,比如C、VB、C++都可以用编程手段实现动态分配数组,不过这会带来性能上的损耗)

线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。

在任意时刻,线性表的长度应该小于等于数组的长度。

3.3.4地址计算方法

由于我们数数都是从1开始数的,线性表的定义也不能免俗,起始也是1,可C语言中的数组却是从0开始第一个下标的,于是线性表的第i个元素是要存储在数组下标为i-1的位置,即数组元素的序号和存放它的数组下标之间存在对应关系。
在这里插入图片描述
用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。

内存中的地址,都是有编号的。存储器中的每个存储单元都有自己的编号,这个编号称为地址。

由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数):
在这里插入图片描述
通过这个公式,可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间。那么对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用算法中学到的时间复杂度的概念来说,它的存取时间性能为0(1)。通常把具有这一特点的存储结构称为随机存储结构。

3.4顺序存储结构的插入与删除

3.4.1获得元素操作

对于线性表的顺序存储结构来说,如果要实现GetElem操作,即将线性表L中的第i个位置元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可,代码:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表L已存在,1<=i<=ListLength*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(SqlList L,ElemType *e)
{
    
    
	if(L.length==0||i<1||i>L.length)
		return ERROR;
	*e=L.data(i-1);
	return OK;
} 

Status是一个整型,返回OK代表1,ERROR代表0。

3.4.2插入操作

插入算法的思路:

  • 如果插入位置不合理,抛出异常
  • 如果线性表长度大于等于数组长度,则抛出异常或动态增加异常
  • 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
  • 将要插入元素填入位置i处
  • 表长加1

实现代码如下:

/*线性表插入元素
初始条件:顺序线性表L已存在,1<=i<=ListLength(L),
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/

Status ListInsert(SqList *L,int i,ElemType e)
{
    
    
	int k;
	if(L->length==MAXSIZE)	/*顺序线性表已经满*/
		return ERROR;
	if(i<1||i>L->length+1)	/*当i不在范围内时*/
		return ERROR;
	if(i<=L->length)	/*若插入数据位置不在表尾*/
	{
    
    
		for(k=L->length-1;k>=i-1;k--)	/*将要插入位置后数据元素向后移动一位*/
			L->data[k+1]=L->data[k];
	}
	L->data[i-1]=e;		/*将新元素插入*/
	L->length++;
	return OK;
}

3.4.3 删除操作

删除算法的思路:

  • 如果删除位置不合理,抛出异常;
  • 取出删除元素;
  • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
  • 表长减1

实现代码如下:

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i,ElemType *e)
{
    
    
	int k;
	if(L->length==0)		/*线性表为空*/
		return ERROR;
	if(i<1||i>L->length)	/*删除位置不正确*/
		return ERROR;
	*e=L->data[i-1];
	if(i<L->length)			/*如果删除不是最后位置*/
	{
    
    
		for(k=i;k<L->length;k++)	/*将删除位置后继元素前移*/
			L->data[k-1]=L->data[k];
	}
	L->length--;
	return OK;
}

插入和删除的时间复杂度:
最好的情况,如果元素要插入到最后一个位置,或者删除最后一个元素,此时时间复杂度为O(1),因为不需要移动元素的。
最坏的情况呢,如果元素要插入到第i个位置,或删除第i个元素,需要移动n-i个元素。根据概率原理,每个位置插入或删除元素的可能性是相同的,也就是说位置靠前,移动元素多,位置靠后,移动元素少。最终平均移动次数和最中间的那个元素的移动次数相等,为n-1/2。
平均时间复杂度还是O(n)。
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。

3.4.4线性表顺序存储结构的优缺点

线性表顺序存储结构的优缺点:
优点

  • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
  • 可以快速地存取表中任一位置地元素

缺点

  • 插入和删除操作需要移动大量元素
  • 当线性表长度变化较大时,难以确定存储空间的容量
  • 造成存储空间的“碎片”

3.5线性表的链式存储结构

3.5.2线性表链式存储结构定义

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置

之前在顺序结构中,每个数据元素只需要存数据信息就可以啦。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
因此,为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。

n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2…an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起,如图:
在这里插入图片描述
把链表中第一个结点的存储位置叫做头指针,整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。

线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。

图示:
在这里插入图片描述
为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针,如图:
在这里插入图片描述

3.5.3头指针与头结点的异同

头指针与头结点的异同点:

头指针

  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头节点的指针
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素

头结点

  • 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
  • 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
  • 头结点不一定是链表必须要素

3.5.4线性表链式存储结构代码描述

若线性表为空表,则头结点的指针域为”空“,如图:
在这里插入图片描述
这里大概的用图示表达了内存中单链表的存储状态,它不是在内存中的实际位置,这只是它所表示的线性表中的数据元素及数据元素之间的逻辑关系,所以改用更方便的存储示意图来表示单链表,如图:
在这里插入图片描述
若带有头结点的单链表,则如图:
在这里插入图片描述
空链表如图:
在这里插入图片描述
单链表中,在C语言中可用结构指针来描述:

/*线性表的单链表存储结构*/
typedef struct Node
{
    
    
	ElemType data;
	struct Node *next;
}Node;
typedef struct Node *LinkList;	/*定义LinkList*/

结点由存放数据元素的数据域,存放后继节点地址的指针域组成

假设p是指向线性表第i个元素的指针,则该结点ai的数据域可以用p->data来表示,p->data的值是一个数据元素,结点ai的指针域可以用p->next来表示,p->next的值是一个指针。p->next指向第i+1个元素,即指向ai+1的指针。

也就是说,如果p->data=ai,那么p->next->data=ai+1。如图:
在这里插入图片描述

3.6单链表的读取

在单链表中,由于第i个元素到底在哪没办法一开始就知道,必须得从头开始找,因此,对于单链表实现获取第i个元素的数据的操作GetElem,在算法上,相对要麻烦一些。

获得链表第i个数据的算法思路:

  1. 声明一个结点p指向链表第一个结点,初始化j从1开始;
  2. 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
  3. 若到链表末尾p为空,则说明第i个元素不存在;
  4. 否则查找成功,返回结点p的数据。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType *e)
{
    
    
	int j;
	LinkList p;		/*声明一结点*/
	p = L->next; 		/*让p指向链表L的第一个结点*/
	j = 1;		/*j为计数器*/
	while(p && j<i)	/*p不为空或者计数器j还没有等于i时,循环继续*/
	{
    
    
		p=p->next;	/*让p指向下一个结点*/
		++j; 
	}
	if(!p||j>i)
		return ERROR;	/*第i个元素不存在*/
	*e=p->data;		/*取第i个元素的数据*/
	return OK;
}

其实就是从头开始找,直到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度是O(n)

由于单链表的结构中并没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for循环来控制循环。其主要核心思想就是”工作指针后移“,这也是很多算法的常用技术。

3.7单链表的插入和删除

3.7.1单链表的插入

假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系的变化,只需将结点插入到结点p和p->next之间即可。

根本用不着惊动其他结点,只需要让s->next和p->next的指针做一点改变即可。

s->next=p->next;
p->next=s;

这两句的顺序不可以交换
插入结点s后,链表如图:
在这里插入图片描述
对于单链表的表头和表尾的特殊情况,操作是相同的,如图:
在这里插入图片描述
单链表第i个数据插入结点的算法思路:

  1. 声明一结点p指向链表第一个结点,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  3. 若到链表末尾p为空,则说明第i个元素不存在;
  4. 否则查找成功,在系统中生成一个空结点s;
  5. 将数据元素e赋值给s->data;
  6. 单链表的插入标准语句s->next=p->next; p->next=s;
  7. 返回成功。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList *L,int i,ElemType e)
{
    
    
	int j;
	LinkList p,s;
	p=*L;
	j=1;
	while(p&&j<i)	/*寻找第i个结点*/
	{
    
    
		p=p->next;
		++j; 
	}
	if(!p||j<i)
		return ERROR;	/*第i个元素不存在*/
	s=(LinkList)malloc(sizeof(Node));	/*生成新结点(C标准函数)*/
	s->data=e;
	s->next=p->next;	/*将p的后继结点赋值给s的后继*/
	p->next=s;			/*将s赋值给p的后继*/
	return OK;
}

C语言的malloc标准函数,它的作用就是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备用来存放e数据s结点。

3.7.2单链表的删除

设存储元素ai的结点为q,要实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可,如图:
在这里插入图片描述
实际上要做的只有一步:
p->next=p->p->next->next,用q来取代p->next,即是

q=p->next;p->next=q->next;

让p的后继的后继结点改成p的后继结点。

单链表第i个数据删除结点的算法思路:

  1. 声明一结点p指向链表第一个结点,初始化j从1开始;
  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
  3. 若到链表末尾p为空,则说明第i个元素不存在;
  4. 否则查找成功,将欲删除的结点p->next赋值给q;
  5. 单链表的删除标准语句p->next=q->next
  6. 将q结点中的数据赋值给e,作为返回;
  7. 释放q结点;
  8. 返回成功。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1<=L<=ListLength(L)
操作结果:删除L的第i个数据要素,并用e返回其值,L的长度减1*/
Status ListDelete(LinkList *i,int i,ElemType *e)
{
    
    
	int j;
	LinkList p,q;
	p=*L;
	j=1;
	while(p->next&&j<i)		/*遍历寻找第i个元素*/
	{
    
    
		p=p->next;
		++j;
	}
	if(!(p->next)||j<i)
		return ERROR;		/*第i个元素不存在*/
	q=p->next;
	p->next=q->next;		/*将q的后继赋值给p的后继*/
	*e=q->data;			/*将q结点中的数据给e*/
	free(q);		/*让系统回收此结点,释放内存*/
	return OK;
}

C语言的另一个标准函数free,它的作用就是让系统回收一个Node结点,释放内存。

单链表插入和删除算法,它们其实都是由两部分组成:第一部分就是遍历查找第i个元素;第二部分就是插入和删除元素。

从整个算法来说,很容易推导出,它们的时间复杂度都是O(n)。如果不知道第i个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个元素,每次都是O(n)。而单链表,只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

3.8单链表的整表创建

单链表和顺序存储结构不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。

所以创建单链表的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。

单链表整表创建的算法思路:

  1. 声明一结点p和计数器变量i;
  2. 初始化一空链表L;
  3. 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
  4. 循环:
    • 生成一新结点赋值给p;
    • 随机生成一数字赋值给p的数据域p->data;
    • 将p插入到头结点与新一结点之间

实现代码算法如下:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead(LinkList *L,int n)
{
    
    
	LinkList p;
	int i;
	srand(time(0));			/*初始化随机数种子*/
	*L=(LinkList)malloc(sizeof(Node));
	(*L)->next=NULL;	/*先建立一个带头结点的单链表*/
	for(i=0;i<n;i++)
	{
    
    
		p=(LinkList)malloc(sizeof(Node));	/*生成新结点*/
		p->data=rand()%100+1;		/*随即生成100以内的数字*/
		p->next=(*L)->next;
		(*L)->next=p;		/*插入到表头*/
	}
}

这段算法代码里,其实用的是插队的办法,就是始终让新结点在第一的位置,也可以把这种算法简称为头插法。如图:
在这里插入图片描述
也可以把新结点放到最后,这才是排队时的正常思维,所谓的先来后到。每次新结点都插在终端结点的后面,这种算法称之为尾插法。

实现代码算法如下:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L,int n)
{
    
    
	LinkList p,r;
	int i;
	srand(time(0));		/*初始化随机数种子*/
	*L=(LinkList)malloc(sizeof(Node));	/*为整个线性表*/
	r=*L;			/*r为指向尾部的结点*/
	for(i=0;i<n;i++)
	{
    
    
		p=(Node *)malloc(sizeof(Node));	/*生成新结点*/
		p->data=rand()%100+1;		/*随机生成100以内的数字*/
		r->next=p;			/*将表尾终端结点的指针指向新结点*/
		r=p;		/*将当前的新结点定义为表尾终端结点*/
	}
	r->next=NULL;	/*表示当前链表结束*/
}

注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。

r->next=p;就是将刚才的表尾终端结点r的指针指向新结点p,如图。①位置的连线就是表示这个意思。
在这里插入图片描述
r=p如图:
在这里插入图片描述
本来r是在ai-1元素的结点,但现在它已经不是最后的结点了,现在最后的结点是ai,所以应该要将p结点这个最后的结点赋值给r,此时r又是最终的尾结点了。

循环结束后,那么应该让这个链表的指针域置空,因此有了“r->next=NULL;”,以便以后遍历时可以确认其是尾部。

3.9单链表的整表删除

当不打算使用这个单链表时,需要把它销毁,其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。

单链表整表删除的算法思路如下:

  1. 声明一结点p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
    • 将下一结点赋值给q;
    • 释放p;
    • 将q赋值给p。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList *L)
{
    
    
	LinkList p,q;
	p=(*L)->next;		/*p指向第一个结点*/
	while(p)		/*没到表尾*/
	{
    
    
		q=p->next;
		free(p);
		p=q;
	}
	(*L)->next=NULL;	/*头结点指针域为空*/
	return OK;
}

p是一个结点,它除了有数据域,还有指针域。在做free§;时,其实是在对它整个结点进行删除和内存释放的工作。

3.10单链表结构与顺序存储结构优缺点

在这里插入图片描述

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构;若需要频繁插入和删除时,宜采用单链表结构。
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,这种用顺序存储结构效率会高很多。

线性表的存储结构和单链表结构各有其优缺点,不能简单的说哪个好,哪个不好,需要根据实际情况,来综合平衡采用哪种数据结构更能满足和达到需求和性能。

3.11静态链表

用数组代替指针,来描述单链表。
让数组的元素都是由两个数据域组成,data和cur。数组的每一个下标都对应一个data和一个cur。
数据域data,用来存放数据元素,也就是通常我们要处理的数据;而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。

把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。

为了方便插入数据,通常会把数组建立的大一些,以便有一些空闲空间可以便于插入时不至于溢出。
代码实现:

/*线性表的静态链表存储结构*/
#define MAXSIZE 1000	/*假设链表的最大长度是1000*/
typedef struct
{
    
    
	ElemType data;
	int cur;			/*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList(MAXSIZE); 

对数组第一个和最后一个元素作为特殊元素处理,不存数据。通常把未被使用的数组元素称为备用链表

数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0的平方。
如图:
在这里插入图片描述
此时的图示相当于初始化的数组状态,代码如下:

/*将一维数组space中各分量链成一备用链表,
space[0],cur为头指针,"0"表示空指针*/
Status InitList(StaticLinklIST space)
{
    
    
	int i;
	for(i=0;i<MAXSIZE-1;i++)
		space[i].cur=i+1;
	space[MAXSIZE-1].cur=0;		/*目前静态链表为空,最后一个元素的cur为0*/
	return OK;
}

3.11.1静态链表的插入操作

静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。

在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现。在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以需要自己实现这两个函数,才可以做插入和删除的操作。

为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

代码如下:

/*若备用空间链表非空,则返回分配的结点下标,否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
    
    
	int i=space[0].cur;	/*当前数组第一个元素的cur存的值,
						就是要返回的第一个备用空间的下标*/
	if(space[0].cur)
		space[0].cur = space[i].cur;	/*由于要拿出一个分量来使用了,所以我们
										就得把它的下一个分量用来做备用*/
	return i;	
} 

这段代码

猜你喜欢

转载自blog.csdn.net/weixin_52777510/article/details/120408395