《大话数据结构》学习记录3——线性表

三、线性表

1、定义

线性表 ( L i s t List List ):零个或多个数据元素的有限序列。

  • 序列:元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
  • 有限:线性表强调是有限的,元素个数当然也是有限的。
  • 数学定义:若将线性表记为 ( a 1 , . . . , a i − 1 , a i , a i + i , . . . , a n ) (a_1, ...,a_{i-1}, a_i, a_{i+i}, ..., a_n) (a1,...,ai1,ai,ai+i,...,an), 则表中 a i − 1 a_{i-1} ai1 领先于 a i a_{i} ai a i a_{i} ai 领先于 a i + 1 a_{i+1} ai+1,称 a i − 1 a_{i-1} ai1 a i a_{i} ai 的直接前驱元素, a i + 1 a_{i+1} ai+1 a i a_{i} ai 的直接后继元素。当$i=1,2,…,n-1 时, 时, 时,a_{i}$ 有且仅有一个直接后继,当 $i=2, 3,…,n $ 时, a i a_{i} ai 有且仅有一个直接前驱。
    • 在这里插入图片描述
    • 线性表元素的个数 n ( n > 0 ) n (n>0) n(n>0) 定义为线性表的长度,当 n = 0 n=0 n=0 时,称为空表。
    • 在较复杂的线性表中,一个数据元素可以由若干个数据项组成。譬如“刘华强”这一数据元素,包含有“喜爱的食物:西瓜”、“坐骑:摩托车”等数据项。

2、线性表的抽象数据类型

线性表的基本操作:

  • InitList(*L): 初始化操作,建立一个空的线性表 L
  • ListEmpty (L): 若线性表为空,返回 true, 否则返回 false
  • ClearList(*L): 将线性表清空
  • GetElem ( L,i,*e):将线性表 L 中的第 i 个位置元素值返回给 e
  • LocateElem ( L,e):在线性表 L 中查找与给定值 e 相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回 0 表示失败
  • Listinsert ( *L,i,e ): 在线性表 L 中的第 i 个位置插入新元素 e
  • ListDelete(*L,i,*e):删除线性表 L 中第 i 个位五元素,并用 e 返回其值
  • ListLength (L): 返回线性表 L 的元素个数

3、线性表的顺序存储结构

(1)顺序存储定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

(2)顺序存储方式

  • C中的一维数组可以来实现顺序存储结构
  • 线性表的顺序存储结构,就是在内存中找了块地儿,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。
  • 线性表中,我们估算这个线性表的最大存储容量,建立一个数组,数组的长度就是这个最大存储容量。
  • 有了起始的位置,也有了最大的容量,于是我们可以在里面增加数据了。随着数据的插入,我们线性表的长度开始变大,不过线性表的当前长度不能超过存储容量,即数组的长度。

据此与上述对应的,顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组 d a t a data data, 它的存储位置就是存储空间的存储位置。
  • 线性表的最大存储容量:数组长度 M a x S i z e MaxSize MaxSize
  • 线性表的当前长度: l e n g t h length length

(3)数据长度与线性表长度区别

数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般是不变的;
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。

(4)地址计算方法

用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。
在这里插入图片描述
存储器中的每个存储单元都有自己的编号,这个编号称为地址

由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是 c c c 个存储单元,那么线性表中第 i + 1 i+1 i+1 个数据元素的存储位置和第 i i i 个数据元素的存储位置满足下列关系( L O C LOC LOC 表示获得存储位置的函数)。
L O C ( a i + 1 ) = L O C ( a i ) + c LOC(a_{i+1})= LOC(a_i)+c LOC(ai+1)=LOC(ai)+c
L O C ( a i ) = L O C ( a 1 ) + ( i − l ) ∗ c LOC(a_i)= LOC(a_1)+(i-l)*c LOC(ai)=LOC(a1)+(il)c
在这里插入图片描述

(5)存入或者取出数据

通过这个公式,你可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间。
那么我们对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用我们算法中学到的时间复杂度的概念来说,它的存取时间性能为 O ( 1 ) O(1) O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。

4、顺序存储结构的插入与删除

