一、定义
有零个或多个数据元素组成的有限序列。
二、特点
- 元素有顺序。
- 处理的元素有限。
- 若元素有多个,则第一个元素无前驱,最后一个元素无后继,其他元素都有且只有一个前驱和后继。
- 若将线性表记为(a1 , … , ai-1 , ai , ai+1, … , an),则表中 ai-1 领先于 ai,ai 领先于 ai+1,称ai-1是ai的直接前驱元素,ai+1 是 ai 的直接后继元素。
- 线性表的长度为线性表元素的个数 n(n>=0)。
- 当 n 等于 0 时,称为空表。
三、抽象数据类型
1、数据类型
- 数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
- 例如很多编程语言的整型,浮点型,字符型这些指的就是数据类型。
2、抽象数据类型
- 抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作。就是把数据类型和相关操作捆绑在一起。
- 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
- 比如 1+1=2 这样一个操作,在不同CPU的处理上可能不一样,但由于其定义的数学特性相同,所在计算机编程者看来,它们都是相同的。
- “抽象”的意义在于数据类型的数学抽象特性。
- 而且,抽象数据类型不仅仅指那些已经定义并实现的数据类型,还可以是计算机编程者在设计软件程序时自己定义的数据类型。
- 例如一个3D游戏中,要定位角色的位置,那么总会出现 x,y,z 三个整型数据组合在一起的坐标。我们就可以定义一个 point 的抽象数据类型,它拥有 x,y,z 三个整型变量,这样我们就可以方便的对一个角色的位置进行操作。
3、抽象数据类型的标准格式
ADT 抽象数据类型
Data
数据元素之间逻辑关系的定义
Operation
操作
endADT
4、抽象数据类型定义
- ADT: 线性表
- Data: 线性表的数据对象集合为{a1 , a2 , … , an},每个元素的类型为DataType。数据元素之间的关系是一对一的关系。
- 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:对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。
四、线性表的存储结构
两种物理存储结构:顺序存储结构、链式存储结构
1、顺序存储结构
- 线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
- 线性表的顺序存储:a1 , a2 , … , an。
- 线性表是从1开始计算的。
#define MAXSIZE 20 //表的最大存储容量
typedef int ElemType;
typedef struct{
ElemType data[MAXSIZE];
int length; //线性表当前长度
}SqList;
1)顺序存储结构封装需要三个属性
- 存储空间的起始位置,数组 data,它的存储位置就是线性表存储空间的存储位置。
- 线性表的最大存储容量:数组的长度 MaxSize。
- 线性表的当前长度:length。
注意,数组的长度与线性表的当前长度需要区分一下:
- 数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。
- 而线性表的当前长度是线性表中元素的个数,是会变化的。
2)地址计算方法
- 假设 ElemType 占用的是 c 个存储单元(字节),那么线性表中第 i+1 个数据元素和第 i 个数据元素的存储位置的关系是(LOC表示获得存储位置的函数):LOC( ai + 1) = LOC( ai ) + c
- 所以对于第 i 个数据元素 ai 的存储位置可以由 a1 推算得出:LOC( ai ) = LOC( a1 ) + ( i - 1 ) * c
3)获取元素操作
实现 GetElem 的具体操作,即将线性表 L 中的第 i 个位置元素 e 返回。我们只需要把数组第 i-1 下标的值返回即可。
typedef int Status;
Status GetElem (SqList L, int i, ElemType *e) {
if(L.length == 0 || i<1 || i>L.length) {
return ERROR;
}
*e = L.data[i-1]; // 下标为 i-1 的是线性表第 i 个元素
}
4)插入操作
在线性表 L 中的第 i 个位置插入新元素 e ,所以插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量;
- 从最后一个元素开始向前遍历到第 i 个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置 i 处;
- 线性表长+1。
Status ListInsert (Sqlist *L, int i, ElemType e) {
//线性表已满
if(L->length == MAXSIZE){
return ERROR;
}
//当i不在范围内
if(i<1 || i> L->length+1){
return ERROR;
}
//插入数据位置不在表尾
if(i<= L->length){
//将要插入位置后元素向后移动一位
for(int k=L->length-1; k>=i-1; k--){
L->data[k+1] = L->data[k];
}
}
//将新元素插入
L->data[i-1] = e;
L->length++;
return OK;
}
5)删除操作
删除第 i 个元素,删除算法的思路:
- 如果删除位置不合理,抛出异常;一取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长-1。
Status ListDelete (Sqlist *L, int i, ElemType *e) {
if(L->length == 0){
return ERROR;
}
if(i<1 || i>L->length){
return ERROR;
}
//表的第i个元素返回给e
*e = L->data[i-1];
if(i<L->length){
for(int k=i; k<L->length; k++){
L->data[k-1] = L->data[k];
}
}
L->length--;
return OK;
}
6)插入和删除的时间复杂度
- 最好的情况:插入和删除操作刚好要求在最后一个位置操作,因为不需要移动任何元素,所以此时的时间复杂度为O(1)。
- 最坏的情况:如果要插入和删除的位置是第一个元素,那就意味着要移动所有的元素向后或者向前,所以这个时间复杂度为O(n)。
- 至于平均情况,就取中间值O((n-1)/2)。
- 平均情况复杂度简化后还是O(n)。
- 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入或删除时,时间复杂度都是O(n)。
- 这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用。
7)顺序存储结构的优缺点
优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间。
- 可以快速地存取表中任意位置的元素。
缺点:
- 插入和删除操作晞要移动大量元素。
- 当线性表长度变化较大时,难以确定存储空间的容量。
- 容易造成存储空间的“碎片”。
2、链式存储结构
- 特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。
- 在链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址(指针)。
- 我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。
- 这两部分信息组成数据元素称为存储映像,称为结点(Node)。
- n个结点链接成一个链表,即为线性表(a1 , a2 , a3 , … , an)的链式存储结构。
- 因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
1)单链表
链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)。
2)头指针与头结点的异同
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
- 无论链表是否为空,头指针均不为空。
- 头指针是链表的必要元素。
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前。
- 其数据域一般不存储任何信息,一般无意义(但也可以用来存放链表的长度)。
- 有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。
- 头结点不一定是链表的必须要素。
3)单链表存储结构
typedef struct Node{
ElemType data;//数据域
struct Node *Next;//指针域
} Node;
typedef struct Node* LinkList;
4)单链表的读取
获得链表第 i 个数据的算法思路:
- 声明一个结点 p 指向链表第一个结点,初始化 j 从1开始;
- 当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向一下结点,j+1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;
- 一否则查找成功,返回结点 p 的数据。
Status GetElem (LinkList L, int i, ElemType *e){
int j;
LinkList p;
p= L->next;
j=1;
while(p && j<i){
p=p->next;
++j;
}
if(!p || j>i){
return ERROR;
}
*e = p->data;
return OK;
}
- 由于这个算法的时间复杂度取决于i的位置,当 i=1 时,则不需要遍历,而 i=n 时则遍历 n-1 次才可以。因此最坏情况的时间复杂度为O(n)。
- 由于单链表的结构中没有定义表长,所以不能实现知道要循环多少次,因此也就不方便使用 for 来控制循环。
6)单链表的插入
- 假存储元素 e 的结点为 s,要实现结点p、p->next 和 s之间的逻辑变化。
s->next = p->next ; p->next = s;
单链表第 i 个数据插入结点的算法思路:
- 声明一结点 p 指向链表头结点,初始化 j 从1开始;
- 当 j<1 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;
- 否则查找成功,在系统中生成一个空结点 s;
- 将数据元素 e 赋值给s->data;
- 单链表的插入
s->next = p->next ; p->next = s;
语句;
Status ListInsert(LinkList *L, int i, ElemType e){
int j;
LinkList p, s;
p = *L;
j = 1;
while(p && j<i){
p = p->next;
j++;
}
if(!p || j>i){
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next