《数据结构》(C++)之第二章:线性表

  • 概述:线性表是一种最基本、最简单的数据结构,数据元素之间仅具有前驱与后继关系

2.1 线性表的逻辑结构

2.1.1 线性表的定义
  • 定义:线性表(linear list)简称表,是n(n >= 0)个 具有相同类型的数据元素有限序列

    • 长度:线性表中数据元素的个数
      • 空表:长度为零时

      • 非空表:L = (a1, a2, ···, an),其中,a(i) (1 <= i <= n)称为数据元素(从1开始计数)

  • 序偶关系:任意一堆相邻的数据元素之间存在序偶关系<a i-1 , a i> (1 < i <= n),有且只有一个前驱及后继(首尾除外)

    • 前驱:a i-1 称为 a i 的前驱
    • 后继:a i 称为 a i-1 的后继
2.1.2 线性表的抽象数据类型定义
  • 对线性表进行操作时,需判断抽象数据类型的五个方面:

    • 1⃣ 前置条件
    • 2⃣ 输入
    • 3⃣ 功能
    • 4⃣ 输出
    • 5⃣ 后置条件

2.2 线性表的顺序存储结构及实现

2.2.1 线性表的顺序存储结构——顺序表
  • 定义:线性表的顺序存储结构称为 顺序表

    • 顺序表是用一段 地址连续 的存储单元依次存储线性表的数据元素
  • 实现:数组

    • C++中,数组的下标是从0开始的,而线性表中元素的序号是从1开始的

      • 因此:线性表中第 i 个元素存贮在数组中下表为 i-1 的位置

      image

  • 随机存取结构:顺序表中数据元素的存储地址是其序号的线性函数,只要确定了存储顺序表的起始地址(即基地址),计算任意一个元素的存储地址的时间是相等的,具有这一特点的存储结构称为 随机存取(random access)结构

    名称意义
    存储结构是数据及其逻辑结构在计算机中的表示
    存取结构是在一个数据结构上做查找操作的时间性能的一种描述
    • 推论:查找时间与数据元素所在顺序表中的位置无关

    • 定位:设顺序表的每个元素占用c个存储单元,则第i个元素的存储地址为LOC(ai) = LOC(a1) + (i-1)*c

2.2.2 顺序表的实现
查找操作
  • (1)按位查找

    • 时间复杂度为 O(1):顺序表中的第i个元素存储在数组中下表为i-1的位置,容易实现按位查找
  • (2)按值查找

    • 时间复杂度为 O(n):循环匹配,问题规模为表长n,平均基本语句执行次数为 n/2
插入操作
  • 伪代码:

    • (1)如果表满了,抛出上溢异常
    • (2)如果元素插入位置不合理,抛出位置异常
    • (3)将最后一个元素直至第 i 个元素分别向后移动一个位置(通过循环实现)
    • (4)将元素x填入位置i处
    • (5)表长加1
  • 平均时间复杂度:在顺序表上实现插入操作,等概率情况下,平均要移动表中一半的元素(基本语句执行次数为 n/2),算法的平均时间复杂度为 O(n)

删除操作
  • 伪代码:

    • (1)如果表空,则抛出下溢异常
    • (2)如果删除位置不合理,则抛出删除位置异常
    • (3)取出被删元素
    • (4)将下标为i,i+1,···,n-1处的元素分别移到下标i-1,i,···,n-2处
  • 时间复杂度:O(n),原因同插入操作

2.3 线性表的链接存储结构及实现

  • 顺序表(数组)的缺点:

    • (1)插入和删除操作需移动大量元素

      • 频繁插入删除不适合使用
    • (2)表的容量难以确定,但数组的长度必须事先确定

      • 长度变化频繁不适合使用
    • (3)造成存储空间的“碎片”,数组要求占用 连续 的存储空间

  • 造成上述缺点的根本原因:静态存储分配

    • 解释:即在处理数据元素前,已划分存储空间
  • 解决:采用 动态存储分配 ,即采用 链接存储结构

    • 静态链表除外