(1)元素操作

  • 获得元素操作
    • 要实现 G e t E l e m GetElem GetElem 操作,即将线性表 L L L中的第 i i i 个位置元素值返回,只要 i i i 的数值在数组下标范围内,就是把数组第 i − 1 i-1 i1下标的值返回即可。
  • 插入操作
    • 要实现Listinsert (*L,i,e), 即在线性表 L L L 中的第 i i i 个位置插入新元素 e e e
    • 插入算法的思路:
      • 如果插入位置不合理,抛出异常;
      • 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
      • 从最后一个元素开始向前遍历到第 i i i个位置,分别将它们都向后移动一个位置;
      • 将要插入元素填入位置 i i i 处;
      • 表长加 1 1 1
  • 删除操作
    • 删除算法的思路:
      • 如果删除位置不合理,抛出异常;
      • 取出删除元素;
      • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
      • 表长减 1 1 1

(2)操作的时间复杂度

  • 如果元素要插入到最后一个位置,或者删除最后一个元素,此时时间复杂度为 O ( 1 ) O(1) O(1), 因为不需要移动元素的。
  • 最坏的情况,如果元素要插入到第一个位置或者删除第一个元素,此时时间复杂度是 O ( n ) O(n) O(n)
  • 至于平均的情况,由于元素插入到第 i i i 个位置,或删除第 i i i 个元素,需要移动 n − i n-i ni个元素。
  • 根据概率原理,每个位置插入或删除元素的可能性是相同的,也就说位置靠前,移动元素多,位置靠后,移动元素少。最终平均移动次数和最中间的那个元素的移动次数相等,为 n − 1 2 \frac{n-1}{2} 2n1。推导可以得出,平均时间复杂度还是 O ( n ) O(n) O(n)
  • 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是 O ( 1 ) O(1) O(1);而插入或删除时,时间复杂度都是 O ( n ) O(n) O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。

(3)优缺点

  • 优点
    • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
    • 可以快速地存取表中任一位置的元素
  • 缺点
    • 插入和删除操作需要移动大量元素
    • 当线性表长度变化较大时,难以确定存储空间的容量
    • 造成存储空间的 “碎片”

5、线性表的链式存储结构

(1)线性表的链式存储结构——通俗解释

干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。

(2)线性表的链式存储结构——特点

用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置
在这里插入图片描述

(3)线性表的链式存储结构——区别

以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址

  • 为了表示每个数据元素 a i a_i ai 与其直接后继数据元素 a i + 1 a_{i+1} ai+1 之间的逻辑关系,对数据元素 a i a_i ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。

