1. 顺序表的基本概念与特点
顺序表是数据结构中最基础、最常用的线性存储结构之一。它采用一组地址连续的存储单元依次存储数据元素,通过物理位置上的相邻关系来体现数据元素之间的逻辑关系。这种存储方式使得顺序表具有随机访问的特性,我们可以直接通过下标访问任意位置的元素,时间复杂度为O(1)。
在实际应用中,顺序表通常使用数组来实现。以C语言为例,一个整型顺序表可以定义为:
c复制#define MAXSIZE 100 // 顺序表的最大容量
typedef struct {
int data[MAXSIZE]; // 存储数据元素的数组
int length; // 当前顺序表的长度
} SeqList;
顺序表的核心特点包括:
- 物理连续:所有元素存储在内存中的连续区域
- 随机访问:通过下标可直接访问任意元素
- 预分配空间:需要预先确定存储空间大小
- 插入删除效率低:平均需要移动n/2个元素
提示:虽然顺序表的插入删除操作效率不高,但在需要频繁随机访问元素的场景中,它仍然是首选的数据结构。
2. 顺序表的基本操作实现
2.1 初始化与销毁
顺序表的初始化需要为其分配存储空间并设置初始长度。以下是典型的初始化实现:
c复制void InitList(SeqList *L) {
L->length = 0; // 初始长度为0
// 如果需要动态分配:
// L->data = (int *)malloc(MAXSIZE * sizeof(int));
}
销毁操作主要是释放动态分配的内存(如果是静态数组则无需特别处理):
c复制void DestroyList(SeqList *L) {
if (L->data != NULL) {
free(L->data);
L->data = NULL;
L->length = 0;
}
}
2.2 插入操作
在顺序表的第i个位置插入新元素e,需要考虑以下情况:
- 表是否已满
- 插入位置是否合法
- 移动后续元素
c复制int ListInsert(SeqList *L, int i, int e) {
if (L->length >= MAXSIZE) return 0; // 表满
if (i < 1 || i > L->length + 1) return 0; // 位置不合法
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j-1]; // 元素后移
}
L->data[i-1] = e;
L->length++;
return 1;
}
时间复杂度分析:
- 最好情况:在表尾插入,O(1)
- 最坏情况:在表头插入,O(n)
- 平均情况:O(n)
2.3 删除操作
删除第i个位置的元素,同样需要考虑位置合法性:
c复制int ListDelete(SeqList *L, int i, int *e) {
if (L->length == 0) return 0; // 表空
if (i < 1 || i > L->length) return 0; // 位置不合法
*e = L->data[i-1];
for (int j = i; j < L->length; j++) {
L->data[j-1] = L->data[j]; // 元素前移
}
L->length--;
return 1;
}
时间复杂度与插入操作相同,都是O(n)。
3. 顺序表的动态扩容策略
静态顺序表的最大缺点是容量固定,而动态顺序表通过扩容机制解决了这个问题。以下是典型的扩容实现:
c复制#define INIT_SIZE 10
#define INCREMENT 5
typedef struct {
int *data; // 动态分配数组
int length; // 当前长度
int capacity; // 当前容量
} DynSeqList;
int InitDynList(DynSeqList *L) {
L->data = (int *)malloc(INIT_SIZE * sizeof(int));
if (!L->data) return 0;
L->length = 0;
L->capacity = INIT_SIZE;
return 1;
}
int DynListInsert(DynSeqList *L, int i, int e) {
if (i < 1 || i > L->length + 1) return 0;
// 需要扩容
if (L->length >= L->capacity) {
int *newbase = (int *)realloc(L->data,
(L->capacity + INCREMENT) * sizeof(int));
if (!newbase) return 0;
L->data = newbase;
L->capacity += INCREMENT;
}
// 插入操作与静态表相同
for (int j = L->length; j >= i; j--) {
L->data[j] = L->data[j-1];
}
L->data[i-1] = e;
L->length++;
return 1;
}
扩容策略的选择直接影响性能:
- 固定增量:每次增加固定数量(如上述代码)
- 倍数扩容:每次容量翻倍(Java ArrayList采用1.5倍)
- 自适应策略:根据历史使用情况动态调整
实际工程中,倍数扩容(如1.5或2倍)通常能更好地平衡空间和时间效率。
4. 顺序表的应用场景与优化
4.1 典型应用场景
顺序表特别适合以下场景:
- 频繁随机访问:如数据库索引、缓存系统
- 数据量相对固定:如学生成绩表、员工信息表
- 内存受限环境:连续存储减少内存碎片
- CPU缓存友好:局部性原理带来性能优势
4.2 性能优化技巧
- 批量操作:一次性插入/删除多个元素,减少移动次数
c复制void BatchInsert(SeqList *L, int pos, int *elems, int n) {
// 先移动出足够空间
for (int i = L->length-1; i >= pos; i--) {
L->data[i+n] = L->data[i];
}
// 批量插入新元素
for (int j = 0; j < n; j++) {
L->data[pos+j] = elems[j];
}
L->length += n;
}
- 预留空间:根据业务特点预先分配足够空间
- 交换替代移动:对无序表,删除时可用末尾元素填充
- 标记删除:先标记待删除元素,定期批量清理
4.3 顺序表与链表的对比
| 特性 | 顺序表 | 链表 |
|---|---|---|
| 存储方式 | 连续内存 | 离散节点 |
| 访问效率 | O(1)随机访问 | O(n)顺序访问 |
| 插入删除 | O(n) | O(1)(已知位置) |
| 空间开销 | 预分配可能浪费 | 每个节点额外指针空间 |
| 缓存友好性 | 好 | 差 |
| 适用场景 | 静态数据、频繁访问 | 动态数据、频繁修改 |
在实际项目中,我经常遇到这样的选择:当数据量不大(如<1000)且访问频繁时,顺序表通常是更好的选择;而当数据量很大或需要频繁插入删除时,链表类结构更合适。
