俗话说,好记性不如烂笔头,刚学了数据结构线性表,想着万一以后又忘了,或者有时候记着又找起来很耽误时间呢。所以就在这记录一下吧。嘻嘻~
一、线性表的定义
- 线性表(List):零个或多个具有相同类型的数据元素的有限序列。
- 线性表数学定义:若将线性表记为
(a1,...,ai-1,ai,ai+1,...,an)
,则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。 - 在非空表中的每个数据元素都有一个确定的位置,比如ai是第i个数据元素,称i为数据元素ai在线性表中的位序。
- 在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
- 线性表的每个数据元素的类型都相同。
二、线性表的顺序存储结构
- 线性表的顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素。
- 若采用数组来实现线性表的顺序存储结构,则数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的;线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。在任意时刻,线性表的长度应该小于等于数组的长度。
- 地址计算方法:线性表不像数组那样起始下标是0,而是1。
- 采用C语言的一维数组来实现线性表的顺序存储结构,定义结构代码如下:
1 | #define OK 1 |
5、获得元素操作:获取线性表的第i个位置的元素,就是把数组第i-1下标的值返回即可。实现代码如下:
1 | /* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度 */ |
6、插入操作:线性表的顺序存储结构在插入数据时的实现过程如下图:
插入算法的思路:
1)如果插入位置不合理,抛出异常;
2)如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3)如果插入位置不在表尾,从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
4)将要插入元素填入位置i处;
5)表长加1。
实现代码如下:
1 | /* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度+1*/ |
7、删除操作:线性表的顺序存储结构删除元素过程如下图:
删除算法的思路:
1)如果删除位置不合理,抛出异常;
2)取出删除元素;
3)如果删除元素不在表尾,从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
4)表长减1。
实现代码如下:
1 | /* 初始条件:顺序线性表L已存在,1≤i≤当前线性表的长度 */ |
8、插入和删除的时间复杂度:最好情况是若元素插入到表尾,或删除最后一个元素,则时间复杂度为O(1),因为不需要移动元素。最坏情况就是元素要插入到第一个位置或删除第一个元素,则意味着要移动所有的元素向后或向前,所以时间复杂度为O(n)。至于平均的情况,由于元素插入到第i个位置,或删除第i个元素,需要移动n-i个元素,根据概率原理,每个位置插入或删除元素的可能性是一样的,也就是说位置靠前,移动元素多,位置靠后,移动元素少,但是最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2,所以,平均时间复杂度还是O(n)。所以:线性表的顺序存储结构,在读数据时,不管是哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。
9、线性表的顺序存储结构优缺点如下图:
三、线性表的链式存储结构
- 线性表链式存储结构定义:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着这些数据元素可以存在内存未被占用的任意位置。除了要存数据元素信息外,还要存储它的后继元素的存储地址。
- 为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息,把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素ai的存储映像,称为结点。
- n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
- 把链表中第一个结点的存储位置叫做头指针,整个链表的存取就必须是从头指针开始进行。
- 由于最后一个元素没有后继,所以线性链表的最后一个结点指针为空。
- 为了操作方便,可以在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针。如下图:
7、头指针与头结点的异同,如下图 :
8、单链表的存储示意图。
9、用C语言的结构指针来描述单链表,如:
1 | #define OK 1 |
10、单链表的读取:获取链表第i个数据元素的算法思路:
1) 声明一个结点p指向链表第一个结点,初始化j从1开始;
2) 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3) 若到链表末尾p为空,则说明第i个元素不存在(链表为空或者当链表不为空时i大于链表长度);或j>i,说明第i个元素不存在(I小于1);
4) 否则查找成功,返回结点p的数据。
实现算法代码如下:
1 | /* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度 */ |
11、单链表的读取时间复杂度:此算法的时间复杂度取决于i的位置,当i=1时,不需要遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。所以最坏情况的时间复杂度为O(n)。
12、单链表的插入.只需执行s->next=p->next
;p->next=s
即可,但是这两句代码的执行次序可不能改变。 对于带有头节点的的单链表的表头和表尾插入的情况如下图:
单链表第i个数据插入节点的算法思路:
1)声明一结点p指向链表头结点,初始化j从1开始;
2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3)若到链表末尾p为空,则说明第i个元素不存在(i 大于当前单链表长度+1,包括第i-1个元素也不存在);
4)否则查找成功,在系统中生成一个空结点s;
5)将数据元素e赋值给s->data;
6)单链表的插入标准语句s->next=p->next
;p->next=s
;
7)返回成功。
实现算法代码如下:
1 | /* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度+1*/ |
13、单链表的删除
删除q节点需要执行的操作就是p->next=p->next->next
,即:q=p->next;p->next=q->next
;
单链表删除第i个数据结点的算法思路:
1)声明一结点p指向链表头结点,初始化j从1开始;
2)当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3)若到链表末尾p->next为空,则说明第i个元素不存在;
4)否则查找成功,将欲删除的结点p->next赋值给q;
5)单链表的删除标准语句p->next=q-next
;
6)将q结点中的数据赋值给e;
7)释放q结点;
8)返回成功。
实现算法代码如下:
1 | /* 初始条件:链式线性表L已存在,1≤i≤链式线性表的长度 */ |
14、单链表插入和删除的时间复杂度:都是由两部分组成,第一是遍历查找第i个元素,第二是插入或删除元素;它们的时间复杂度都是O(n)。若从第i个位置插入10个元素,对于顺序存储结构的线性表意味着每一次插入都需要移动n-i个元素,每次都是O(n),但是对于单链表而言只需要在第一次找到第i个位置的指针,仅此为O(n),接下来只是简单的通过赋值移动指针,时间复杂度都是O(1),所以对于插入和删除数据越频繁的操作,单链表的效率优势就越明显。
15、单链表的整表创建:对于单链表而言它所占用空间的大小和位置不像顺序线性表,是不需要预先分配划定的,所以创建单链表的过程就是一个动态生成链表的过程,其整表创建的算法思路如下:
1)声明一结点p和计数器变量i;
2)初始化一空链表L;
3)让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4)循环:生成一新结点赋值给p;随机生成一数字赋值给p的数据域p->data;将p插入到头结点与前一新结点之间;
实现算法代码如下:
1 | /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */ |
这里始终是让新结点在第一的位置,可以把这种算法简称为头插法;若每次都把新结点插在终端结点的后面称为尾插法,代码如下:
1 | /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */ |
16、单链表整表的删除:算法思路如下:
1)声明一结点p和q;
2)将第一个结点赋值给p;
3)如果p不为空,将下一结点赋值给q;释放p;将q赋值给p;循环
实现算法代码如下:
1 | /* 初始条件:链式线性表L已存在。操作结果:将L重置为空表 */ |
17、单链表结构与顺序存储结构优缺点, 如下:
四、静态链表
- 用数组来代替指针来描述单链表,让数组的元素都是由两个数据域组成,data和cur,其中数据域data用来存放数据元素,数据域cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫游标实现法。
- 线性表的静态链表存储结构定义如下:
1 | #define OK 1 |
3、另外,对数组第一个和最后一个元素作为特殊元素处理,数据域不存数据。通常把未被使用的数组元素称为备用链表。而数组第一个元素的cur存放备用链表的第一个结点在数组中的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点的作用,当整个链表为空时,则为0。
4、初始化数组,代码如下:
1 | /* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */ |
5、静态链表要解决的问题:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。在动态链表中,结点的申请和释放使用的是malloc()和free()函数来实现,而在静态链表中操作的是数组,不存在像动态链表的结点申请和释放问题,所以需要手动实现这两个函数。首先是申请,为了辨别数组中哪些分量未被使用,解决的办法是将所有未被使用过的以及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点,实现代码如下:
1 | /* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */ |
释放:
1 | /* 将下标为k的空闲结点回收到备用链表 */ |
6、静态链表的插入操作:实现代码如下:首先是求静态链表的数据元素个数函数:
1 | /* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */ |
然后是插入操作实现函数:
1 | /* 在L中第i个元素之前插入新的数据元素e */ |
7、静态链表的删除操作:代码如下:
1 | /* 删除在L中第i个数据元素 */ |
8、静态链表的优缺点:总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。其优缺点如下图:
五、循环链表
- 循环链表定义:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
- 为了使空链表与非空链表处理一致,通常设一个头结点,但这头结点并不是必要的,循环链表带有头结点的空链表和非空链表。
- 循环链表和单链表的主要差异在于循环的判断条件上,单链表是判断
p->next
是否为空,循环链表是判断p->next
不等于头结点,则循环未结束。 - 在单链表中采用头结点时,可以用O(1)的时间访问第一个结点,用O(n)的时间访问到最后一个结点;采用指向终端结点的尾指针来表示循环链表时可以用O(1)的时间由链表指针访问到最后一个结点。
六、双向链表
- 双向链表定义:双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
- 双向链表也可以是循环表,如下图是双向链表的循环带头结点的空链表和非空链表。
- 双向链表比单链表多了如可以反向遍历查找等操作,但是在插入和删除时,需要更改两个指针变量。对于插入操作重要的就是操作顺序很重要。
- 双向链表由于保存了两份指针,所以在空间上要占用略多一些,但是对于结点的前后结点的操作带来了方便,可以有效提高算法的时间性能,也就是用空间来换时间。
七、总结
线性表包括有2种结构:顺序存储结构和链式存储结构。链式存储结构又包括单链表、静态链表、循环链表和双向链表。结构如下:
线性表
顺序存储结构
链式存储结构
- 单链表
- 静态链表
- 循环链表
- 双向链表