唠嗑
时隔两周再来写这篇笔记,是因为对于我这种大学本着及格就好的学神来说这一章节还是有点难度和思考,有些东西知道,但是具体细节已经没有概念了,所以需要认真思考和总结一下,所以在此还是想跟各位学生党朋友说一句,学习要有目标,有些东西你觉得没什么用可能是你还没接触到,等到真的需要你的这一些知识的时候就悔之晚矣了。
好了,话不多说,展开这次的笔记,第三章-线性表。
第三章 线性表
线性表(list):零个或多个数据元素的有限序列。
这里有几个需要注意的关键点:
- 序列:顾名思义是有顺序的数列,数据元素之间是有顺序的。就是说,一列队伍在排队,第一个前面没有人,最后一个后面没有人,其他中间的人有且只有一个前驱和后继。
- 有限:线性表强调是有限的,事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。
- 数据元素的类型需要相同:这就相当于占座,书包和人能一样吗?能算一个数列吗?
数学语言定义
若将线性表记为(a1,...,a(i-1),a(i),a(i+1),...,a(n)),则表中a(i-1)领先于a(i),a(i)领先于a(i+1),称a(i-1)是a(i)的直接前驱元素,a(i)是a(i+1)的直接后继元素。并且各元素有且只有一个直接前驱和直接后继。
所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
在非空表中的每个数据元素都有一个确定的位置,称i为数据元素a(i)在线性表中的位序。
线性表的抽象数据类型
现在我们思考一下,线性表会有一些什么操作?
拿一个现实中的例子来说明吧:
我想大家都有学生时代排队的经历,那么一个班的学生该怎么排队呢?按身高从矮到高还是按照名字拼音还是按照姓氏笔画呢?这个过程就是线性表的创建和初始化。
一开始可能没有经验按照姓氏笔画来排队,然后发现有高有矮,队伍很难看,这个时候就需要重新排队,这就是线性表重置为空表的操作。
那么队伍排好了,会有什么操作呢?会不会有说队伍第几个人出来一下的操作?会不会有说队伍报数看看有多少人的操作?会不会有其他班的老师来问xxx是不是你们班的学生这样的操作?更或者会不会来个插班生,按照身高直接排在队伍中呢?
以上的集中情况都是比较常见的线性表操作,分别是根据位序得到数据元素,查找某个数据元素是否存在,获得线性表长度,插入和删除数据。
所以,线性表的抽象数据类型定义如下:
ADT 线性表 (List)
Data
线性表的数据对象集合为(a1,a2,...,a(n)),每个元素的类型均为DataType。其中,除第一个
元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素a(n)外,每一个元素有且只有一个直接
后继元素。数据元素之间的关系是一对一关系。
Operation
InitList(*L); 初始化操作,建立一个空的线性表L。
ListEmpty(L); 判断线性表是否为空。
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
以上就是线性表的最基本操作,对于更复杂的操作,完全可以通过基本操作的组合来实现。
线性表的顺序存储结构
前面说过,计算机的物理存储分为两种,一种是顺序存储,一种是链式存储。先来说说比较简单的线性表的顺序存储结构。
线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。
顺序存储结构,说白了,其实就是在内存中找了块地方,通过占位的方式,把一段连续的内存给占了,然后把相同数据类型的数据元素依次放入这块内存中。同时,数据类型相同,就直接可以使用一维数组来实现顺序存储结构,即把第一个数据元素存在数组下标为0的位置中,依次把线性表相邻的元素存储在数组中相邻的位置。
那么,我们可以思考一下,这里的关键点:
- 第一个是首位置,也就是这一块内存的起始位置。
- 第二个是我们需要多大的内存,这是需要预估的,多了浪费,少了容易溢出,所以数组的长度就是这个最大存储容量。
接下来,我们看看线性表的顺序存储的结构代码
#define MAXSIZE 20 /*存储空间初始分配量*/
typedef int ElemType; /*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct
{
ElemType data[MAXSIZE]; /*数组存储数据元素,最大值为MAXSIZE*/
int length; /*线性表当前长度*/
}SqList;
这里,我们发现描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度MAXSIZE。
- 线性表的当前长度:length。
注意:需要区别一下数组长度和线性表长度。数组长度是存放线性表的存储空间的长度,内存分配后这个量一般是不变的(当然排除动态分配数组,比如OC中可变数组,这会带来性能上的一定损耗)。线性表的长度是线性表中数据元素的个数,随着线性表的插入删除,这个量是变化的。所以,在任何时刻,线性表的长度应该小于等于数组的长度,也就是我们需要估算数组长度多少比较合适。
线性表顺序结构的存取时间复杂度
已经知道了顺序结构是连续的,那么我们想想如果要计算存取的时间复杂度,只需要了解一下内存中的地址的计算方法,就可以很轻松的得出时间复杂度。
线性表的起始是1,数组的起始下标是0,所以线性表的第i个元素是要存储在数组下标为i-1的位置,即数据元素和数组下标有如下对应关系:
对应关系.png存储器中的每个存储单元都有自己的编号,这个编号称为地址。当我们开辟内存后,第一个位置已经确定,那么后面的位置是不是,只要首地址+长度*间距数,就可以计算出来呢?公式如下:
LOC(a(i)) = LOC(a1)+(i-1)*c /*c表示数据类型的长度*/
通过这个公式就可以随时计算出线性表中任意位置的地址,那么我们对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常熟,所以它的存取时间性能为O(1)。
也因此,我们如果要得到线性表中的第i个位置的数据元素,其实也是很简单的,只要取得数组的i-1下标的值,用代码来写的话,如下:
#define OK 1
#define ERROR 0
typedef int Status;
/*
Status是函数的类型,其值是函数结果状态代码
初始条件:顺序线性表已经存在,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;
}
线性表顺序结构的插入与删除
我们来想一下,如果一个队伍排在那,有人插队,会有什么情况发生,肯定是后面的人每个人都要往后退一个位置。顺序结构的线性表插入数据的实现过程也就是这样。
插入算法的思路:
1.如果插入位置不合理,抛出异常;
2.如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3.从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
4.将要插入数据填入位置i处;
5.表长加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; /*新元素插入i*/
L->length++; /*表长加1*/
return OK;
}
同样的道理,如果一个队伍中有一个走了,那是不是后面的人都可以上前一位呢,这也就是顺序线性表的删除实现过程。
删除算法的思路:
1.如果删除位置不合理,抛出异常;
2.取出删除元素;
3.从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
4.表长减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),最坏的情况时间复杂度是O(n),至于平均的情况为(n-1)/2,时间复杂度还是O(n)。
这样的话,我们就可以总结一下顺序结构存储线性表的优缺点了:
- 优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间;
可以快速的存取表中任一位置的元素。 - 缺点:插入和删除操作需要移动大量位置(可以想想如果插入的不是一个元素而是多个连续的元素);当线性表长度变化较大时,难以确定存储空间的容量;容易造成存储空间的“碎片”。
接下来,我们就来分析一下线性表的链式存储结构,又有什么不同和优缺点。
线性表的链式存储结构
既然知道顺序结构的插入和删除这么麻烦,那有没有什么方法可以解决呢?首先我们思考一下为什么顺序结构会有这种问题,原因在于相邻两元素的存储位置也具有邻里关系,在内存中的位置也是挨着的,中间没有空隙,这就导致插入的时候,需要移动n-i个元素,删除也一样。
那么解决办法也就可以思考得出,就是那不然大家都不要挨着了,全部散开,这样的话,插入和删除就不需要移动这么多元素,那么数据元素就不单单存数据元素的信息,也需要存它的后继元素的存储地址。这种形式也就是单链表。
为了表示每个数据元素a(i)与其直接后继数据元素a(i+1)之间的逻辑关系,对数据元素a(i)来说,除了存储
其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信
息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或连。这两部分信息
组成数据元素a(i)的存储映像,称为结点(Node)。
n个结点(a(i)的存储映像)链结成一个链表,即为线性表的链式存储结构,因为此链表的每个结点只包含一
个指针域,所以叫做单链表。
我们把链表中第一个结点的存储位置叫做头指针。整个链表的存取就必须是从头指针开始进行了,之后的每一个结点,就是上一个结点的后继指针指向的位置,如此,最后一个结点的后继指针就为空(NULL或^符号表示)。
有时,我们习惯在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,或者存储如线性表的长度等附加信息,头结点的指针域指向第一个结点的指针。
头指针:头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针;头指针具有标识
作用,所以常用头指针冠以链表的名字;无论链表是否为空,头指针均不为空。头指针是链表的必须要素。
头结点:头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存
放链表的长度);有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作和其他结点的操作就统一
了;头结点不一定是链表必须要素。
单链表
带头结点的单链表
空联表
/*线性表的单链表存储结构*/
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList; /*定义LinkList*/
从定义可以看出来,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
总结
这篇就写到这吧,写的是真累,回到老家,发现书桌没了,一直是坐在床沿边写,太累了,这一章节的后一半单链表的读取,插入删除,以及静态链表,双向链表,循环链表,就下一篇再写吧,初步预估后天会写好发出来,希望各位大佬多多谅解。
最后祝祖国母亲68周岁快乐,祝大家国庆,中秋双节快乐!有时间的话可以多陪陪爸妈,反正我是打算在家宅8天,哈哈!!
Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!
网友评论