在前面我提到的按照视点的不同,数据结构被分为逻辑结构和物理结构。
线性表在书中的定义是零个或多个数据元素的有限序列。首先它是一个序列,数量有限,而且元素之间有顺序,线性表元素的个数n(n>0)定义为线性表的长度,当n=0时,称为空表。
在较复杂的线性表中,一个数据元素可以由若干个数据项组成,例如学生信息名单,学生作为有限序列,除了学号这个元素,还有姓名、性别等多个数据项。
1、线性表的抽象数据类型:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,······,an},每个元素的数据类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素,数据元素之间的关系是一对一的关系。
Operation
InitList(*L): 初始化操作,建立一个空的线性表L;
ListEmpty(L): 若线性表为空,返回true,否则返回false;
ClearList(*L): 将线性表清空;
GetElem(L,i,*e): 将线性表中的第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
对于不同的应用线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中设计的关于线性表的更复杂的操作需要这些基本操作的组合来完成,例如实现两个线性表集合A和B的并集操作,实现的思路是循环集合B中的每个元素,判断当前元素是否存在A中,若不存在插入到A中,假设La代表集合A,Lb代表集合B,实现的代码如下:
void unionL(List *La,List Lb)
{
int La_len,Lb_len;
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)) /*La中不存在和e相同的数据元素*/
ListInsert(La,++La_len,e); /*插入值*/
}
]
2、线性表的顺序存储结构
线性表的顺序存储结构指的是用一段连续的存储单元依次存储线性表的数据元素。
线性表顺序存储的结构代码:
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType ; /*ElemType类型根据实际情况而定,这里为int*/
typedef struct
{
ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表的当前长度*/
}SqList;
顺序存储结构需要三个特性:
- 存储空间的起始位置:数组data的位置就是存储空间的存储位置;
- 线性表的最大存储容量:数组长度MAXSIZE;
- 线性表的当前长度:length。
- 在任意时刻线性表的长度应该小于等于数组的长度。
存储器中的每个存储单元都有自己的编号,这个编号称为地址。
由于每个数据元素不管是整型还是字符型都需要占据一定的存储单元空间。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数):
所以对于第i个数据元素的存储位置可以由
推算得出:
通过这个公式可以推算出线性表中任意位置的地址,对于线性表中每个位置的存入或取出数据,对计算机而言都是相等的时间都是一个常数,它的存取性能为O(1),因此具有这一特点的存储结构称为随机存取结构。
3、顺序存储结构的插入与删除
插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第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;
}
删除算法的思路:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置;
- 表长减1
/*删除L中第i个元素并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L,int i,ElemType *e)
{
int k;
if(i<1||i>L->length)
return ERROR;
if(L->length==0)
return ERROR;
if(i<L->length) /*如果删除不是最后位置*/
{
for(k=i;k<L->length;k++)
L->data[k-1]=L->data[k];
}
*e=L-data[i-1];
L->length--;
return OK;
}
时间复杂度:理想情况插入到最后一个位置 或删除最后一个元素,时间复杂度为O(1),最坏情况插入到第一个位置或删除第一个元素,意味着所有的元素向后或者向前移动,时间复杂度为O(n),至于平均情况,最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2。所以可以得出线性表的顺序存储结构,在存读数据时不管是哪个位置,时间复杂度都是O(1),插入或删除数据时,时间复杂度都是O(n)。
4、线性表的链式存储结构
4.1顺序存储结构的不足以及解决办法
顺序存储结构最大的缺点就是插入和删除需要移动大量的元素,需要耗费很多的时间。原因与解决思路:相邻的元素之间存储位置有邻居关系,所以插入时无空位,删除会有空隙需要填补。解决办法就是所有元素不考虑相邻位置,只让每个元素知道它下一个元素的位置,这样通过遍历就可以找到所有元素。
4.2线性表的链式存储结构定义
线性表的链式存储是采用一组任意的存储单元存放线性表的元素。这组存储单元可以是连续的,也可以是不连续的。因此,为了表示每个元素ai与其直接后继ai+1的逻辑关系,除了存储元素本身的信息外,还需要存储一个指示其直接后继元素的信息(即直接后继元素的地址)。这两部分构成的存储结构称为结点(node)。结点包括两个域:数据域和指针域。其中,数据域存放数据元素的信息,指针域存放元素的直接后继的存储地址。指针域中存储的信息称为指针。
通过指针域将线性表中n个结点元素按照逻辑顺序链在一起就构成了链表。由于链表中的每一个结点的指针域只有一个,所以这样的链表称为线性链表或者单链表。如下图所示:
单链表的每个结点的地址存放在其直接前驱结点的指针域中,而第一个结点没有直接前驱结点,因此需要一个头指针指向第一个结点。由于表中的最后一个元素没有直接后继,需要将单链表的最后一个结点的指针域置为“空”(NULL)。 一般情况下,我们只关心链表中结点的逻辑顺序,而不关心它的实际存储位置。通常用箭头表示指针,把链表表示成通过箭头链接起来的序列。
为了操作上的方便,在单链表的第一个结点之前增加一个结点,称之为头结点。头结点的数据域可以存放如线性表的长度等信息,头结点的指针域存放第一个元素结点的地址信息,使其指向第一个元素结点。
4.3 线性表的单链表的存储结构
typedef struct Node
{
DataType data;
struct Node *next;
}ListNode,*LinkList;
其中,ListNode是链表的结点类型,LinkList是指向链表结点的指针类型。