线性表(List)的定义
线性表:由零个(0个元素的情况是空表)或多个数据元素组成的有限序列。
需要强调的几个关键地方:
是一个有先来后到的有限序列。
有多个元素的时候,第一个元素没有前驱,最后一个没有后继。其他元素都只有一个前驱和后继。
因为要马上要涉及线性表的抽象数据类型,所以先认识一下抽象数据类型。
抽象数据类型
先来了解一下数据类型的定义:是指一组性质相同的值的集合以及定义在此集合上的一些操作的总称。
例如在C语言中,按照取值的不同,数据类型可以分为两大类:
原子类型。不可再分解下去的基本类型,如整型、浮点型。
结构类型。例如数组、结构体。
抽象:是指抽取事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概况。
抽象是一种思考问题的方式,它隐藏了繁杂的细节。
抽象数据类型(Abstract Data Type, ADT):是指一个数学模型以及定义在该模型上的一组操作。
抽象数据类型的定义仅仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
为了便于后面对抽象数据类型进行规范描述,我们给出描述抽象数据类型的标准格式:
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作
endADT
ADT 线性表(List)
Data
{a1, a2, ..., an}
Operation
InitList(*L):初始化操作,建立一个空的线性表。
ListEmpty(L):判断线性表是否为空表,若线性表为空,返回ture,否则返回false。
ClearList(*L):将线性表清空。也就是填0。
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
说明:上述操作只是最基本的,对于实际问题中涉及的更复杂操作,完全可以用这些基本操作的组合来实现。
二、线性表的顺序存储结构
线性表的两种物理存储结构:顺序存储结构、链式存储结构。
顺序存储结构就是用一段地址连续的存储单元一次存储线性表的数据元素。跟丫的数组是一样一样的!!!
物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把数据元素依次放在这块空地中。
所以根据上面的描述,初始地址是非常非常重要的说!
顺序存储结构封装需要三个属性:
存储空间的起始位置。
线性表的最大存储容量,也就是数组的长度MaxSize。这是存放线性表的存储空间的总长度,初始化后不变。
线性表的当前长度length。这是线性表中元素的个数,是会变化的。
就像组团去电影院看电影。买到的第一个票就是起始位置。买到的票数就是最大容量。实际到的看电影的人数就是当前长度。
元素地址计算方法:
任意元素ai的存储位置可以由起始元素a1推算得出。
LOC(ai) = LOC(a1) + (i-1)*C。
C表示每个元素所占的字节数(存储空间)。LOC表示获得存储位置的函数。
获得元素操作:
只需要把数组下标对应的值返回即可。时间复杂度O(1)。
插入操作:
ListInsert(*L, i, e),即在线性表L中的第i个位置插入新元素e,插入算法的思路:
——如果插入位置不合理,抛出异常(可以刁难的位置)。
——如果线性表长度大于等于数组长度(也就是存储容量MaxSize),则抛出异常或者动态增加数组容量(很简单,因为容量满了,放不下了嘛)。
——从最后一个元素开始向前遍历到第i个位置,分别将它们向后移动一个位置。
——将要插入元素填入位置i处。
——线性表长度+1。
删除操作:
ListDelete(*L, i, *e):删除线性表L中第i个位置元素,并用e返回其值。删除算法的思路:
——如果删除位置不合理,抛出异常。
——如果线性表为空表,则抛出异常。
——取出删除元素。
——从第i个位置开始向后遍历到最后一个位置,分别将它们向前移动一个位置。
——线性表长度-1。
删除和插入算法的时间复杂度
最好情况:插入和删除操作刚好在最后一个位置,因为不需要移动任何元素,此时的时间复杂度为O(1)。
最坏情况:插入和删除的位置是第一个元素,那就意味着要移动所有的元素向前或者向后,所以此时的时间复杂度为O(n)。
至于平均情况:就取中间值O((n-1) / 2)。简化之后还是O(n)。
所以就得出了结论
线性表顺序存储结构的优缺点
线性表的顺序存储结构,在读写数据时,不管在哪个位置,时间复杂度都是O(1)。而插入或删除操作的复杂度为O(n)。
这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是读写数据。
优点:
无须为表示表中元素之间的逻辑关系而增加额外的存储空间。
可以快速存取表中的任意位置的元素。
缺点:
插入和删除操作需要移动大量元素。
当线性表长度变化较大时,难以确定存储空间的容量。
容易造成存储空间的碎片。
三、线性表的链式存储结构
线性表链式存储结构的特点:是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置。
与顺序存储的不同:除了要存储数据元素信息外,还要存储它后继元素的存储地址(指针)。
定义:把存储数据元素信息的域称为数据域,把存储后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,也叫结点(Node)。
n个结点链接成一个链表,即为线性表的链式存储结构。
因为此链表的每个结点中只包含一个指针域,所以叫单链表。
单链表的头和尾:
对线性表来说,总得有头有尾,链表也不例外。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)。这里要注意,头指针不在表的范围里,它只是能指向头结点,可它本身是在表外面的。
头指针与头结点的异同
头结点的数据域不存储任何信息,它只有指针域,相当于举个小旗子。
头指针:
是链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
无论链表是否为空,头指针都不为空。
头指针是链表的必要元素。
头结点:
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。
有了头结点,对在第一个元素之前插入结点和删除第一个结点的操作与其他结点的操作就统一了。
头结点不一定是链表的必须元素。
二者关系:头指针指向头结点。头结点数据域一般不存东西,头结点指针域指向第一个元素。
单链表的读取
获得链表第i个数据的算法思路:
——声明第一个结点p指向链表第一个结点,初始化j从1开始。
——当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1。
——如果链表末尾p为空,则说明第i个元素不存在。
——否则查找成功,返回结点p的数据。
分析:
实质就是从头开始找,直到第i个元素为止。
时间复杂度为O(n)。
单链表的结构中没有定义表长,所以不知道要循环多少次,也就不方便用for来控制循环。
核心思想就是“工作指针后移”,其实也是很多算法的常用技术。
单链表的插入
在第i个数据插入结点的算法思路
——声明一个结点指针p指向链表头结点,初始化j从1开始。
——当j<i时,就遍历链表,让指针p向后移动,不断指向下一个结点,j累加1。
——若到链表末尾p为空,则说明第i个元素不存在。
——否则查找成功,在系统中生成一个空结点s。
——将数据元素e赋值给s->data。
——单链表的插入。两个标准语句:
p->next = s;
s->next = p->next;
——返回成功。
单链表的删除
实际做的就只是一步:p->next = p->next->next;
写好看一点就是:
q = p->next;
p->next = q->next;
单链表第i个数据删除结点的算法思路:
——声明一个结点指针p指向链表头结点,初始化j从1开始。
——当j<i时,就遍历链表,让指针p向后移动,不断指向下一个结点,j累加1。
——若到链表末尾p为空,则说明第i个元素不存在。
——否则查找成功,将欲删除结点的p->next赋值给q。
——单链表的删除标准句:p->next = q->next。
——将q结点中的数据赋值给e,作为返回。
——释放q结点。
效率大PK
单链表的插入和删除算法,时间复杂度都为O(n)。单看的话语顺序表相比好像并没有什么优势。
但是当规模变大之后,比如插入连续10个元素,对顺序存储结构来说,每次移动的复杂度都是O(n)。
而单链表,只是第一次为O(n),剩下的都是O(1)。
显然,对于插入或删除数据频繁的操作,单链表的效率优势就很明显啦~
四 单链表的整表创建
整表创建顺序线性表:数组初始化。
链表创建:数据分散,增长也是动态的。所占空间的大小和位置不需要预先分配划定,是根据实际需求和系统情况即时生成。
创建过程:是一个动态生成链表的过程,从“空表”的初始状态起,依次建立各
算法思路:
——声明一个结点p和计数器变量I。
——初始化一空链表L。
——让L的头结点的指针指向NULL,即建立一个带头结点的单链表。
——循环实现后继结点的赋值和插入。
头插法建立单链表:
就是把新加的元素放在表头后的第一个位置。
——先让新结点的next指向头结点之后。
——然后让表头的next指向新结点。
语句:
p->next = L->next; //表头的L->next本来指向旧结点的,现在让新加进来的结点指向旧结点。
L->next = p; //然后让表头的L->next指向新结点p,也就是让p成为新的第一个结点,头插嘛,让他插成头。
尾插法建立单链表:
就是把新结点插到最后一个位置。
语句:
r->next = p; //让旧尾的r->next指向新尾
r = p; //把新尾标记成旧尾,方便后面循环迭代,不断插入新尾。
单链表的整表删除
就是在内存中将它释放掉,以便留出空间给其他程序使用。
删除的算法思路:
——声明结点p和q。
——把q标记成q的下一个结点。
——循环释放p,然后把q再变成新p,继续操作。
语句:
q = p->next;
free(p);
p = q;
五 静态链表
静态链表:用数组描述的链表叫做动态链表,这种描述方法叫做游标实现法。
静态链表存储结构:包括游标,数据,下标三部分。表的首尾(下标为0和下标为maxSize-1)数据不放任何东西。
最后一个游标存第一个有数据元素的下标。第一个游标指向没有存放数据元素的下标。
其他元素的游标存放下一个数据元素的下标,这样就可以类似链表一样,通过游标找到下一个元素。
静态链表初始化:初始化静态链表相当于初始化数组。
备忘: