【学习总结】《大话数据结构》- 第3章-线性表

【学习总结】《大话数据结构》- 总

第3章线性表-代码链接

启示:

  • 线性表:零个或多个数据元素的有限序列。

目录

========================================

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的后继结点<顺序不可换否则会覆盖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的前驱和后继,再后结点的前驱,最后前结点的后继


  • 双向链表的删除操作:



  • 好处和弊端分析

    • 弊端:在插入删除时,需要更改两个指针变量,并且占用更多空间

    • 好处:良好的对称性,有效提高算法的时间性能

    • 综上:用空间换时间

========================================

3.15 总结回顾

========================================

3.16 结尾语

END

posted @ 2019-05-17 12:14  anliux  阅读(2356)  评论(0编辑  收藏  举报