线性表:零个或多个数据元素的有限序列。
若元素存在多个,则第一个元素无前驱,最后一个元素无后继
线性表强调有限
线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
在较复杂的线性表中,一个数据元素可以由若干个数据项组成
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); 在线性表L中第i个位置插入新元素e。
ListDelete(*L,i,*e); 删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L); 返回线性表L的元素个数。
endADT
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
占用一定的内存空间,然后依次把相同类型的数据存放在内存上
顺序存储结构的三个属性
1.存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
2.线性表的最大存储容量:数组长度MaxSize。
3.线性表的当前长度:length。
数组长度是存放线性表的存储空间的长度。
线性表的长度是线性表中数据元素的个数。
在任意时刻,线性表的长度应该小于数组的长度。
用数组存储顺序结构意味着要分配固定长度的数组空间,由于线性表中可以进行删除和插入操作,因此分配的数组空间要大于等于当前线性表的长度。
存储器中每个存储单元都有自己的编号,这个编号称为地址。
假设一个线性表首地址为t,一个数据元素占用c个存储单元
则第n个数据元素的存储位置为:
n=t+(n-1)*c
即我们对于第n个元素的位置都可通过这个公式计算得出,我们可以随时算出线性表中任意位置的地址,对于计算机,读取线性表中任意元素的时间都是一样的,时间复杂度为O(1)。
我们通常把具有这一特点的存储结构称为 随机存储结构
要实现GetElem操作,即将第i个元素的值返回,我们只需要把数组下标i-1的元素返回就好了。
it‘s so easy so代码略
时间复杂度:O(1)
因为顺序存储结构内存连续,要在线性表第i个位置插入一个元素,就要把第i个位置即它以后的所有元素后移一位,然后把这个元素插入。
插入算法步骤:
1.从最后一个元素遍历到第i个元素,分别将他们都向后移动一个位置
2.将插入元素填入位置i中
3.表长+1
时间复杂度:O(n)
同样是因为顺序存储结构性质,要删除一个元素我们需要在删除这个元素后将这个元素之后的每个元素前移一个位置。
删除算法步骤
1.取出删除的元素
2.从删除元素的下一个元素遍历到最后一个元素,分别将他们前移一个位置
3.表长-1
时间复杂度:O(n)
优点:
1.无需为表示表中元素之间的逻辑关系而增加额外的存储空间
2.可以快速的存取表中任意位置的元素
缺点:
1.插入和删除操作需要移动大量元素
2.当线性表长度变化较大时,难以确定存储空间的容量
3.造成存储空间“碎片”
前面说了顺序存储结构。它最大的确定就是删除和插入需要移动大量元素,而链式存储结构可以很好的完成这两个任务
简单来说,链式存储结构就是让 每一个元素都知道他的下一个元素在哪,这样我们在插入和删除元素时只要让这个元素的前一个元素认识的“人”改变就可以了
结点:为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对于数据元素ai来说,除了存储本身的信息之外,还需存储一个指示其直接后继元素的信息(即直接后继元素的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继元素位置的域叫做指针域。指针域中存储的信息称作指针或链。这两部分信息组成的数据元素ai的存储映像,称为结点(node)。
单链表:n个结点链结成一个链表,即为线性表(a1,a2…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
头指针:链表中第一个结点的存储位置叫做头指针
头结点:为了更方便的对链表进行操作,有时候我们会在单链表的第一个结点前附设一个结点,称为头结点(一般数据域可以不存储信息)
头指针:
1.头指针是指向第一个结点的指针
2.头指针具有标识作用,所以常用头指针冠以链表的名字
3.无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点:
1.头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
2.有了头结点,对在第一个结点前插入结点和删除第一个结点,其操作与其他节点的操作就统一了
3.头结点不一定是链表必须元素
通过前面的内容我们已经知道,结点是由存储数据元素的数据域和存放后继结点的指针域构成。
/*线性表链式存储结构的代码描述*/
typedef struct node
{
ElemType data;
struct node *next;
} Node;
typedef struct node *LinkList; /*定义LinkList*/
要读取单链表的第i个元素,我们需要从第一个结点开始不断寻找下一个结点直到第i个结点
获取链表第i个元素的算法步骤
1.声明一个指针p指向第一个结点,初始化j为1
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j++
3.若到链表尾p为NULL,说明第i个节点不存在
4.否则查找成功,返回节点p的数据
时间复杂度:O(n)
要将一个新结点插入到链表当中,只需要让它的前一个结点指向它,让它指向它的下一个结点就好了
单链表第i个数据插入结点的算法步骤:
1.声明一个指针p指向第一个结点,初始化j为1
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j++
3.若到链表尾p为NULL,说明第i个节点不存在
4.否则查找成功,在系统中生成一个空结点s
5.将数据元素e赋值给s->data
6.单链表的标准插入语句 s->next=p->next;p->next=s;
7.返回成功
先找到需要删除的结点,让它前一个结点指向它的后一个结点,并将此节点free掉
单链表第i个数据删除结点的算法步骤:
1.声明一个指针p指向第一个结点,初始化j为1
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j++
3.若到链表尾p为NULL,说明第i个节点不存在
4.否则查找成功,将欲删除的结点p->next赋值给q
5.单链表删除标准语句 p->next=q->next
6.将q结点中的数据赋值给e,作为返回
7.释放q结点
8.返回成功
单链表的整表创建算法步骤:
1.声明一结点p和计数器变量i
2.初始化一空链表L
3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表
4.循环
生成一新结点赋值给p
随机生成一数字赋值给p的数据域p->data
将p插入到头结点与前一新结点之间(头插法,始终让新结点在第一的位置),也可以将p插入到终端结点的后面(尾插法)
单链表的整表删除步骤:
1.声明一结点p和q
2.将第一个结点赋值给p
3.循环
将下一结点赋值给q
释放p
将q赋值给p
单链表结构与顺序存储结构对比:
存储分配方式
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能
查找
顺序存储结构O(1)
单链表O(n)
插入和删除
顺序存储结构需要平均移动表长一半的元素,时间为O(n)
单链表在线出某位置的指针后,插入和删除时间仅为O(1)
空间性能
顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢
单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
若线性表频繁查找,很少插入或删除操作时,宜采用顺序存储结构。若需频繁插入和删除时,宜采用单链表结构
当线性表中元素个数变化较大或不知道有多大时,最好采用单链表。如事先知道存储空间大小问题,宜采用顺序存储结构。
对于那些没有指针的语言,要实现链式存储结构可以用数组来代替指针,用数组描述的链表叫静态链表
静态链表,就是用另一个数组来存放当前元素的下一个元素的下标来模拟指针
对于静态链表,要解决的是:如何用静态模拟动态链表的存储空间分配
为了找到那些位置没有使用,我们可以将所有未使用的位置以及被删除的分量用游标链成一个备用的链表
要把一个新的元素插入
1.先从备用链表里获取一个位置并从备用链表删除
2.给此位置赋值
3.让要新增元素的指针数组的值等于要插入位置的下一个元素的下标,让要插入位置的上一个元素的指针数组值等于新增元素下标
要把一个某元素删除
1.取出此元素值并作为返回
2.将此位置上一个元素的指针数组值赋值为此位置指针数组值
3.备用链表中插入此位置下标
优点
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
缺点
没有解决连续存储分配带来的表长难以确定的问题
失去了顺序存储结构随机存储的特性
*作为一种思考问题的新方式
将单链表中终端结点的指针端由空指针改为指向头指针,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域