2.3.1 单链表
1、单链表的存储方法
  • 定义:单链表是用一组 任意 的存储单元存放线性表的元素,每个结点只有一个指针域

    • 这组存储单元可以连续/不连续/零散分布在内存中的任意位置
  • 结点:为了能正确表示元素之间的逻辑关系,每个存储单元在 存储数据元素 的同时,还必须 存储其后继元素所在的地址信息 ,这个地址信息称为 指针 ,这两部分组成了数据元素的存储映像,称为结点(node)

    • 结点结构:data | next
    • (1)data:数据域,用来存放数据元素
    • (2)next:指针域(也称链域),用来存放该结点的后继结点的地址
  • 单链表的结构

    image

    • (1)头指针与开始结点:单恋表中每个 结点的存储地址 存放在其前驱结点的next域中,而第一个元素无前驱,所以设 头指针(head pointer) 指向第一个元素所在的结点(称为 开始结点

      • 头指针作用:整个 单链表的存取必须从头指针开始进行 ,因而头指针具有标识一个单链表的作用
    • (2)尾标志与终端结点:由于最后一个元素无后继,故最后一个元素所在结点(称为 终端结点)的指针域为空,即NULL(用^)表示,也称为 尾标志(tail mark)

  • 头结点:为了避免对头指针的单独处理,通常在单链表的开始结点之前附设一个 同类型 的结点,称为 头结点(head node)

    image

    • 加上头结点之后,无论单链表是否为空,头指针始终指向头结点,使得空表和非空表的处理统一

    • 个人理解:头指针存放在头结点的next域中

2、单链表的实现
  • 工作指针:设置一个工作指针p,当指针p指向某结点时执行响应的处理,然后将指针p修改为指向其后继结点,直到p为null为止(p != null常为继续遍历的条件)

    • eg:工作指针的初始化

      Node<DataType> * first;  //声明单链表的头指针
      
      p = first -> next;  //工作指针p初始化
      
    • 补充:

      • *的用法:1⃣ 声明指针 2⃣ 对指针指向的地址进行取值操作
      • 不可以使用p++进行指针后移:因为存储单元可能不连续,p++不能保证指向下一个结点
      • ->.
        操作符操作对象使用时机
        ->A->B则A为指针,->是成员提取,A->B是提取A中的成员B,A只能是指向类、结构、联合的指针当访问 地址 (指针或迭代器)的成员或数据时,用“->”
        .A.B则A为对象或者结构体当访问 直接对象 的成员或数据时,用“.”
  • (1)遍历操作

    • 1⃣ 工作指针初始化
    • 2⃣ 重复执行下述操作,直到p为空
      • 输出结点p的数据域
      • 工作指针p后移
  • (2)求线性表的长度

    • 1⃣ 工作指针p初始化;
    • 2⃣ 累加器 conut 初始化;
    • 3⃣ 重复执行下述操作,直到p为空
      • 工作指针p后移;
      • count++
    • 4⃣ 返回累加器count的值
  • (3)查找操作

    • 1⃣ 按位查找:即使知道被访问结点的位置i(即序号),也只能从头指针出发顺next域逐个结点向下搜索

      • 时间复杂度:O(n)
      • 因此,单链表是 顺序存取(Sequential Access)结构
    • 2⃣ 按值查找

  • (4)插入操作

    • 1⃣ 工作指针p初始化
    • 2⃣ 查找第 i-1 个结点并使工作指针p指向该结点(循环遍历实现)
    • 3⃣ 若查找不成功,说明插入位置不合理,抛出插入位置异常
    • 4⃣ 若查找成功,将新结点s插入到结点p之后
      //申请一个结点s,使其数据域为x
      s = new Node;
      s -> data = x;
      
      //将结点s插入到结点p之后
      s -> next = p -> next;
      p -> next = s;
      
  • (5)构造函数

    • 初始化一个空链表

      first = new Node;
      first -> next = null;
      
    • 1⃣ 头插法添加数据元素:将每次申请的结点插在头结点的后面

      for(...) {
          s = new Node;
          s -> data = a[i];
      
          s -> next = first;
          first -> next = s;
      }
      
    • 2⃣ 尾插法添加数据元素:将每次申请的结点插在终端结点的后面

      //尾指针初始化
      r = first 
      
      for(...) {
          s = new Node;
          s -> data = a[i];
      
          r -> next = s;
          r = s;
      }
      
      
  • (6)删除操作

    • 时间复杂度: O(n)

      • 时间主要耗费在查找正确的删除位置上
    • 伪代码:

      • 1⃣ 工作指针p初始化
      • 2⃣ 若删除第 i 个结点,则查找第 i-1 个结点并使工作指针p指向该结点(循环实现)
      • 3⃣ 若p不存在或p的后继结点不存在,则抛出位置异常
      • 4⃣ 若查找成功:
        步数摘链过程C++代码示例
        1暂存被删结点和被删元素值q = p -> next; x = q -> data
        2摘链,将结点p的后继结点从链表上摘下p -> next = q -> next(将第 i-1 个结点,即p,的后继指向第 i+1 个结点,即 q->next)
        3释放被删结点delete q
        4返回被删元素值return x
  • (7)析构函数

    while(first != null) {     //从链表头开始释放单链表的每一个结点的存储空间
        q = first;           //暂存被释放结点
        first = first -> next;      //first指向被释放结点的下一个结点
        delete q;           //删除被释放结点
    }
    
2.3.2 循环链表
  • 定义:将单链表的 终端结点的指针域 由空指针改为 指向头结点 ,使整个单链表形成一个环,这种头尾相接的单链表称为循环链表

    image

  • 时间复杂度:O(n)

    • 小技巧:改 用指向终端结点的尾指针来指示循环链表 ,使查找开始结点和终端结点的时间复杂度都为O(1)
      • 如果仍用头指针,则找到开始结点为 O(1),找到终端结点为 O(n)
  • 遍历循环的条件:用作循环变量的工作指针是否等于某一指定指针(如头指针或尾指针),以判定工作指针是否扫描了整个循环链表

    • 遍历可以从循环链表中任一结点出发
2.3.3 双链表
  • 定义:如果希望快速确定表中任一结点的前驱结点,可以在单链表的每个结点中再设置一个指向其 前驱结点 的指针域,这样就形成了双链表

    image

  • 结点结构:prior | data | next

    • prior为 前驱指针域 ,存放该结点的前驱结点的地址
    • next为 后继指针域 ,存放该结点的后继结点的地址
  • 双链表的对称性:(p -> prior) -> next = p = (p -> next) -> prior

    • 即结点p的存储地址既存放在其前驱结点的后继指针域中,也存放在它的后继结点的前驱指针域中
1、插入
  • 在结点p的后面插入一个新结点s,需要修改4个指针(修改时注意相对顺序)
    • 1⃣ s -> prior = p
    • 2⃣ s -> next = p -> next
    • 3⃣ p -> next -> prior = s
    • 4⃣ p -> next = s
2、删除
  • 摘除结点p的过程:顺序可颠倒
    • 1⃣ (p -> prior) -> next = p -> next
    • 2⃣ (p -> next) -> prior = p -> prior

2.4 顺序表和链表的比较

2.4.1 时间性能比较
  • 时间性能:指基于某种存储结构的基本操作(即算法)的时间复杂度

  • 比较

    存储结构查找操作时间复杂度插入/删除操作时间复杂度
    顺序表(数组)按位置随机访问(下标)O(1)因连续存储空间,所以需移动数组中的其他元素(平均需移动 n/2 个元素)O(n)
    链表需遍历查找插入的位置O(n)操作插入位置前后结点的指针实现O(1)
    • 使用顺序表:适于查找

      • 线性表需要频繁查找却很少进行插入删除
      • 其操作和“数据元素在线性表中的位置”密切相关时
    • 使用链表:适于插入/删除

      • 频繁进行插入和删除操作
2.4.2 空间性能比较
  • 空间性能:指某种存储结构所占用的存储空间的大小

    • 存储密度存储密度 = 数据域占用的存储量 / 整个结点占用的存储量
      • eg:顺序表中每个结点(数组元素)只存放数据元素,存储密度为1,链表的每个点点则需要多存放指针域,因此存储密度小于1

      • 从结点的存储密度上讲,顺序表的存储空间利用率高

  • 比较

    存储结构存储密度存储空间预分配
    顺序表(数组)1需预分配定量的空间,存在不能充分利用空间和上溢空间再分配的问题
    链表< 1不需要预分配,只要有内存空间可以分配,链表中的元素个数就没有限制
    • 使用链表:线性表中元素个数变化较大或未知
    • 使用顺序表:事先知道线性表的大致长度,空间效率更高

2.6 线性表的其他存储方法

2.5.1 静态链表
  • 定义:静态链表(static linked list) 是用数组来表示单链表,用数组元素的下标来模拟单链表的指针

    image

    • 每个数组元素的构成:

      • data域存放数据元素
      • next域存放该元素的后继元素所在的 数组下标
    • 组成:

      • 1⃣ 空闲链:所有空闲数组单元组成的单链表
      • 2⃣ 静态链表
    • 由于它是利用数组定义的,属于 静态存储分配

  • 优点:解决了顺序表在插入和删除时需要大量移动元素的缺点

  • 缺点:仍未解决连续存储分配带来的表长难以确定的问题

1、添加操作
  • 假设新结点插在结点p的后面:avail是空闲链的 “头指针”(即头结点的下标)

    s = avail;                     //不用申请新结点,利用空闲链的第一个结点
    
    avail = SList[avail].next       //空闲链的头指针后移
    
    SList[s].data = x;              //将x填入下标为s的结点
    
    SList[s].next = Slist[p].next;  //将下标为s的结点插入到下标为p的结点后面
    
    SList[p].next = s;
    

    image

2、删除操作
  • 进行删除操作时,将被删除结点从静态链表摘下,在插入空闲链的最前端

    • 假设删除结点p的后继结点
    q = Slist[p].next;              //暂存被删结点的下标
    
    SList[p].next = SList[q].next;  //摘链
    
    SList[q].next = avail;          //将结点q插在空闲链avail的最前端
    
    avail = q;                     //空闲链头指针avail指向结点q
    

    image

2.5.2 间接寻址
  • 定义: 间接寻址(indirect address) 是将数组和指针结合起来的一种方法,他将 数组中 存储数据元素的单元改为 存储指向该元素的指针

    image

    • 顺序存储:利用数组单元在物理位置上的临街关系来表示线性表中数据元素的逻辑关系,使得顺序表可以实现随机存取

    • 链接存储:利用指针将表中元素依次串联在一起,使得在修改线性表中元素之间的逻辑关系时,只需修改相应指针而不需要移动元素

  • 优点:保持了顺序表随机存取的优点(快速查找),同时改进了插入和删除操作的时间性能

    • 利用间接寻址存储的线性表,在位置i处执行插入/删除操作,仍需后移/前移其他 数组元素的指针 ,时间复杂度仍为 O(n)

    • 但当每个元素占用的空间较大时,比顺序表插入/删除操作快得多(指针占用空间较小)

  • 缺点:仍未解决连续存储分配带来的表长难以确定的问题

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值