【学习总结】《大话数据结构》- 总
启示:
线性表:零个或多个数据元素的有限序列。
目录
- 3.1 开场白
- 3.2 线性表的定义
- 3.3 线性表的抽象数据类型
- 3.4 线性表的顺序存储结构
- 3.5 顺序存储结构的插入与删除
- 3.6 线性表的链式存储结构
- 3.7 单链表的读取
- 3.8 单链表的插入与删除
- 3.9 单链表的整表创建
- 3.10 单链表的整表删除
- 3.11 单链表结构与顺序存储结构优缺点
- 3.12 静态链表
- 3.13 循环链表
- 3.14 双向链表
- 3.15 总结回顾
- 3.16 结尾语
----------------------------------------------
3.1 开场白
- 一些可以略过的场面话...
- 由一个例子引入:幼儿园小朋友排队,左右两侧每次都是固定的人,方便清点和避免丢失。
3.2 线性表的定义
定义:线性表(list)- 零个或多个数据元素的有限序列。
几个关键点:
1-“序列”:第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
2-“有限”:元素的个数是有限的。
3-再加一点:元素类型相同。
数学语言定义:
线性表的长度:线性表元素的个数n(n≥0)定义为线性表的长度。n=0时,称为空表。
位序:ai是第i个数据元素,称i为数据元素ai在线性表中的位序。
一些线性表的例子:
- 12星座-有序,有限,是线性表
- 公司的组织架构,一个boss手下几个人那种:不是线性表,每个元素不只有一个后继。
- 班级同学之间的友谊:不是线性关系,每个人都可以和多个同学建立友谊
- 爱情:不是线性关系,否则每个人都有一个爱的人和被爱的人且不是同一个人....
- 班级点名册:是线性表,按学号排序,有序有限,类型相同,并且作为复杂的线性表,一个数据元素由若干个数据项组成。
- 书包占位所以要插队:不能,因为排队是线性表,而书包数据类型不同,别人都是人,书包不是人,所以不是线性表,不能插队。
3.3 线性表的抽象数据类型
第一章讲过“抽象数据类型”,大致包含类型名称、data、operation等
线性表的操作:
线性表的创建和初始化过程:老师给小朋友们排一个可以长期使用的队
线性表重置为空表的操作:排好后发现高矮不一,于是解散重排
根据位序得到数据元素:问到队伍里第5个小朋友是谁,老师很快能说出来这个小朋友的名字、家长等。
查找某个元素:找一下麦兜是否是班里的小朋友,老师会告诉你,不是的
插入数据和删除数据:有新来的小朋友,或者有小朋友生病请假时
线性表的抽象数据类型定义:
涉及更复杂的操作时,可以用以上的基本操作的组合来实现。
例如求两个线性表集合A和B的并集:可以把存在B中但不存在A中的数据元素插入到A中
3.4 线性表的顺序存储结构
这部分了解一下线性表的两种物理结构之一----顺序存储结构
1-定义:线性表的顺序存储结构,指用一段地址连续的存储单元依次存储线性表的数据元素。
2-顺序存储方式:
依次,每个数据元素类型相同,可以用c语言的一维数组来实现顺序存储结构。
代码实现:
描述顺序存储结构需要三个属性:
1-存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置
2-线性表的最大存储容量:数组长度MaxSize
3-线性表的当前长度:length
3-数据长度和线性表长度的区别
数组长度:是存放线性表的存储空间的长度,存储分配后这个量是一般不变的。
线性表长度:线性表中数据元素的个数,随着插入删除操作而变化。
注意:线性表的长度总是小于等于数组的长度。
4-地址的计算方法
地址:存储器中的每个存储单元都有自己的编号,这个编号称为地址。(内存地址)
计数:线性表从1开始,而c语言中的数组从0开始
LOC:获得存储位置的函数(假设每个数据元素占c个存储单元)
通过上述公式,可以随时算出线性表中任意位置的地址,且都是相同时间。
即时间复杂度:O(1) -- 每个位置存、取数据,都是相等的时间,也就是一个常数。
通常把具有这一特点的存储结构称为
3.5 顺序存储结构的插入与删除
0-注意点
0.1-线性表的第 i 个数是从1开始计数,而数组下标[]是从0开始计数
0.2-时刻保持清醒,分清到底是 i 还是 i-1
0.3-L->data和L.data:(点出现在取数中,箭头出现在插入删除中)
L->data中L是结构体指针;L.data中L是结构体变量;一说:点适合顺序结构,箭头适合链式结构。
1-获得元素操作 - GetElem
2-插入操作
图示:
插入算法的思路:
1-如果插入位置不合理,抛出异常;
2-如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
3-从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
(注:从最后一个元素开始,依次往后挪一位,否则最后一个不动,前面没位置的)
4-将要插入元素填入位置i处;
5-表长加1.代码实现:
(注:if中排除i>length+1是符合的,因为跟表不挨着,插入和表无关啦,而if中不包含临界点i=length+1的情况,因为if进行的是判断错误和移动数据,当i=length+1时,直接运行插入数据赋值即可,即if判断后的那条语句)3-删除操作
图示:
删除算法的思路:
1-如果删除位置不合理,抛出异常;
2-取出删除元素;
3-从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
(注:从删除元素的位置开始,依次往前挪,后面的跟上,就把空位补上了)
4-表长减1.代码实现:
4-插入、删除的时间复杂度分析:O(n)
最好的情况:在最后一个位置插入或删除:O(1)
最坏的情况:在第一个位置插入或删除:O(n)
平均:(n-1)/2
5-线性表的顺序存储结构---不同操作的时间复杂度对比:
存取操作:O(1)
插入删除:O(n)
综上:顺序存储结构适合元素个数不太变化的,更多是存取数据的应用
6-线性表的顺序存储结构的优缺点:
3.6 线性表的链式存储结构
1-顺序存储结构的不足的解决方法
顺序存储的最大缺点:插入和删除需要移动大量元素,非常耗费时间。
原因:相邻元素的存储位置也具有邻居关系,它们在内存中的位置是挨着的,无法快速介入。
思路:不要考虑元素的相邻位置,只让每个元素知道它的下一个元素的地址,可以找到即可
2-线性表链式存储结构的定义
特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以连续,也可以不连续。(顺序存储要求地址连续)
负担:顺序结构中,每个数据元素只需要存数据元素的信息,而链式结构中,还需要存储它的后继元素的存储地址。
头指针:链表中第一个结点的存储位置叫做头指针。整个链表的存取从头指针开始。
最后一个结点的指针:后继不存在,应为空,通常用NULL或“^”表示是
头结点:为方便对链表进行操作,在单链表的第一个结点前附设一个结点,称为头结点。
头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储第一个结点的指针。
3-头结点与头指针的异同
注:头指针-->(头结点)-->开始结点(第一个结点)
- 一个单链表可以由其头指针唯一确定,一般用其头指针来命名单链表
- 不论链表是否为空,头指针总是非空
- 单链表的头指针指向头结点。
- 头结点的指针域存储指向第一结点的指针(即第一个元素结点的存储位置)
- 若线性表为空表,则头结点的指针域为空。
参考:数据结构中的开始结点、头指针、头结点
4-线性表链式存储结构的代码描述
空链表图示:线性表为空表,则头结点的指针域为‘空’
更方便的单链表图示:
带有头结点的单链表图示:
新的空链表图示:
单链表的代码:(用结构指针来描述)
结点:由存放数据元素的数据域,和存放后继结点地址的指针域组成。
设p是指向线性表第i个元素的指针,则
p->data:其值是一个数据元素,表示结点ai的数据域
p->next:其值是一个指针,表示结点ai的指针域,指向第i+1个元素
p->data=ai
p->next->data=ai+1
p和(p->next)都是指针,同等看待
3.7 单链表的读取
相比顺序存储结构的非常容易的读取,要得到单链表的第i个元素,必须从头开始找
算法思路:
1-声明一个结点p指向链表第一个结点,初始化j从1开始;
2-当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3-若到链表末尾p为空,则说明第i个元素不存在;
4-否则查找成功,返回结点p的数据。代码实现:
(ps: 链表L的头结点是L->next??)
时间复杂度:O(n)
最好的情况:i=1时不需要遍历
最坏的情况:i=n时遍历n-1次
PS:事先不知道要循环的次数,因此不方便用for循环。while循环也是循环!!!~~
核心思想:“工作指针后移”
3.8 单链表的插入与删除
1-单链表的插入
将s结点插入到结点p和p->next之间
操作代码:
s->next=p->next; p->next=s;
-- 把p的后继结点改为s的后继结点,再把结点s变成p的后继结点next的值>
单链表的表头和表尾的操作:
单链表第i个数据插入结点的算法思路:
1-声明一结点p指向链表第一个结点,初始化j从1开始;
2-当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3-若到链表末尾p为空,则说明第i个元素不存在;
4-否则查找成功,在系统中生成一个空节点s;
5-将数据元素e赋值给s->data;
6-单链表的插入标准语句:s->next=p->next; p->next=s;
7-返回成功。代码实现:
2-单链表的删除
将结点q删除,其中q为存储元素ai的结点。
其实就是将它的前继结点的指针绕过,指向它的后继结点即可。
操作代码:
q=p->next; p->next=q->next;
(p->next=p->next->next,用q来取代p->next)单链表第i个数据删除结点的算法思路:
1-声明一结点p指向链表第一个结点,初始化j从1开始;
2-当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3-若到链表末尾p为空,则说明第i个元素不存在;
4-否则查找成功,将欲删除的结点p->next赋值给q;
5-单链表的删除标准语句:p->next=q->next
;
6-将q结点中的数据赋值给e,作为返回;
7-释放q结点;
8-返回成功。代码实现:
3-时间复杂度分析
单链表的插入和删除:首先是遍历查找第i个元素,其次是插入和删除操作
故时间复杂度是O(n)
当插入删除一个元素时,与顺序结构比,没有太大优势
但是当从第i个元素的位置插入10个元素时:
顺序结构:每次插入都需要移动元素,每次都是O(n)
单链表:只需要在第一次时,找到第i个位置的指针,此时为O(n),后面每次都是O(1)
综上,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
3.9 单链表的整表创建
对比顺序结构和单链表的创建:
顺序存储结构的创建:相当于一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。
单链表的创建:动态结构,对于每个链表,它所占空间的大小和位置不需要预先分配划定,可根据需求即时生成。
故:创建单链表:一个动态生成链表的过程,即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
1-声明一结点p和计数器变量i;
2-初始化一空链表L;
3-让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4-循环:
-- 生成一新结点赋值给p;
-- 随机生成一数字赋值给p的数据域p->data;
-- 将p插入到头结点与前一新结点之间。头插法:
头插法思路:始终让新结点在第一的位置
头插法--代码实现:
注:先把 ( * L ) -> next 赋值给 p 结点的指针域,再把 p 结点赋值给头结点的指针域
否则会覆盖 ( * L ) -> next
头插法--图示:
尾插法:
尾插法思路:新结点插在终端结点的后面
尾插法--代码实现:
(注:头插法先定义一个头结点,而尾插法不需要头结点)
尾插法--图示:
注:L是指整个单链表,r是指向尾结点的变量,r随着循环不断地变化结点,而L随着循环增长为一个多结点的链表
循环结束后,
r->next=NULL
--让这个链表的指针域置空。
3.10 单链表的整表删除
单链表整表删除的算法思路:
1-声明一结点p和q;
2-将第一个结点赋值给p;
3-循环;
-- 将下一结点赋值给q;
-- 释放p;
-- 将q赋值给p代码实现:
3.11 单链表结构与顺序存储结构优缺点
简单对比单链表结构和顺序存储结构;
对比得出的一些经验性的结论:
1-若线性表多查找,少插删,宜采用顺序存储结构;若多查删,宜采用单链表结构
举例:游戏开发中,用户注册信息时,除了注册时插入,其他时候多读取,用顺序;而玩家的装备随时增删,用单链表。
2-若线性表的元素个数变化大或不确定时,宜采用单链表;若确定或事先明确个数,用顺序存储效率更高。
3-总结:各有优缺点,不能简单说哪个好哪个不好,需要根据实际情况综合分析哪种更合适。
3.12 静态链表
引入:
1-出现问题:c有指针,而有些面向对象语言如java、c#等启用了对象引用机制,间接实现了指针的作用;而有些语言如basic等没有指针,如何实现链表结构呢?
2-解决思路:用数组代替指针,来描述单链表定义:用数组描述的链表叫做静态链表,或曰“游标实现法”。
数组的元素都是由两个数据域组成的,data(数据)和cur(游标)
即数组的每个下标都对应一个data和一个cur
数据域data:用来存放数据元素,也就是我们要处理的数据。
游标cur:相当于单链表中的next指针,存放该元素的后继在数组中的下标。
注:此处的游标cur不是数组的索引,而是数组索引之外的又一个值
备用链表:通常把未被使用的数组元素称为备用链表。(即数组的后面还没填充数据的空闲空间)
对于数组特殊元素的设置:
上图示此时相当于初始化的数组状态。对应的代码实现:
将数据存入后的状态:
(注,左上角笔误:空闲空间,不是空间空间。。还有图中标记的所谓“头结点”存疑,可以说是开始结点)1-静态链表的插入操作
动态链表中:结点的申请和释放分别借用函数malloc()和free()来实现
静态链表中:操作的是数组,需要自己实现这两个函数,以便进行插删操作
malloc()函数的静态链表代码实现:(配合上图食用效果更佳)
静态链表插入操作的代码实现
静态链表插入操作的图示:(在乙和丁之间插入丙,只需改变乙和丙的cur即可)
2-静态链表的删除操作
free()函数的静态链表代码实现:
静态链表删除操作的代码实现:
静态链表删除操作的图示:()
3-静态链表优缺点
3.13 循环链表
循环链表(circular linked list)的定义:
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成了一个环
这种头尾相接的单链表称为单循环链表,简称循环链表
优势:
解决了一个问题:即如何从当中一个结点出发,访问到链表的全部结点。
带有头结点的循环链表图示-空链表和非空链表
循环链表和单链表的差异:循环的判断条件
单链表:判断p->next是否为空
循环链表:判断p->next不等于头结点,则循环未结束
尾指针rear
引入: 有头结点时,访问第一个结点用 O(1),但访问最后一个结点用O(n)
有了尾指针以后,访问第一个结点和最后一个结点的时间均为O(1)了
- 尾指针:rear
- 头指针:rear->next
尾指针图示:
示例:将两个循环链表合并成一个表时:
图示:
代码实现:
3.14 双向链表
双向链表(double linked list)的定义:
在单链表的每个结点中,再设置一个指向其前驱结点的指针域
即在双向链表中的结点都有两个指针域,一个指向直接后继,一个指向直接前驱
代码实现:
带有头结点的双向链表的图示:空链表和非空链表
双向链表的前驱的后继:它自己
p->next>prior = p = p->prior->next
双向链表的插入操作:
注意顺序:先s的前驱和后继,再后结点的前驱,最后前结点的后继
双向链表的删除操作:
好处和弊端分析
弊端:在插入删除时,需要更改两个指针变量,并且占用更多空间
好处:良好的对称性,有效提高算法的时间性能
综上:用空间换时间