一、什么是线性表
目前,市面上几乎所有的数据结构教材都将线性表作为开篇第一章讲解,这引发了一个思考:线性表在数据结构中究竟处于什么地位?
在我看来,线性表如同数据结构体系中的"基础构件" 。后续学习的栈、队列、树、图等复杂数据结构,大多是在顺序表的逻辑结构基础上,通过施加不同的操作约束和扩展存储关系而形成的。比如:
- 栈是操作受限(LIFO)的线性表
- 队列是操作受限(FIFO)的线性表
- 多维数组可视为线性表的扩展
- 矩阵是带有特殊关系的线性表
因此,掌握线性表是理解更复杂数据结构的基石。本篇将首先讲解物理结构最简单的线性表——顺序表,从三个关键维度展开:
- 逻辑结构:数据元素之间的线性关系
- 存储结构:数据在内存中的物理存储方式
- 基本操作:对数据的主要运算实现
二、顺序表的逻辑结构
顺序表的逻辑结构体现了最简单、最直观的数据组织方式——线性关系。
2.1. 逻辑结构的核心特征
1.元素间的"一对一"关系
- 每个元素有且仅有一个直接前驱(首元素除外)
- 每个元素有且仅有一个直接后继(尾元素除外)
2. 确定的先后次序
- 每个元素有且仅有一个直接前驱(首元素除外)
- 每个元素有且仅有一个直接后继(尾元素除外)
- 有限的元素集合
4.元素都是数据元素,每个元素都是单个元素
上述所提及的逻辑结构特点并不只是顺序表特有的的,链表也同样具有。简言之,这是线性结构所具有的特点。
三、顺序表的物理结构
线性表的两种基本实现——顺序表和链表,其根本区别在于物理存储结构的不同。这种存储差异直接决定了它们各自的操作特性和算法适用性。顺序表的连续存储特性支持随机访问,使其适合快速排序、折半查找等高效算法;而链表的离散存储特性虽然牺牲了随机访问能力,但在动态插入删除方面具有天然优势。
顺序表的存储方式是内存开辟一整块连续的空间进行存储,其特点是存储密度高,支持随机存取。但同时一次性开辟一整块连续空间灵活性交较差、分配空间不方便、改变容量不方便。
四、顺序表的基本操作
顺序表的操作主要就创建、销毁、增、删、改、查。根据创建选择的分配方式不同,又可以分为静态分配和动态分配。
4.1 静态分配创建顺序表
静态分配的核心特点:
- 固定大小:
MAX_SIZE在编译时确定 - 连续存储:元素在内存中连续存放
- 空间预分配:无论是否使用,都占用固定内存
适用场景
- 数据量固定或变化不大的情况
- 频繁查找,较少插入删除的场景
- 内存充足,对存储效率要求高的应用
#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE 100 // 定义顺序表的最大容量
// 定义顺序表结构体
typedef struct {
int data[MAX_SIZE]; // 静态数组存储数据元素
int length; // 当前顺序表的长度
} SqList;
// 初始化顺序表
void InitList(SqList *L) {
L->length = 0; // 初始长度为0
printf("顺序表初始化成功!\n");
}
// 判断顺序表是否为空
bool ListEmpty(SqList L) {
return L.length == 0;
}
// 判断顺序表是否已满
bool ListFull(SqList L) {
return L.length >= MAX_SIZE;
}
// 获取顺序表长度
int ListLength(SqList L) {
return L.length;
}
// 按位置插入元素
bool ListInsert(SqList *L, int i, int e) {
// 判断插入位置是否合法
if (i < 1 || i > L->length + 1) {
printf("插入位置不合法!\n");
return false;
}
// 判断顺序表是否已满
if (ListFull(*L)) {
printf("顺序表已满,无法插入!\n");
return false;
}
// 将第i个位置及之后的元素后移
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
// 插入新元素
L->data[i - 1] = e; // 注意:数组下标从0开始,位序从1开始
L->length++;
printf("元素 %d 插入成功!\n", e);
return true;
}
// 按位置删除元素
bool ListDelete(SqList *L, int i, int *e) {
// 判断删除位置是否合法
if (i < 1 || i > L->length) {
printf("删除位置不合法!\n");
return false;
}
// 判断顺序表是否为空
if (ListEmpty(*L)) {
printf("顺序表为空,无法删除!\n");
return false;
}
// 保存被删除的元素
*e = L->data[i - 1];
// 将第i个位置之后的元素前移
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
printf("元素 %d 删除成功!\n", *e);
return true;
}
// 按位置查找元素
bool GetElem(SqList L, int i, int *e) {
if (i < 1 || i > L.length) {
printf("查找位置不合法!\n");
return false;
}
*e = L.data[i - 1];
printf("第 %d 个元素是:%d\n", i, *e);
return true;
}
// 按值查找元素位置
int LocateElem(SqList L, int e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
printf("元素 %d 位于第 %d 个位置\n", e, i + 1);
return i + 1;
}
}
printf("未找到元素 %d\n", e);
return -1;
}
// 遍历输出顺序表
void PrintList(SqList L) {
if (ListEmpty(L)) {
printf("顺序表为空!\n");
return;
}
printf("顺序表元素:");
for (int i = 0; i < L.length; i++) {
printf("%d ", L.data[i]);
}
printf("\n");
}
// 清空顺序表
void ClearList(SqList *L) {
L->length = 0;
printf("顺序表已清空!\n");
}
int main() {
SqList L; // 声明顺序表变量
int deletedElem;
// 初始化顺序表
InitList(&L);
// 插入元素测试
ListInsert(&L, 1, 10); // 在第1个位置插入10
ListInsert(&L, 2, 20); // 在第2个位置插入20
ListInsert(&L, 3, 30); // 在第3个位置插入30
ListInsert(&L, 2, 15); // 在第2个位置插入15
// 遍历输出
PrintList(L);
// 查找操作测试
GetElem(L, 2, &deletedElem); // 查找第2个元素
LocateElem(L, 20); // 查找值为20的元素位置
// 删除操作测试
ListDelete(&L, 2, &deletedElem); // 删除第2个元素
PrintList(L);
// 获取长度
printf("当前顺序表长度:%d\n", ListLength(L));
return 0;
}
4.2 动态分配创建顺序表
相较于静态分配,动态分配具有以下优势:
- 灵活性:根据实际数据量调整内存
- 内存效率:避免预分配过多内存的浪费
- 适应性:适合数据量变化大的场景
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define INIT_SIZE 10 // 初始容量
// 定义动态顺序表结构体
typedef struct {
int *data; // 动态数组指针
int length; // 当前长度
int maxSize; // 当前最大容量
} SeqList;
// 初始化顺序表
void InitList(SeqList *L) {
L->data = (int*)malloc(INIT_SIZE * sizeof(int));
if (L->data == NULL) {
printf("内存分配失败!\n");
exit(1);
}
L->length = 0;
L->maxSize = INIT_SIZE;
printf("顺序表初始化成功!初始容量:%d\n", INIT_SIZE);
}
// 扩展顺序表容量
void ExpandList(SeqList *L, int newSize) {
int *newData = (int*)realloc(L->data, newSize * sizeof(int));
if (newData == NULL) {
printf("内存扩展失败!\n");
return;
}
L->data = newData;
L->maxSize = newSize;
printf("顺序表容量扩展为:%d\n", newSize);
}
// 自动扩容(当空间不足时)
void AutoExpand(SeqList *L) {
if (L->length >= L->maxSize) {
int newSize = L->maxSize * 2; // 容量翻倍
ExpandList(L, newSize);
}
}
// 判断顺序表是否为空
bool ListEmpty(SeqList L) {
return L.length == 0;
}
// 获取顺序表长度
int ListLength(SeqList L) {
return L.length;
}
// 获取当前容量
int GetCapacity(SeqList L) {
return L.maxSize;
}
// 按位置插入元素
bool ListInsert(SeqList *L, int i, int e) {
// 检查插入位置合法性
if (i < 1 || i > L->length + 1) {
printf("插入位置不合法!当前长度:%d\n", L->length);
return false;
}
// 自动扩容检查
AutoExpand(L);
// 移动元素
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j - 1];
}
// 插入新元素
L->data[i - 1] = e;
L->length++;
printf("元素 %d 插入成功!当前长度:%d,容量:%d\n", e, L->length, L->maxSize);
return true;
}
// 按位置删除元素
bool ListDelete(SeqList *L, int i, int *e) {
if (i < 1 || i > L->length) {
printf("删除位置不合法!\n");
return false;
}
if (ListEmpty(*L)) {
printf("顺序表为空,无法删除!\n");
return false;
}
// 保存被删除元素
*e = L->data[i - 1];
// 移动元素
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
printf("元素 %d 删除成功!当前长度:%d\n", *e, L->length);
// 缩容检查(可选:当长度远小于容量时)
if (L->length < L->maxSize / 4 && L->maxSize > INIT_SIZE) {
int newSize = L->maxSize / 2;
ExpandList(L, newSize);
}
return true;
}
// 按位置查找元素
bool GetElem(SeqList L, int i, int *e) {
if (i < 1 || i > L.length) {
printf("查找位置不合法!\n");
return false;
}
*e = L.data[i - 1];
printf("第 %d 个元素是:%d\n", i, *e);
return true;
}
// 按值查找元素位置
int LocateElem(SeqList L, int e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
printf("元素 %d 位于第 %d 个位置\n", e, i + 1);
return i + 1;
}
}
printf("未找到元素 %d\n", e);
return -1;
}
// 遍历输出顺序表
void PrintList(SeqList L) {
if (ListEmpty(L)) {
printf("顺序表为空!\n");
return;
}
printf("顺序表元素[长度=%d, 容量=%d]:", L.length, L.maxSize);
for (int i = 0; i < L.length; i++) {
printf("%d ", L.data[i]);
}
printf("\n");
}
// 销毁顺序表(释放内存)
void DestroyList(SeqList *L) {
free(L->data);
L->data = NULL;
L->length = 0;
L->maxSize = 0;
printf("顺序表已销毁,内存已释放!\n");
}
// 清空顺序表(保留内存)
void ClearList(SeqList *L) {
L->length = 0;
printf("顺序表已清空!当前容量:%d\n", L->maxSize);
}
int main() {
SeqList L;
int deletedElem;
// 初始化顺序表
InitList(&L);
// 测试插入操作(触发自动扩容)
printf("\n=== 测试插入操作 ===\n");
for (int i = 1; i <= 15; i++) {
ListInsert(&L, i, i * 10);
}
PrintList(L);
// 测试查找操作
printf("\n=== 测试查找操作 ===\n");
GetElem(L, 5, &deletedElem);
LocateElem(L, 80);
// 测试删除操作
printf("\n=== 测试删除操作 ===\n");
ListDelete(&L, 3, &deletedElem);
ListDelete(&L, 7, &deletedElem);
PrintList(L);
// 测试手动扩容
printf("\n=== 测试手动扩容 ===\n");
ExpandList(&L, 30);
PrintList(L);
// 测试清空和销毁
printf("\n=== 测试清空操作 ===\n");
ClearList(&L);
PrintList(L);
// 重新插入测试
ListInsert(&L, 1, 999);
ListInsert(&L, 2, 888);
PrintList(L);
// 最终销毁
DestroyList(&L);
return 0;
}
五、总结
相较链表来说,顺序表是非常容易能实现的。对这部分内容的学习还是要动手写代码才能理解的更深刻。