线性表(线性逻辑结构)
线性表的定义
线性表(Linear List):是具有相同数据类型的$n(n \geq 0)$个数据元素的有限序列。线性表的结构是逻辑结构中的线性结构。一般表示为:
$$L = (a_1, a_2, \cdots, a_i, a_{i + 1}, \cdots, a_n) (n \geq 0)$$
线性表中元素具有相同的数据类型,即各元素所占空间大小相同。
线性表是$n(n \geq 0)$个元素的有限序列。说明线性表中元素个数有限,元素之间有序。(元素之间有序指的是线性表中的各元素之间有先后次序,不是其值有先后次序,注意理解元素和值的区别)逻辑特性
- 表头元素:$a_1$是唯一的“第一个”数据元素。
- 表尾元素:$a_n$是唯一的“最后一个”数据元素。
- 直接前驱:除第一个元素外,每个元素有且仅有一个直接前驱。
- 直接后继:除最后一个元素外,每个元素有且仅有一个直接后继。
线性表的基本操作
- CreatList(&L):初始化表。构造一个空的线性表。
- Length(L):求表长。返回线性表L的长度。
- Get(L, i):按位查找。返回线性表L中第i个元素的值。
- Locate(L, x):按值查找。返回线性表L中值为x的数据元素的首次出现位序值。
- Insert(L, x, i):插入操作。在表L中的第i个位置上插入指定元素x。
- Delete(L, x):删除操作。删除表L中的第i个位置的元素。
上述操作中参数中"&"表示C++中的引用传递,在C语言中采用指针可以达到同样的效果。在C/C++中,需要使用传入传出参数时,可以使用引用传递或地址传递等方式进行传值操作。顺序表(线性表的顺序表示)
顺序表的定义
顺序表:线性表的顺序存储又称顺序表。使用一组地址连续的存储单元依次存储线性表中的数据元素。- 特点:表中元素的逻辑顺序与其物理顺序相同。
- 优点:线性表中任一数据元素都可以随机存取。(因为线性表中每个数据元素的存储位置都和线性表起始位置相差一个和该数据元素的位序成正比的常数,所以可以根据起始位置和位序之间计算出该元素的存储地址。)
高级程序设计语言中通常采用数组描述顺序表。
注意:线性表中元素的位序是从1开始的,数组元素的下标是从0开始的。
一般来说,线性表的第$i$个元素$a_i$的存储位置为:
$$Loc(a_i) = Loc(a_1) + (i - 1) \times Sizeof(DataType) \qquad 1 \leq i \leq n$$
假设线性表$L$的存储结构为LOC(A),线性表的数据元素类型为ElemType,Sizeof(ElemType)是每个数据元素所占存储空间的大小,则表L对应的存储结构如下图所示:
在创建顺序表时,可以通过静态分配和动态分配创建顺序表。
静态分配
在创建顺序表时,顺序表容量一经确定,不可以再更改。
- 缺点:由于数组大小和空间事先已经固定,一旦空间占满,再添加新的数据会产生溢出导致程序崩溃。
```C
// 静态分配顺序表的最大长度define MaxSize 50
typedef struct {
// 顺序表的数据元素
DataType data[MaxSize];
// 顺序表的当前长度
int length;
} SequeenList;
> 注意顺序表中length和MaxSize的区别,MaxSize指的是顺序表理论最大元素个数,length指顺序表当前长度个数
### 动态分配
动态分配,存储数组的空间在程序执行过程中通过动态存储分配语句分配的,数据空间存满,可以另外开辟一块更大的空间来替换原来的存储空间,达到扩容的目的,从而不需要一次性地分配所有空间。
> 动态分配依旧是顺序存储结构,不是链式存储结构,只是分配的空间大小可以在运行时动态决定。
```C
// 顺序表的初始化长度
#define InitSize 100
typedef struct {
// 动态数组指针
DataType *data;
// 顺序表的当前长度
int lenght;
// 数组最大容量,动态分配的最大长度是不确定的,所以相比于静态分配,需要额外的参数来存储数组最大容量值
int MaxSize;
} SequeenList;
C语言动态分配语句:
L.data = (DataType *)malloc(sizeof(DataType) * InitSize;
malloc函数:动态申请内存空间
会申请连续的一整片的内存空间,malloc函数返回一个指针,需强制转换成定义的数据元素类型指针。
free函数:动态释放内存空间
释放对应的内存空间,与malloc函数成对出现。在不需要内存空间时,请主动释放对应的内存空间,减少系统资源的占用顺序表的特性
顺序表的主要特点有:
- 随机访问,即通过首地址和元素位序可以在时间$O(1)$内找到指定元素。
- 存储密度高,每个结点只存储数据元素。
- 逻辑上相邻的元素物理上也相邻。
- 插入和删除操作需要移动大量元素。
静态分配顺序表的主要特点:
- 容量一经分配就已固定,不可以更改。
- 空间占满时再添加数据会产生溢出,导致程序崩溃。
- 通常采用数组实现顺序存储。
动态分配顺序表的主要特点:
- 容量再程序运行时动态决定。
- 一旦数据空间占满,可以通过开辟更大的存储空间进行扩容操作。
顺序表的基本操作
这里仅讨论顺序表的动态分配表上的基本操作,静态分配表的操作和动态分配表大同小异。
在下列操作中,若函数定义参数需要在外部使用,则需使用传入传出参数,通过指针// 动态分配顺序表 typedef struct { DataType *data; int length; int MaxSize; } SequenceList;
*
来进行地址传递,在调用函数时,或变量为普通变量,不是指针变量,则需要在参数中使用&
来进行取地址操作。
下列实现中,若无特别说明,以int的返回值作为函数是否执行成功的标志。0:执行失败,非0:执行成功。初始化顺序表
为数据元素分配内存空间,并初始化顺序表长度和最大值。/** * 初始化顺序表 * @param L 需要初始化的顺序表 * @param initLength 顺序表初始化长度 * @return */ int InitSequenceList(SequenceList *L, int initLength) { // 动态分配顺序表存储空间 L->data = (DataType *) malloc(sizeof(DataType) * initLength); // 初始化顺序表参数 L->length = 0; L->MaxSize = initLength; return 1; }
插入操作
- 判断位序是否越界:位序$i$的取值范围:$1 \leq i \leq L.length + 1$,插入时可以在表尾后插入数据,所以位序可以等于$L.length + 1$
- 判断顺序表存储空间是否已满,若满,则不能进行插入操作,否则会产生溢出。空间已满:$L.length \geq L.MaxSize$
- 可以正常插入,将以$i$开始后面的元素都向后移动一个存储空间,给元素$i$留下存储位置
- 插入成功后,$L.length$加1
/** * 插入操作 * @param L 需要执行插入的顺序表 * @param i 位序(i <= i <= length + 1) * @param data 插入元素 * @return */ int SequenceListInsert(SequenceList *L, int i, DataType data) { if (i < 1 || i > L->length + 1) { // 插入位置非法:i <= i <= length + 1(i为顺序表位序) printf("Error:Illegal insertion position!\n"); return 0; } else if (L->length >= L->MaxSize) { // 顺序表溢出,顺序表最大长度为MaxSize,超过MaxSize,则会产生溢出错误 printf("Error:Sequence list overflow!"); return 0; } else { // 可以进行正常插入操作 // 从i开始的所有元素后移一位 for (int j = L->length; j >= i; j--) { L->data[j] = L->data[j - 1]; } // 在位置i处放入插入元素 L->data[i - 1] = data; // 顺序表长度加1,注意该语句,不可省略 L->length++; return 1; } }
后移:int j = L->length; j >= i; j--
- 最好情况:在表尾插入($i = n + 1$),元素不执行后移操作,时间复杂度$O(1)$
- 最坏情况:在表头插入($i = 1$),元素后移执行n次,时间复杂度$O(n)$
- 平均情况:在第$i$个位置上插入的平均次数为$\frac{n}{2}$,时间复杂度为$O(n)$
删除操作
- 判断位序是否越界:$1 \leq i \leq n$,注意和插入操作的位序取值范围,插入时可以在表尾后插入,但删除时不能删除表尾后的元素(理论上没有该元素,自然无法删除)
- 将第$i + 1$个元素及其后的所有元素依次往前移动一个位置
- 删除成功后,$L.length$减1
/** * 删除操作 * @param L 需要执行插入的顺序表 * @param i 位序(i <= i <= length) * @param data 传入传出参数,用于接收删除元素的值 * @return */ int SequenceListDelete(SequenceList *L, int i, DataType *data) { if (i < 1 || i > L->length) { // 删除位置非法:i <= i <= length(i为顺序表位序) printf("Error:Illegal delete position!\n"); return 0; } else { // 可以进行正常删除操作 // 将删除元素的值传出给data *data = L->data[i - 1]; // 从i + 1开始后的所有元素前移一位 for (int j = i; j < L->length; j++) { L->data[j - 1] = L->data[j]; } L->length--; return 1; } }
前移:int j = i; j < L->length; j++
- 最好情况:删除表尾元素($i = n$),无须移动元素,时间复杂度$O(1)$
- 最坏情况:删除表头元素($i = 1$),需移动除表头元素外的所有元素,时间复杂度$O(n)$
- 平均情况:在第$i$个位置上删除需要移动结点的平均次数为$\frac{n - 1}{2}$,时间复杂度为$O(n)$
按位序查找
/** * 按位序查找 * @param L 需要执行查找的顺序表 * @param i 位序(i <= i <= length) * @param data 传入传出参数,用于接收查找到的值 * @return */ int SequenceListGet(SequenceList *L, int i, DataType *data) { if (i < 1 || i > L->length) { // 获取元素位置非法:i <= i <= length(i为顺序表位序) printf("Error:Illegal get data position!\n"); return 0; } else { *data = L->data[i - 1]; return 1; } }
按值查找
- 从头到尾依次遍历元素,比较是否和需要查找的元素相等,相等则返回位序,反之返回0
/** * 按值查找 * @param L 需要执行查找的顺序表 * @param data 需要比较的元素 * @return 位序,0:未查找到该元素 */ int SequenceListLocate(SequenceList *L, DataType data) { for (int i = 0; i < L->length; i++) { if (L->data[i] == data) { // 这里的i为数组下标,需转换为位序值 return i + 1; } } return 0; }
销毁顺序表
/** * 销毁顺序表 * @param L 需要销毁的顺序表 * @return */ int DestroySequenceList(SequenceList *L) { L->length = 0; L->MaxSize = 0; // 释放申请的空间 free(L->data); L->data = NULL; return 1; }