【数据结构与算法】线性表的定义和基本操作

728 阅读7分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,点击查看活动详情

🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年9月18日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生

线性表的定义

线性表,通俗来讲,就像我们去食堂排队打饭一样,它具有线一样性质的结构

线性表(List):由零个或多个数据元素组成的有限序列

这里需要强调几个关键的地方:

  • 首先它是一个序列,也就是说元素之间是有顺序的,按先来后到排序
  • 如果元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继
  • 另外,线性表强调是有限的,事实上无论计算机发展到多强大,它所处理的元素都是有限的

如果用数学语言来进行定义,可如下:

若将线性表记为(a1, ... , ai-1, ai, ai+1, ... , an),则表中 ai-1 领先于 aiai 领先于 ai+1,称 ai-1ai 的直接前驱元素,ai+1ai 的直接后继元素

image.png

所以线性表元素的个数 n (n≥0) 定义为线性表的长度,当 n=0 时,我们称之为空表

数据类型

什么是数据类型呢?

数据类型是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。比如,很多编程语言的整型、浮点型、字符型这些指的就是数据类型

在计算机中,内存不是无限大的,如果你想计算 1+1=2 这样的整型数字的加减乘除运算,显然不需要开辟很大的内存空间

而当你需要计算 1.23456789+2.987654321 这样带大量小数的,就需要开辟比较大的空间才存放得下

于是计算机的研究者们就考虑,要对数据类型进行分类,分出多种数据类型来适合各种不同的计算条件差异

例如在 C语言 中,按照取值的不同,数据类型可以分为两类:

  • 原子类型:不可以再分解的基本类型,例如整型、浮点型、字符型等等
  • 结构类型:由若干个类型组合而成,是可以再分解的,例如整型数组是由若干个整型数据组成的

抽象数据类型

什么是抽象?

抽象是指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括,抽象是一种思考问题的方式,它隐藏了繁杂的细节

我们对已有的数据类型进行抽象,就有了抽象数据类型

抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作

抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关

比如 1+1=2 这样的一个操作,在不同的 CPU 上处理可能不一样,但由于其定义的数学特性相同,所以在计算机编程者看来,它们都是相同的

“抽象” 的意义在于数据类型的数学抽象特性,而且,抽象数据类型不仅仅指的是那些已经定义并实现的数据类型,还可以是计算机编程者在设计软件程序时自己定义的数据类型

例如在一个 3D 游戏中,要定位角色的位置,那么总会出现 x, y, z 三个整型数据组合在一起的坐标。我们就可以定义一个 point 的抽象数据类型,它拥有 x, y, z 三个整型变量,这样我们就可以方便地对一个角色的位置进行操作

看到这里,我相信你应该明白抽象数据类型是什么了

描述抽象数据类型的标准格式如下

ADT 抽象数据类型名
Data 
	数据元素之间逻辑关系的定义
Operation
	操作
endADT

线性表的抽象数据类型

线性表的抽象数据类型定义:

ADT 线性表(List)
Data
 线性表的数据对象集合为 {a1, a2, ... , an}, 每个元素
 的类型均为 DataType。其中,除第一个元素 a1 外,每一
 个元素有且只有一个直接前驱元素,除了最后一个元素 an 外,
 每一个元素有且只有一个直接后继元素。数据元素之间的关系
 是一对一的关系
Operation
 InitList(&L):初始化表。构造一个空的线性表 L,分配内存空间
 DestroyList(&L):销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间
 ListInsert(&L, i, e):插入操作。在线性表 L 中第 i 个位置插入指定元素 e 
 ListDelete(&L, i, &e):删除操作。删除线性表 L 中第 i 个位置的元素,并用 e 返回删除元素的值 
 LocateElem(L, e):按值查找操作。在线性表 L 中查找与给定值 e 相等的元素,如果查找成功,则返回该元素在表中序号;否则,返回 0 表示失败
 GetElem(L, i):按位查找操作。获取线性表 L 中第 i 个位置的元素的值
 Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数
 PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值
 ListEmpty(L):判空操作。如果线性表 L 为空表,则返回 true,否则返回 false
endADT 

注意:

  1. 对数据的操作 —— 创建销毁、增删改查
  2. C语言函数的定义 —— <返回值类型> 函数名 (<参数1类型> 参数1, <参数2类型> 参数2, ......)
  3. 实际开发中,可根据实际需求定义其他的基本操作
  4. 函数名和参数的形式、命名都可以改变,命名要有可读性
  5. 什么时候要传入引用 “&” —— 对参数的修改结果需要 “带回来”

对于第 3 点的具体解释

对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的其他更复杂的操作,完全可以用这些基本操作的组合来实现

举个例子,比如我们想实现两个线性表 A、B 的并集操作,即要使得集合 A=AUB,说白了,就是将存在集合 B 中但不存在集合 A 中的元素插入到 A 中即可

具体操作就是,循环遍历集合 B 中的每个元素,然后判断当前元素是否存在集合 A 中,如果不存在,则插入集合 A 中即可

需要用到的基本操作有:

  • Length(L);
  • GetElem(L, i);
  • LocateElem(L, e);
  • ListInsert(&L, i, e);

对于第 5 点的具体解释

我们先看一下下面这个程序

#include <stdio.h>

void test(int x) {
	x = 1024;
	printf("test函数内部:x=%d\n", x);
}

int main() {
	int x = 1;
	printf("调用test前:x=%d\n", x);
	test(x);
	printf("调用test后:x=%d\n", x);
	return 0;
}

运行结果如下:

image.png

为什么 x 的值没有改变呢?

其实,test 函数里面的 xmain 函数里面 x 的一个复制品,虽然它们都叫做 x,但是在内存中的地址却不同,对一个 x 的修改自然不会影响到另一个 x

接下来,我们将 test 函数修改一下,传引用进去

#include <stdio.h>

void test(int &x) {
	x = 1024;
	printf("test函数内部:x=%d\n", x);
}

int main() {
	int x = 1;
	printf("调用test前:x=%d\n", x);
	test(x);
	printf("调用test后:x=%d\n", x);
	return 0;
}

运行结果如下:

image.png

为什么要实现对数据结构的基本操作?

  • 团队合作编程,你定义的数据结构要让别人能够很方便地使用(封装)
  • 将常用的操作 / 运算封装成函数,避免重复工作,降低出错风险