顺序表作为数据结构中最基础的存储结构之一,其核心思想是用一段连续的内存空间来存储线性表的元素。这种存储方式与我们日常生活中使用的数组非常相似,但比单纯的数组多了一个关键属性——有效长度计数器。
顺序表主要由两部分组成:
这种设计带来了几个重要特性:
c复制#define MAXSIZE 100 // 定义顺序表的最大容量
typedef int ElemType; // 定义元素类型
typedef struct {
ElemType data[MAXSIZE]; // 存储数据的数组
int length; // 当前有效元素个数
} SeqList;
在64位系统中,上述结构体占用的内存大小为404字节(100个int型元素×4字节 + 1个int型length变量×4字节)。这种固定大小的设计虽然简单,但也带来了容量限制的问题。
顺序表有几个关键特性需要特别注意:
逻辑位置与物理位置的映射:
有效长度的作用:
操作的时间复杂度:
提示:在实际应用中,如果查找操作频繁,可以考虑先对顺序表排序,然后使用二分查找将查找时间复杂度降为O(logn)。
初始化是使用顺序表的第一步,其核心是将有效长度置为0,表示当前是一个空表。
c复制void initList(SeqList *L) {
L->length = 0; // 将有效长度置0
}
这里需要注意几点:
在顺序表尾部追加元素是最简单的插入操作,不需要移动现有元素。
c复制int appendElem(SeqList *L, ElemType e) {
if (L->length >= MAXSIZE) {
printf("顺序表已满\n");
return 0; // 插入失败
}
L->data[L->length] = e; // 在末尾插入新元素
L->length++; // 长度增加
return 1; // 插入成功
}
这个操作的时间复杂度是O(1),因为它只涉及一次赋值和一次长度增加。但需要注意先检查顺序表是否已满,避免数组越界。
遍历顺序表就是依次访问并处理所有有效元素。
c复制void listElem(SeqList *L) {
for (int i = 0; i < L->length; i++) {
printf("%d ", L->data[i]);
}
printf("\n");
}
关键点:
在顺序表中间插入元素需要先移动后续元素腾出空间,然后再插入新元素。
c复制int insertElem(SeqList *L, int pos, ElemType e) {
// 边界检查
if (L->length >= MAXSIZE) {
printf("表已满\n");
return 0;
}
if (pos < 1 || pos > L->length + 1) {
printf("插入位置错误\n");
return 0;
}
// 元素后移
for (int i = L->length - 1; i >= pos - 1; i--) {
L->data[i + 1] = L->data[i];
}
// 插入新元素
L->data[pos - 1] = e;
L->length++;
return 1;
}
注意事项:
删除操作需要先保存被删除元素,然后移动后续元素填补空缺。
c复制int deleteElem(SeqList *L, int pos, ElemType *e) {
if (L->length == 0) {
printf("空表\n");
return 0;
}
if (pos < 1 || pos > L->length) {
printf("删除位置错误\n");
return 0;
}
*e = L->data[pos - 1]; // 保存被删除元素
// 元素前移
for (int i = pos; i < L->length; i++) {
L->data[i - 1] = L->data[i];
}
L->length--;
return 1;
}
关键点:
顺序表的查找是线性查找,需要遍历所有元素。
c复制int findElem(SeqList *L, ElemType e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1; // 返回逻辑位置
}
}
return 0; // 未找到
}
查找操作的优化空间:
完整的测试程序应该覆盖所有基本操作,并验证边界条件。
c复制int main() {
SeqList list;
initList(&list);
printf("初始化后长度: %d\n", list.length);
// 测试追加
appendElem(&list, 10);
appendElem(&list, 20);
appendElem(&list, 30);
printf("追加后: ");
listElem(&list);
// 测试插入
insertElem(&list, 2, 15);
printf("插入后: ");
listElem(&list);
// 测试删除
ElemType deleted;
deleteElem(&list, 3, &deleted);
printf("删除的元素: %d\n", deleted);
printf("删除后: ");
listElem(&list);
// 测试查找
int pos = findElem(&list, 20);
printf("元素20的位置: %d\n", pos);
return 0;
}
在实际编码过程中,经常会遇到以下问题:
数组越界访问:
长度维护错误:
指针使用错误:
元素移动方向错误:
顺序表各种操作的时间复杂度如下:
| 操作 | 最好情况 | 最坏情况 | 平均情况 |
|---|---|---|---|
| 访问 | O(1) | O(1) | O(1) |
| 查找 | O(1) | O(n) | O(n) |
| 插入 | O(1) | O(n) | O(n) |
| 删除 | O(1) | O(n) | O(n) |
顺序表的空间复杂度是O(n),其中n是MAXSIZE。实际使用的空间是n×sizeof(ElemType) + sizeof(int)。
虽然顺序表的基本性能由其连续存储特性决定,但仍有一些优化空间:
批量操作优化:
缓存友好性:
预分配策略:
特殊场景优化:
顺序表最适合以下场景:
顺序表不适用于:
掌握了静态顺序表后,可以进一步学习:
动态顺序表:
多维顺序表:
特殊顺序表:
与其他结构结合:
在实际工程实践中,顺序表虽然简单,但仍然是许多复杂数据结构的基础。理解其核心原理和实现细节,对于后续学习更高级的数据结构至关重要。建议读者不仅要理解代码实现,还要通过绘图、调试等方式深入理解内存布局和操作过程。