【数据结构】第二章 线性表

简介: 线性表是具有相同数据类型(值及包含的操作)的n个数据元素的有限序列(n为表长,n=0代表线性表是一个空表)。

考点(线性表是算法题命题的重点) 这类算法题实现起来比较容易且代码量较少,但是要求具有最优的性能(时间复杂度、空间复杂度,才能获得满分。因此,应固掌握线性表的各种基本操作(基于两种存储结构),在平时的学习中多注重培养动手能力。另外,需要提醒的是,算法最重要的是思想!考场上的时间紧迫,在试卷上不一定要求代码具有实际的可执行性,因此应尽力表达出算法的思想和步骤,而不必过于拘泥每个细节注意算法题只能用 C/C++语言实现。

1 线性表的相关概念

线性表是具有相同数据类型(值及包含的操作)的n个数据元素的有限序列(n为表长,n=0代表线性表是一个空表)。表示为

线性表有下面两条逻辑特性:

  1. a1是第一个元素,称为表头元素。除表头元素外,其余元素都有一个直接前驱。
  2. an是最后一个元素,称为表尾元素。除表尾元素外,其余元素都有一个直接后继。

线性表有下面五条特点:

  1. 元素个数有限
  2. 具有逻辑上的顺序性,表示元素先后次序
  3. 表中元素都是数据元素(数据记录,如学生记录(学号, 姓名, 宿舍))
  4. 数据类型相同
  5. 表中元素具有抽象性,(仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容)

线性表基本操作(其它操作都需要调用基本操作来完成)

  1. 初始化表
  2. 求表长
  3. 按值查找
  4. 按位查找
  5. 插入
  6. 删除
  7. 输出
  8. 判空
  9. 销毁

下面要总结的是基于两种存储结构(顺序结构和链式结构)实现的线性表,需要注意的是线性表是一种逻辑结构,顺序结构和链式结构是存储结构。

2 线性表的顺序表示(顺序表)

用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。 下表第i个元素后面紧接着存储第i+1个元素,其中i称为元素ai在线性表的位序。

  1. LOC(A)代表数组A的起始位置。
  2. sizeof(ElemType)代表每个元素占用的存储空间大小,每个元素都与前一个元素相差一个元素存储空间大小。
  3. 虽然数组索引从0开始,但是数组的位序从1开始
  4. 通过2、3两条可以看出顺序表可以随机存取

2.1 顺序存储类型描述

通过数组定义顺序表,而一位数组可以是静态分配也可以是动态分配的

  1. 静态分配由于数组大小和空间事先已经定义好,一旦空间占满,再加入新的数据会导致程序崩溃

  1. 动态分配一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性地划分所有空间。
# define InitSize 100  // 表长度初始定义
type struct{
    ElemType *data;
    int MaxSize, length;
}

2.2 基本操作的实现

2.2.1 插入操作

在线性表的第i个位置插入数据元素e。若i输入不合法,则返回False。 否则将第i个元素后面的所有元素依次移动一个位置,将元素e插入位置i

注意这里i指的是位序(1到n) 时间复杂度:

  1. 最好时间复杂度:在表尾插入,不执行后移操作,O(1)
  2. 最坏时间复杂度:在表头插入,执行n次后移操作,O(n)
  3. 平均时间复杂度:计算节点平均移动的次数(过程如下图)为n/2,因此时间复杂度为O(n)

2.2.2 删除操作

删除顺序表L中第i个位置的元素。若i的输入不合法,返回False。否则,将被删元素赋予变量e,并将第i+1个元素及后面元素向前移动一格,返回True。

bool ListDelete(SqList &L, int i, Elemtype &e){
    if(i<1 || i>L.length) return false;
    e=L.data[i-1];
    for(int j=i; j<L.length; j++){
        L.data[j-1] = L.data[j];
        L.length--;
        return true;
    }
}
  1. 最好时间复杂度:删除表尾元素,不需要移动元素,O(1)
  2. 最坏时间复杂度:删除表头元素,需要移动n-1个元素,O(n)
  3. 平均时间复杂度:假设pi是删除第i个位置的概率,计算移动结点的平均次数为(n-1)/2,则O(n)

2.2.3 按值查找、按序查找

在循序表L中找到一个值等于e的元素并返回位序。

  1. 最好时间复杂度:查找元素在表头,仅进行一次比较,O(1)
  2. 最坏时间复杂度:查找元素在头尾,进行n次比较,O(n)
  3. 平均时间复杂度:假设pi是需要查找的元素的在位置i上的概率,则计算平均查找次数为(n+1)/2,则O(n)

由于顺序表有随机存取的特点,因此按序查找时间复杂度是O(1)

2.2.4 总结

  1. 插入操作和删除操作时间主要耗费在移动元素上
  2. 查找操作时间主要耗费在比较上

3 线性表的链式表示

链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需修改指针。但也会失去顺序表可随机存取的优点。 链表分为单链表、双链表、循环链表、静态链表

3.1 单链表

通过一组任意的存储单元来存储线性表中的数据元素,每个元素结点/存储单元都包含元素信息和指向下一个元素的指针,以此建立元素之间的线性关系。

3.1.1 结点

单链表结点如上图所示,data为数据域存放元素数据,next为指针域存放后继结点的位置。 单链表的结点类型描述

3.1.2 带头结点的单链表

  1. 一般在第一个结点之前会设置一个头结点,头结点的数据域可以不存放任何信息或存放表长信息(此时头结点称为第一个结点)
  2. 当L指向NULL时,表示是一个空表。

区分头结点和头指针 如图2.4,头指针指的是指向第一个结点的指针,头结点指的是带头链表中第一个结点(通常带有存储信息)。

头结点带来的两个好处

  1. 第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理
  2. 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一

3.1.3 头插法建立单链表

  1. 创建头结点,并初始化链表(头指针L指向NULL)
  2. 创建新结点N
  3. 新结点后继结点指向头结点的后继结点
  4. 头结点后继结点指向新结点

假设每个结点插入时间为O(1),则n个结点为O(n)

3.1.4 尾插法建立单链表

  1. 创建头结点,并初始化链表(头指针L指向NULL)
  2. 表尾指针指向结点的后继结点指向新结点
  3. 表尾指针指向新结点

时间复杂度与头插法一致

3.1.5 按序号查找

从单链表头结点出发,顺着next指针搜索位置i的结点,如果没有则返回False

时间复杂度是O(n)

3.1.6 按值查找

从单链表第一个开始,顺着next依次比较值。若某结点的数据域的值等于给定值返回指向该结点的指针,否则返回False。

时间复杂度是O(n)

3.1.7 插入操作

在位置i插入给定值

  1. 找到第i个结点的前驱结点,即第 i-1个结点p
  2. 新结点后继结点指向p结点的后继结点b
  3. p结点的后继结点指向新结点

插入操作的时间主要耗费在寻找位序,复杂度为O(n)。而插入仅仅为O(1)

3.1.8 删除结点

将单链表的第i个结点删除。

  1. 查找删除结点的前驱结点,如上图B
  2. 将前驱节点B的后继结点指向删除结点的后继结点
  3. 释放删除结点的空间

与插入节点类似,时间主要耗费在查找上,复杂度为O(n)

3.1.9 求表长

计算单链表中数据结点(不含头结点)的个数。从第一个结点开始依次查找每个结点,同时设置一个计数器,每访问到一个结点,计数器加1,直至访问结点为空。时间复杂度为O(n)。 需要注意的是计算表长不算头结点。

3.2 双链表

双链表中存在两个指针——prior(指向前驱结点)和next(指向后继结点)

结点类型描述:

typedef struct DNode{
        ElemType data;
        struct DNode *prior, *next;
}DNode, *DLinkList;

双链表只是增加了一个前驱结点,因此按值查找和按位查找操作与单链表一致,但是插入和删除有了较大的改变。

3.2.1 插入操作

位置i插入结点

  1. 找到位置i结点的前一个结点和后一个结点,即p结点和c结点
  2. 插入结点的后继结点指向c结点,c结点的前驱结点指向插入结点
  3. p结点的后继节点指向插入结点,插入结点的前驱结点指向p结点

注意顺序不一致可能导致出错

3.2.3 删除操作

将第i个结点删除。

  1. 删除结点的前驱结点的后继节点指向删除结点的后继结点
  2. 删除结点的后继结点的前驱节点指向删除结点的前驱结点
  3. 释放删除结点的空间

3.3 循环链表

循环单链表和单链表的区别在于,表中最后一个结点的指针不是 NULL,而改为指向头结点从而整个链表形成一个环。分为循环单链表和循环双链表。

3.3.1 循环单链表

表尾结点的next指向不再为空,而是表头结点。因此,循环单链表判空的条件不再是头指针指向是否为空,而是指向是否是头指针自身。 一般情况下,对循环单链表只设置表尾指针。原因是,若设的是头指针,对在表尾插入元素需要 O()的时间复杂度,而若设的是尾指针r,r->next 即为头指针,对在表头或表尾插入元素都只需要 O(1)的时间复杂度。

3.3.2 循环双链表

与双链表对比不同的是表头结点的前驱结点指向表尾结点,表尾结点的后继结点指向表头结点

3.4 静态链表

静态链表借助数组来表达链式结构,如下图

结点也存在数据域和指针域,指针域存储的是下一个结点在数组中的地址(相对地址),称为游标。 需要注意的是静态链表也需要分配一个连续的内存空间,且以-1作为不存在下一个结点的标志。

数据类型描述为

3.5 单链表、双链表、循环链表的比较

单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问一个结点时间复杂度为O(n)。双链表改进了上述问题。 循环单链表一般仅设尾指针,以使得操作效率更高。原因是,若设的是头指针,对在表尾插入元素需要 O(n)的时间复杂度,而若设的是尾指针 r,r->next 即为头指针,对在表头或表尾插入元素都只需要 O(1)的时间复杂度。

4 顺序表和链表的比较

  1. 读取方式和结构

存取(读写)方式

逻辑结构和物理结构

扩容

顺序表

顺序存取、随机存取

逻辑上相邻的物理上也相邻

静态存储不能,动态存储能

链表

顺序存取

逻辑上相邻的物理上未必相邻

可以

需要注意态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。因此,链式存储在这方面更加高效。

  1. 时间复杂度

顺序表

链表

按序查找

O(1)

O(n)

按值查找

顺序表无序时O(n),有序时

O(n)

插入/删除

平均时间复杂度O(n) 最好时间复杂度O(1) 最坏时间复杂度O(n)

查找+O(1)

5 错题记录

第一题

线性表:相同数据类型(数据元素和操作),有限元素

相关文章
|
5月前
|
存储 C语言
数据结构中的线性表链式存储介绍及其基本操作
链式存储是线性表的一种重要存储方式,它通过节点和指针的结构,实现了灵活的动态存储管理。本文介绍了单向链表的基本操作,并提供了相应的C语言代码示例。理解和掌握链表的操作对学习和应用数据结构具有重要意义。希望这篇博客能帮助你更好地理解线性表的链式存储。
131 2
|
1月前
|
存储 Java
数据结构第二篇【关于java线性表(顺序表)的基本操作】
数据结构第二篇【关于java线性表(顺序表)的基本操作】
31 6
|
25天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储
【数据结构】线性表和顺序表
【数据结构】线性表和顺序表
22 1
|
5月前
|
算法
数据结构和算法学习记录——线性表之双向链表(上)-结点类型定义、初始化函数、创建新结点函数、尾插函数、打印函数、尾删函数
数据结构和算法学习记录——线性表之双向链表(上)-结点类型定义、初始化函数、创建新结点函数、尾插函数、打印函数、尾删函数
49 0
|
1月前
01(数据结构考研)线性表相关操作代码
01(数据结构考研)线性表相关操作代码
68 0
|
1月前
|
存储 C语言
数据结构之线性表的初始化及其操作
数据结构之线性表的初始化及其操作
36 0
|
2月前
|
存储 Java
java数据结构,线性表顺序存储(数组)的实现
文章介绍了Java中线性表顺序存储(数组)的实现。线性表是数据结构的一种,它使用数组来实现。文章详细描述了线性表的基本操作,如增加、查找、删除、修改元素,以及其他操作如遍历、清空、求长度等。同时,提供了完整的Java代码实现,包括MyList接口和MyLinearList实现类。通过main函数的测试代码,展示了如何使用这些方法操作线性表。
|
5月前
|
存储 测试技术
【数据结构】操作受限的线性表,栈的具体实现
【数据结构】操作受限的线性表,栈的具体实现
62 5
|
5月前
|
存储 测试技术
【数据结构】操作受限的线性表,队列的具体实现
【数据结构】操作受限的线性表,队列的具体实现
45 4