(本文是对于鱼c-小甲鱼的数据结构和算法的b站视频的笔记,目的是便于复习和加强记忆)
首先我们要说的肯定是线性表到底是个什么东西
线性表定义:
数学语言进行定义,线性表相似于(a,…,an-1,an,an+1…afinal)的结构,我们称an-1为an的前驱元素,an+1为an的后继元素。注意的是,对于一个线性表,它要满足:
“第一个元素无前驱,最后一个元素无后继,其他元素有且只有一个前驱和后继”
而线性表元素个数即为线性表的长度,如果线性表有n个元素,则其长度就为n,当n为0的时候,我们称其为空表。
抽象数据类型
数据类型的定义:
一组性质相同的值的集合及定义在此集合上的一些操作的总称,例如整型,浮点型,字符型。
由于计算机中内存并不是无限大的,简单的计算和复杂的计算肯定是有不同的空间需求,所以数据的类型就要进行分类,根据数据的分类来适应不同的场合和条件。
例如在C语言中,根据取值不同,数据类型可以分为:
原子类型:不能再分解的基本类型,比如int,float…
结构类型:若干个类型组合成,例如数组…
而我们对已有的数据类型进行抽象,就有了抽象数据类型(ADT)。
抽象数据类型:一个数学模型及定义在该模型上的一组操作
抽象数据类型的定义取决于它的一组逻辑特性,与在计算机中如何表示和实现无关。
例如1+1=2,在不同cpu的处理上可能不一样但其数学特性相同。
说句人话,这就是把数据类型和相关操作捆绑在一起,就像是面向对象的编程中的”类“一样。
为什么要将抽象数据类型呢,因为之后的大多数数据类型都有其抽象数据类型定义,所以进行一下讲解。
那么线性表的抽象数据类型定义是什么呢?
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):在线性表中第i个位置插入新元素e。
ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L):返回线性表L的元素个数。
endADT
Example
实现两个线性表A,B的并集操作,使得集合A=A并B。
只需要循环遍历集合B的每个元素,判断当前元素是否存在于A中,若不存在则插入A中。
其实只需要使用几个基本操作的组合即可:
ListLength(L)
GetElem(L,i,*e)
LocateElem(L,e)
ListInsert(*L,i,e)
代码实现(使用C语言,但只是逻辑)
void union(List, *La, list Lb)
{
int La_len, Lb_len, i;
ElemType e;
La_len = ListLengh(*La);
Lb_len = ListLengh(*Lb);
for (i = 1; i <= Lb_len; i++)
{
GetElem(Lb,i,&e);
if(!LocateElem(*La,e))
{
ListInsert(La, ++La_len, e);
}
}
}
线性表的顺序存储结构
用一段地址连续的存储单元依次存储线性表的数据元素
(类似于数组)。
物理上的存储方式事实上就是在内存中找个初始地址然后通过占位的形式将一定的内存空间占据,然后把相同数据类型的数据元素依次放在这个空间中。
结构代码
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length; //线性表当前长度
} Sqlist;
这里封装了一个结构,事实上就是对数组进行封装,增加了个当前长度的变量罢了。
总之,顺序存储结构封装需要三个属性:
1.存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置。
2.线性表的最大存储容量:数组长度MAXSIZE。
3.线性表当前长度:length
数组的长度与线性表的当前长度需要区分:数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会变化的。
注意,线性表1是从1开始计数的。
假设ElemType占用c个字节,那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是(LOC表示获得存储位置的函数):LOC(ai+1)= LOC(ai)+ c
所以对于第i个数据元素ai的存储位置可以由a1推算得出:
LOC(ai)= LCO(a1)+(i-1)*c
通过这个公式,我们可以随时计算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间,所以它的存储时间性能是O,我们通常称为随机存储结构。
获取元素操作
实现GetElem的具体操作,即将线性表L中的第i个位置元素值返回。
代码实现
#define OK 1
#define ERROR 0
#define True 1
#define False 0
typedef int Status;
//Status 是函数的类型,其值是函数结果状态代码,如OK。
//初始条件: 顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(Sqlist L, int i, ElemType *e)
{
if(L.length == 0 || i<1 || i>L.length)
{
return ERROR
}
*e = L.data[i-1];
return OK;
}
插入操作
思路:
如果插入位置不合理,抛出异常;
如果线性表长度大于等于数组长度,抛出异常或动态增加数组容量;
从最后一个元素开始向前遍历到第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)
{
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已存在,1<=i<=ListLength(L)*/
/*操作结果,在L中第i个位置之前插入新的数据元素e,L长度-1*/
Status ListInsert(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)。
最坏的情况:第一个元素,所有元素移动,时间复杂度为O(n)。
平均复杂度还是O(n)。
线性表顺序存储结构优缺点:
存读为O(1),插入删除为O(n)。
适合元素个数稳定,不经常插入和删除元素,更多是存取应用。
优点:
无需表示表中元素之间逻辑关系增加额外存储空间。
快速存取表中任意位置元素。
缺点:
插入和删除操作需要移动大量元素。
当线性表长度变化较大时难以确定存储空间容量,容易造成存储空间碎片。