(4)线性表的链式存储结构——术语名词

  • 我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息称做指针或链。这两部分信息组成数据元素 a i a_i ai 的存储映像,称为结点(Node)。

  • 结点由存放数据元素的数据域存放后继结点地址的指针域组成

  • n 个结点(即的存储映像)链结成一个链表,即为线性表( a 1 , a 2 , … , a n a_1,a_2, …,a_n a1a2,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域,将线性表的数据元素按其逻辑次序链接在一起。

  • 我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。

  • 最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为 “空”(通常用 NULL 或 "^"符号表示)

  • 为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。

  • 在这里插入图片描述

  • 在这里插入图片描述

  • 带有头结点的单链表
    在这里插入图片描述

  • 空链表
    在这里插入图片描述

  • 头指针与头结点的异同

    • 头指针:指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
      • 头指针具有标识作用,所以常用头指针冠以链表的名字。
      • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
    • 头结点:为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义( 也可存放链表的长度 )
      • 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
      • 头结点不一定是链表必须要素

6、单链表的读取

不同于线性表的顺序存储结构,单链表获取第 i i i 个元素的数据比较困难, 获得链表第 i i i 个数据的算法思路:

  • 声明一个结点 p p p 指向链表第一个结点,初始化 j j j 从1开始;
  • j < i j<i j<i 时,就遍历链表,让 $p 的指针向后移动,不断指向下一结点, 的指针向后移动,不断指向下一结点, 的指针向后移动,不断指向下一结点,j$ 累加1;
  • 若到链表末尾 p p p 为空,则说明第 i i i 个元素不存在;
  • 否则查找成功,返回结点 p p p 的数据

缺点:

  • 说白了,就是从头开始找,直到第 i i i 个元素为止。由于这个算法的时间复杂度取决于 i i i 的位置,当 i = 1 i=1 i=1 时,则不需遍历,第一个就取出数据了,而当 i = n i=n i=n 时则遍历 n − i n-i ni 次才可以。因此最坏情况的时间复杂度是 O ( n ) O(n) O(n)
  • 由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用 for 来控制循环。其主要核心思想就是“工作指针后移”,这其实也是很多算法的常用技术。

7、单链表的插入与删除

(1)单链表的插入

  • 假设存储元素 e e e 的结点为 s s s, 要实现结点 p p p p − > n e x t p->next p>next s s s 之间逻辑关系的变化,只需将结点 s s s 插入到结点 p p p p − > n e x t p->next p>next 之间即可。

  • p p p 的后继结点改成 s s s 的后继结点,再把结点 s s s 变成 p p p 的后继结点。 s->next=p->next; p->next=s;
    在这里插入图片描述

  • 单链表的表头和表尾的特殊情况
    在这里插入图片描述

(2)单链表的删除

  • 设存储元素 a i a_i ai 的结点为 q q q, 要实现将结点 q q q 删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可。
  • p->next=p->next->next, 用 q q q 来取代 p − > n e x t p->next p>nextq=p->next; p->next=q->next;
    在这里插入图片描述

(3)分析

  • 单链表插入和删除算法的时间复杂度都是 O ( n ) O(n) O(n)。如果在我们不知道第 i i i 个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第 i i i 个位置,插入10 个元素,对于顺序存储结构意味着,每一次插入都需要移动 n − i n-i ni 个元素,每次都是 O ( n ) O(n) O(n)。而单链表,我们只需要在第一次时,找到第 i i i 个位置的指针,此时为 O ( n ) O(n) O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是 O ( 1 ) O(1) O(1)
  • 显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。

8、单链表的整表创建

单链表和顺序存储结构不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。

单链表整表创建的算法思路:

  • 声明一结点 p p p 和计数器变量 i i i
  • 初始化一空链表 L L L;
  • L L L 的头结点的指针指向 N U L L NULL NULL, 即建立一个带头结点的单链表;
  • 循环:
    • 生成一新结点赋值给 p p p
    • 随机生成一数字赋值给 p p p 的数据域 p − > d a t a p->data p>data;
    • p p p 插入到头结点与前一新结点之间。

(1)头插法

本质是插队的办法,就是始终让新结点在第一的位置
在这里插入图片描述

/*随机产生n 个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead ( LinkList *L, int n)
{
  LinkList p;
  int i;
  srand(time(0));                   /*初始化随机数种子*/
  *L = (LinkList)malloc(sizeof(Node));
  (*L)->next = NULL;                  /*先定立一个带头结点的单链表*/
  for ( i=0; i<n; i++)
  {
    p = (LinkList)malloc(sizeof(Node));/*生成新结点*/
    p->data = rand()%100+l;/*随机生成 100 以内的数字*/
    p->next = (*L)->next;
    (*L)->next = p;   /*插入到表头*/ 
  }
}

(2)尾插法

把每次新结点都插在最后,所谓的先来后到。

/* 随机产生 n 个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail( LinkList *L, int n)
{
  LinkList p,r;
  int i;
  srand(time(0));                         /*初始化随机数种子*/
  *L = (LinkList)malloc(sizeof(Node));    /*为整个线性表*/
  r=*L;                                  /*r 为指向尾部的结点*/
  for ( i=0; i<n; i++)
  {
    P = (Node *)malloc(sizeof(Node)); /* 生成新结点*/
    p->data = rand()%100+l;           /*随机生成 100 以内的敷字*/
    r->next=p;                         /*将表尾终端结点的指针指向新结点*/
    r = P;                            /*将当前的新结点定义为表尾终端结点*/
  }
  r->next = NULL;                    /*表示当前链表结束*/
}

L L L 是指整个单链表,而 r r r 是指向尾结点的变量, r r r 会随着循环不断地变化结点,而 L L L 则是随着循环增长为一个多结点的链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

frozendure

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值