1.1 线性表(List): 零个或多个数据元素的有限序列
1.2 线性列表中的元素的数据类型相同
线性表的操作
1、ADD(在最后面追加)
2、INSERT(在任意位置插入元素)
3、DELETE(删除元素)
1.3 线性表存储结构:顺序存储和链式存储
顺序存储:使用一段连续的地址的存储单元依次存储线性表的数据元素
1.3.1 顺序存储结构3大要素:
- 存储空间的起始位置
- 线性表的最大存储容量(事先要给出线性表的最大长度)。
- 线性表当前的长度,小于或等于最大存储容量,小于说明此线性表还有空余的没有使用完
1.3.2 顺序存储,地址计算方式:第i个元素的地址 = start+(i-1)*c
start 表示第1个元素的地址
c 表示每个元素占用几个字节
从上图可以看出,线性表中无论读取/存储元素所在的位置在哪里,存取的时间复杂度是O(1),通常把具有这一特点的存储结构称为随机存取结构
1.3.3 线性列表插入元素的思路:
- 如果插入的位置不合理,抛出异常
- 如果线性表已满,还向里插入数据,则抛出异常,或者动态增加容量
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置
- 将要插入元素填入位置i处
- 表长加1
1.3.4 删除线性列表中的元素的思路:
- 如果删除的位置不合理,抛出异常
- 取出删除的元素
- 从删除元素位置开始遍历到最后一个元素,分别将他们向前移动一个位置
表长减1
1.3.5 插入和删除的时间复杂度
- 如果插入的位置是最后一位或者删除的元素是最后一个,那么时间复杂度为O(1),不需要移动元素
- 最坏的情况,如果元素从第一个位置插入,或者删除第一个元素,时间复杂度为O(n)
- 插入到第i个元素,或删除第i个元素,需要移动n-i个元素。插入/删除位置靠前移动元素多,位置靠前,移动元素少。中间那个元素移动的次数:(n-1)/2
- 平均时间复杂度还是0(n)
1.3.6 线性表顺序存储结构
优点:
- 无需为表中的元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存储表中任一位置的元素
缺点:
- 插入和删除操作需要移动大量的元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的碎片
1.4 线性表的链式存储结构:
- 每个数据元素不仅需要存储数据元素信息就可以了,还需要存储后继元素的存储地址
- 存储数据元素信息的域称为数据域
- 存储直接后继位置的域称为指针域,指针域中存储的信息称做指针域或链
- 数据域和指针域组成数据元素ai的存储映像,称为节点(Node)
- 链表中第一个结点的存储位置叫做头指针,整个链表的存储就必须从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置
- 最后一个结点的指针为”空”
- 有时为了方便对链表进行操作,会在单链表的第一个结点钱附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储线性表的长度等附加信息。头结点的指针域存储指向第一个结点的指针
1.4.1 头结点和头指针的区别
头指针
- 头指针:链表指向第一个结点的指针,若链表有头结点,则指向头结点
- 头指针具有标识作用,所以常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空,头指针是链表必须的元素
头结点:
- 头结点为了操作的统一和方便而设立的,放在第一个元素的节点之前,其数据域一般无实际的意义,也可以存储链表的长度
- 有了头结点,对在第一个元素节点前插入节点和删除第一个节点,其操作与其他节点的操作就统一了
- 头结点不一定是链表必需的元素
1.4.2 链表存储示意图
1.4.3 单链表的读取
获得链表第i个数据的算法思路:
- 声明一个结点p指向链表第一个结点,初始化j从1开始
- 当 j < 1 时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,返回结点p的数据
- 算法时间复杂度取决于i的位置。i=1, 时间复杂度O(1),而当i=n 时则遍历n-1 次才可以。因此最坏情况的时间复杂度是O(n)
- 单链表的结构中没有定义表长,所以就无法得知循环多少次,不方便使用for来控制循环。其主要核心思想就是”工作指针后移”
1.4.4 单链表的插入
算法思路:
- 声明一结点p指向链表第一个结点,初始化j从1开始
- 当 j < 1 时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,在系统中生一个空结点s
- 将数据元素e赋值给s->data
- 单链表的插入标准语句 s->next=p,p->next=s
1.4.5 单链表的删除
单链表删除第i个节点的算法思路:
- 声明一结点p指向链表第一个结点,初始化j从1开始
- 当 j < i,就遍历链表,让p 的指针向后移动,不断指向下一个结点,j 累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,将欲删除的节点p->next 赋值给q
- 单链表删除标准语句p->next=q->next
- 将q结点中的数据赋值给e,作为返回
释放q结点
1.4.6 单链表插入和删除时间复杂度是一样的,取决于结点i的位置,和读取i结点的时间复杂度有关。因为无论是插入还是删除都是两部分组成:先遍历查找第i个元素;第二部分就是插入和删除元素。所以他们的时间复杂度都是O(n)
这里可能觉得单链表的插入和删除,时间性能和顺序存储的查找和删除,时间性能相同,实则不然,插入/删除1个元素两者一样,如果连续插入/删除多个元素,这个时候单链表优势就表现出来了,单链表只需要查询1次,然后删除和插入的时间复杂度O(1),而顺序存储,无论是连续删除/插入还是单一删除/插入,时间复杂度都是O(n)
1.5 单链表的整表创建
单链表整表创建的算法思路:
- 声明一结点p和计量器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环:
- 生成一个新结点赋值给p
- 随机生成一数字赋值给p的数据域p->data
- 将p插入到头结点与前一新结点之间
- 这里有两种实现方式:每次插入的节点都是第一个节点,或者每次插入的节点都是最后一个节点
1.6 单链表整表的删除
单链表删除的算法思路:
- 声明一结点p和q
- 将第一个结点赋值给p
- 循环
- 将下一个结点赋值给q
- 释放p
- 将q赋值给p
1.7 单链表结构与顺序存储结构优缺点
存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能
查找
- 顺序存储结构O(1)
- 单链表O(n)
插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在查出某位置的指针后,插入和删除时间仅为O(1)
- 空间性能
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了容易发生上溢
- 单链表不需要分配存储空间,只要有可以分配,元素的个数不受限制
- 若线性表需要频繁查找,很少插入和删除操作时,宜采用顺序存储结构。若需要频繁的插入和删除时,宜采用单链表结构。比如游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况下都是读取,所以应该考虑顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不太合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周7天这种用顺序存储结构效率会高很多
1.8 静态链表
对应没有指针的编程语言实现链表这种数据结构的方法。
java,C#,python 这些面向对象的程序语言虽然没有指针但启用了对象引用机制,从某种角度也简介实现了指针的某些功能,而如Basic,Fortran 等早期的编程高级语言,由于没有指针,所以只能使用静态链表的方式实现链表
静态链表:可以理解为一个二维数组,一个元素由两部分组成,data和cur(游标相当于单链表中的next指针,存放该元素的后继在数组中的下标)
- 备用链表:未被使用的数组元素称为备用链表
- 第一个元素的data域存放备用链表第一个结点的下标
- 最后一个元素cur存储第一有值元素的下标,相当于单链表中的头结点
1.8.1静态链表的插入操作
算法思路:
- 将未被使用和已删除的分量用游标链成一个备用的链表
- 每当进行插入时,便可以从备用链表上取得第一个阶段作为待插入的新结点
- 我觉得如果以后用到静态表,也只会使用这里面的思想而已,到不会真真使用静态表,毕竟现在还使用那些basic,Fortran等这些早期编程语言
插队的示意图
1.8.2静态链表的删除操作
1.8.3 静态链表优缺点
- 优点:
- 在插入和删除操作是,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
- 缺点:
- 没有解决连续存储分配带来的表长难以确定的问题
- 失去顺序存储结构随机存取特性
1.9 循环链表:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
单循环链表和单链表的区别:
- 循环判断条件的区别:
- 单链表循环终止条件:p->next=NULL
- 单循环链表循环终止条件:p->next=头结点
- 查询元素起始点
- 单链表从第1个结点开始
- 单循环链表从任意一个结点开始。这是单循环链表相对于单链表来说最大的优点
在单链表中访问第一个元素,时间复杂度是O(1),访问最后一个元素的时间复杂度是O(n)。
在单循环列表中如果使用尾指针(rear),访问最后一个元素的时间复杂度就是O(1)。而开始结点,其实就是rear->next->next
1.10 双向循环链表:在单向循环链表中的每个结点,再设置一个指向前驱结点的指针域
解决了单向循环链表中查询前面一个结点时间复杂度为O(n)的问题,在双向循环链表中,查找前一个结点的时间复杂度是O(1),查找后一个结点的时间复杂度也是O(1)
p->netx->prior = p = p->prior->next
双向链表中很多操作和单向链表中相同,如:求长度的ListLength,查找元素的GetElem,获取元素的位置LocateElem 等。这些操作都只要涉及一个方向的指针即可,另一个指针不考虑
1.10.1 双向链表的插入
① s -> prior = p
② s -> next = p ->next
③ p -> next = prior = s
④ p -> next = s
1.10.2 双向链表的删除
① p->prior->next = p->next
② p->next->prior = p->prior
③ free(p)