尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

线性表的定义与基本操作 — 从逻辑结构到 ADT 接口

线性表的定义与基本操作 — 从逻辑结构到 ADT 接口 引言第一次接触线性表这个名词时第一反应是把它和数组Array一组连续内存划等号。学完链表之后又觉得线性表就是数组或链表的一种线性表Linear List本质上是一个逻辑结构Logical Structure概念。它描述的是数据元素之间一对一的线性关系与数据在内存中如何存放——顺序存储还是链式存储——是两个层次的问题。本文逐层拆解线性表的定义、九种 ADT 接口规范为后续顺序表和链表的实现篇铺路。核心要点线性表是逻辑结构不是物理实现。顺序表Sequential List和链表Linked List是线性表的两种存储方式——同一套逻辑接口,两种不同的物理组织。线性表 ADT 定义了九种基本操作。InitList、DestroyList、ListInsert、ListDelete、LocateElem、GetElem、Length、PrintList、Empty 构成线性表的完整接口契约Interface Contract所有实现都必须支持这些操作。位序从 1 开始计数。线性表的位序Position是 1-based与 C 语言数组下标0-based不同代码中需要 i-1 转换。“数据元素不同于数据项”。一个数据元素可以有多个数据项如学生记录包含学号、姓名、成绩把学号误称为数据元素是选择题中反复出现的干扰项设计手法。什么是线性表——从逻辑结构谈起[共识] 线性表的严格定义由 n (n 0) 个相同数据类型Data Type的数据元素Data Element组成的有限序列Finite Sequence。记作L(a1,a2,…,ai,ai1,…,an) L (a_1, a_2, \ldots, a_i, a_{i1}, \ldots, a_n)L(a1​,a2​,…,ai​,ai1​,…,an​)其中 n 为表长Length。当 n 0 时L 为空表Empty List。每个 a_i 是一个数据元素可以是简单的整数值也可以是复杂的结构体——但同一线性表中所有元素的数据类型必须相同。[共识] 线性结构区别于树结构和图结构的根本特征在于一对一的逻辑关系除第一个元素表头元素无直接前驱、最后一个元素表尾元素无直接后继外表中每个数据元素有且仅有一个直接前驱Immediate Predecessor和一个直接后继Immediate Successor。这种线性约束将元素组织成一条逻辑上的链而非树的一对多展开或图的多对多网状连接。对此类比的系统讨论可参见本系列第六章关于图结构作为非线性结构的代表的文章。数据元素Data Elementvs 数据项Data Item——这是 408 选择题中反复出现的一对概念近十年真题中至少出现了 5 次 [引用]。二者的区别在于层级数据元素是线性表的基本组成单位而一个数据元素内部可以包含多个数据项。以学生信息表为例整个表是一个线性表表中每一位学生的完整记录是一个数据元素而学号、姓名、成绩分别是这个数据元素的三个数据项。题干中常见的干扰手法是把学号或成绩描述为数据元素——这正是偷换了层级关系。记忆锚点“有限序列同类数据线性关系”——12 个字概括线性表的全部定义要素。** 线性表逻辑结构示意**位置: 第1位 第2位 第3位 第4位 第5位 ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ a₁ │ → │ a₂ │ → │ a₃ │ → │ a₄ │ → │ a₅ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ ↑ 无前驱 ↑ 无后继 首元素 尾元素 每个中间元素 aᵢ (i2,3,4)唯一前驱 aᵢ₋₁ 唯一后继 aᵢ₊₁线性表不等于顺序表——逻辑与物理的层次分离数据结构的三要素——逻辑结构、存储结构物理结构和运算——构成了一个自上而下的层次体系。线性表位于逻辑结构层描述的是元素之间是什么关系而数据具体怎样存放在内存中属于存储结构层的事。把线性表等同于顺序表就像把交通工具等同于汽车——前者是抽象类别后者是具体实现。顺序表采用顺序存储Sequential Storage所有元素存放在一片地址连续的存储单元中逻辑相邻的元素物理上也相邻。链表采用链式存储Linked Storage元素分布在不连续的内存空间通过每个结点携带的指针Pointer串联其逻辑后继。二者都以线性表的逻辑关系为蓝本但在物理组织方式上完全不同。因此以下说法在 408 考试中是错误的——“线性表的存储结构是顺序存储”——正确表述应该是线性表可以采用顺序存储结构实现。学完链表后被问到什么是线性表时脱口而出线性表就是数组或链表。这正是逻辑结构与物理结构的混淆——把接口和实现看成了同一个东西。先定义接口ADTAbstract Data Type抽象数据类型再讨论实现不是课本在故弄玄虚而是软件工程中最基本的抽象分层思想。定义清楚线性表能做什么之后再分别用顺序存储和链式存储去兑现这些接口——这本质上就是面向对象设计中面向接口编程Program to an Interface在数据结构领域的直接映射。线性表的 ADT——九种基本操作严蔚敏《数据结构C 语言版》将线性表的基本操作归纳为以下九种 [引用]构成一套完整的接口契约。任何线性表的实现——无论是基于数组的顺序表还是基于指针的链表——都必须对外提供这九种操作。下面给出完整的数据类型定义和九种操作的函数签名。为便于阅读先定义基础类型别名和顺序表结构体链式版本将在后续文章展开// 基础类型定义#defineMAXSIZE100// 顺序表最大容量可根据需要调整typedefintElemType;// 元素类型示例使用 int实际可替换为任意类型typedefintStatus;// 操作状态返回值OK 或 ERROR#defineOK1#defineERROR0// 顺序表结构体定义typedefstruct{ElemType data[MAXSIZE];// 存储数据元素的数组intlength;// 当前表长已存储的元素个数}SqList;在此结构体之上九种基本操作的接口规范如下/* 1. 初始化线性表构造一个空表将 length 置 0 */StatusInitList(SqList*L);/* 2. 销毁线性表释放表占用的内存资源静态数组无需额外释放 动态分配的数组需调用 free */StatusDestroyList(SqList*L);/* 3. 插入操作在表 L 的第 i 个位序处插入元素 e 插入前要求 1 i L.length 1允许在表尾后一个位置插入 插入成功后原第 i 个及之后元素依次后移一位表长 1 */StatusListInsert(SqList*L,inti,ElemType e);/* 4. 删除操作删除表 L 的第 i 个位序处的元素用 *e 返回其值 删除后原第 i1 个及之后元素依次前移一位表长 -1 */StatusListDelete(SqList*L,inti,ElemType*e);/* 5. 按值查找在表 L 中查找第一个值等于 e 的元素 返回其位序1-based若未找到则返回 0 */intLocateElem(SqList L,ElemType e);/* 6. 按位查找返回表 L 中第 i 个位序的元素值通过 *e 传出 注意i 是线性表位序1-based不是 C 数组下标 */StatusGetElem(SqList L,inti,ElemType*e);/* 7. 求表长返回表 L 中当前数据元素的个数 */intLength(SqList L);/* 8. 输出全表依次打印表 L 中所有元素的值 */StatusPrintList(SqList L);/* 9. 判空若表 L 为空表length 0返回 true否则返回 false */boolEmpty(SqList L);以上九种操作可以根据其功能归纳为一条记忆口诀“增删改查创建销毁判空求长遍历输出。”其中 ListInsert 和 ListDelete 是两种核心写操作——它们的实现细节元素移动、边界判断、时间复杂度分析构成了后续顺序表和链表文章的主体内容。值得留意的是所有操作中使用位序的参数 i 都从 1 开始取值。这并非来自 C 语言的惯例而是 ADT 层面的独立设计——位序概念属于逻辑层不应被具体编程语言的下标规则所约束。下一节会展开讨论这一设计选择带来的实际编码影响。** 九种基本操作速查表**操作名参数功能摘要返回值位序相关InitList(L)线性表引用构造一个空的线性表OK / ERROR否DestroyList(L)线性表引用销毁线性表释放所有资源OK / ERROR否ListInsert(L, i, e)线性表引用, 位序, 元素在第 i 位插入元素 eOK / ERROR是 (1 ≤ i ≤ length1)ListDelete(L, i, e)线性表引用, 位序, 元素引用删除第 i 位元素并用 e 返回OK / ERROR是 (1 ≤ i ≤ length)LocateElem(L, e)线性表, 元素值查找第一个值为 e 的元素位序位序 (0 表示未找到)否 (按值查找)GetElem(L, i)线性表, 位序获取第 i 位元素的值元素值 / NULL是 (1 ≤ i ≤ length)Length(L)线性表返回线性表中元素的个数整数 (≥ 0)否PrintList(L)线性表遍历输出线性表所有元素无否Empty(L)线性表判断线性表是否为空true / false否位序从 1 开始——一个细节线性表的位序Position从 1 开始计数a_1 是第 1 个元素a_2 是第 2 个元素依次类推至 a_n。这与 C 语言中数组下标从 0 开始的惯例形成了一处需要时刻注意的翻译关系。教材统一采用 1-based 位序并非随意为之。一方面序列中第 1 个的表述来自数学中的自然数序贴合人类的直觉——没有人会说第 0 个学生。另一方面位序的定义独立于任何编程语言无论你使用 C0-based 数组、Fortran1-based 数组还是 Python0-based list线性表 ADT 的位序始终是 1 到 n。这使得接口规范具有语言无关性——先定好逻辑层的坐标系再交给具体语言去做映射。但在用 C 语言实现时位序 i 与数组下标 i-1 之间的转换是绕不开的一步。以顺序表的按位查找操作为例// 按位查找获取顺序表 L 中第 i 个位置位序的元素值// 参数说明L 为顺序表值传递因为只读i 为位序1-based// *e 为传出参数用于接收查找到的元素值StatusGetElem(SqList L,inti,ElemType*e){// 步骤 1位序合法性检查// i 的合法范围是 [1, L.length]注意这里用了 L.length 而非 MAXSIZEif(i1||iL.length){returnERROR;}// 步骤 2位序 → 数组下标的转换// a1 存放在 data[0]a2 存放在 data[1]……ai 存放在 data[i-1]*eL.data[i-1];returnOK;}这段不到十行的代码中L.data[i - 1]的转换是 408 选择题和代码填空题直接考察最多的细节之一。命题人常在循环条件、边界判断和数组下标中把i和i-1互换以检验考生是否真正理解了两套计数体系的关系。建议在草稿纸上建立一个简单的对应表——位序在上、数组下标在下——写代码时逐项核对避免凭直觉直接填入。两种物理实现的预览——顺序存储与链式存储[共识] 线性表的两种基本物理实现方式在内存组织和操作效率上存在系统性差异。以下先给出概念层面的对比后续四篇文章将逐一对每种实现展开代码级分析。顺序存储Sequential Storage——顺序表。所有数据元素存放在一片连续的存储单元中。由于逻辑相邻的元素物理上也相邻顺序表支持 O(1) 的随机存取Random Access——给出位序 i直接通过data[i-1]即可访问。代价是插入和删除操作需要移动大量元素平均时间复杂度为 O(n)。链式存储Linked Storage——链表。数据元素存储在任意位置每个结点Node除数据域外还包含指针域Pointer Field指向其逻辑上的后继结点。逻辑相邻的元素在物理上可以相距很远。链表的插入和删除不需要移动元素仅修改指针即可时间复杂度为 O(1)但按位查找需要从表头逐个遍历时间复杂度为 O(n)。[共识] 两条重要结论(1) 两种实现方式下同一逻辑操作的时间复杂度不同——例如按位查找顺序表是 O(1)链表是 O(n)。(2) 即使某操作在两种实现下的时间复杂度量级相同实际运行效率也可能差异显著 [经验]——例如顺序表的遍历受益于 CPU 缓存Cache友好的连续内存布局而链表遍历伴随频繁的指针跳转和缓存未命中Cache Miss常数因子可以相差数倍。这正是时间复杂度相同的算法实际性能也接近这一常见误区的反例。** 顺序存储 vs 链式存储概览**对比维度顺序存储顺序表链式存储链表内存布局连续的一段存储空间离散的结点通过指针串联按位查找O(1)地址公式直接计算O(n)需从头遍历插入删除O(n)需移动后续元素O(1)已知位置仅修改指针空间开销仅数据域密度 100%额外指针域密度 ~50%单链表随机存取支持不支持扩容代价需预分配或整体搬迁按需分配无扩容开销缓存友好是连续内存预取友好否指针追逐cache miss 高适用场景频繁查找、数据量可预测频繁增删、数据量波动大常见问题 FAQQ1线性表和顺序表到底有什么区别为什么考研题里反复考这个线性表是逻辑结构定义了元素间一对一的线性关系顺序表是存储结构是线性表在连续内存中的一种具体实现。二者是接口与实现的关系。它直接检验考生是否建立了数据结构三要素逻辑结构、存储结构、运算的分层认知——而不是靠死记代码来应试。Q2线性表是逻辑结构那数组是逻辑结构还是存储结构在 C 语言的语境下数组本质上是一种存储结构——它代表一段连续内存每个元素大小相同支持按下标的快速寻址。当我们说用数组实现线性表时数组充当的是物理存储的载体。在 408 考试体系中通常将数组归类为存储结构概念顺序表则是以数组为存储载体的线性表实现。需要区分的是数组的下标规则0-based是语言层面的约定不是逻辑结构的属性。Q3为什么要先学 ADT 再看具体实现直接学代码不行吗ADT 的价值在于分离做什么和怎么做。先掌握九种操作的接口语义再去学顺序表和链表的代码实现你会发现两种实现虽然内部机制差别很大——一个靠下标跳转、一个靠指针遍历——但对外提供的操作名称和参数完全一致 。这层抽象不仅减少记忆负担不需要为每种实现单独背一套操作清单也是工程实践中面向接口编程的核心思想 [经验]。Q4位序从 1 开始代码里每次都要写 i-1 吗能提前把 i-- 再用吗是的在 C 语言实现的顺序表中因为数组下标从 0 开始而位序从 1 开始确实需要在位序 i 和数组下标之间做 i-1 转换。但不建议在调用方就把 i 减 1 再传入——这会导致位序的含义在调用链中不一致后续维护代码时极容易出错 [经验]。推荐做法是保持接口参数 i 的语义为位序1-based在函数体内部完成合法性检查i 1 || i L.length后续所有数组访问统一写成data[i - 1]转换点集中、可追溯。总结线性表是数据结构学习中第一个需要透彻理解逻辑结构独立于存储结构这一分层原则的概念。它以九种 ADT 操作为接口契约由顺序表和链表分别用不同的物理组织方式去兑现。掌握这一分层思想之后学习栈、队列、串、树、图等后续章节时你自然会先问它的逻辑结构是什么再问有哪些实现方式——这正是 408 考试希望考察的系统性思维也是工程师面对复杂系统时最重要的一种抽象能力。下一篇将深入线性表的顺序存储实现[顺序表的结构定义、插入删除与扩容]。 延伸阅读 (Further Reading)本系列第 2 篇[线性表的顺序存储实现——顺序表] —— 动态内存分配、插入删除的 O(n) 分析、扩容策略源码解读。本系列第 3 篇[线性表的链式存储实现——单链表] —— 结点结构、头插法与尾插法、带头结点与不带头结点的设计权衡。非线性结构对比[图结构作为非线性结构的代表]—— 理解线性结构一对一的约束边界之后再对比图结构多对多的自由连接模型。
返回列表