顺序表作为数据结构中最基础的线性存储方式,本质上就是用一组地址连续的存储单元依次存储数据元素。这种结构就像电影院里的座位排列——每个座位(元素)都有固定的编号(地址),观众(数据)按照票号对号入座。
在Day1的学习中我们已经了解到,顺序表的核心特征包括:
今天我们将深入探讨顺序表的实现细节和典型应用场景。先来看一个最简单的整型顺序表结构体定义(C语言实现):
c复制#define MAXSIZE 100 // 最大容量
typedef struct {
int data[MAXSIZE]; // 存储数组
int length; // 当前长度
} SqList;
关键理解:顺序表的"顺序"体现在两个方面——逻辑上相邻的元素在物理存储上也相邻;元素间的顺序关系由存储位置决定而非指针链接。
初始化顺序表时需要明确两个关键参数:
c复制// 初始化
Status InitList(SqList *L) {
L->length = 0;
return OK;
}
// 销毁(静态分配无需特殊操作)
void DestroyList(SqList *L) {
L->length = 0; // 逻辑清空
}
实际工程中更常用动态分配方式,此时销毁需要释放内存:
c复制free(L->data); L->data = NULL;
插入元素时需要处理三种典型情况,时间复杂度各不相同:
| 插入位置 | 移动元素数量 | 时间复杂度 |
|---|---|---|
| 表头 | n | O(n) |
| 表尾 | 0 | O(1) |
| 中间位置i | n-i | O(n) |
具体实现代码示例:
c复制Status ListInsert(SqList *L, int i, ElemType e) {
// 边界检查
if (i<1 || i>L->length+1) return ERROR;
if (L->length >= MAXSIZE) return ERROR;
// 元素后移
for(int j=L->length; j>=i; j--) {
L->data[j] = L->data[j-1];
}
// 插入新元素
L->data[i-1] = e;
L->length++;
return OK;
}
删除操作同样需要移动元素,但可以通过延迟移动策略优化:
c复制Status ListDelete(SqList *L, int i, ElemType *e) {
if (i<1 || i>L->length) return ERROR;
*e = L->data[i-1]; // 返回被删除元素
// 前移元素
for(int j=i; j<L->length; j++) {
L->data[j-1] = L->data[j];
}
L->length--;
return OK;
}
工程技巧:批量删除时可先标记要删除的元素,最后统一移动,减少操作次数。
静态数组实现的顺序表有容量限制,实际工程中更多使用动态扩容方案:
c复制#define INIT_SIZE 10
#define INCREMENT 5
typedef struct {
ElemType *elem; // 动态数组指针
int length; // 当前长度
int capacity; // 当前容量
} DynSeqList;
Status InitList(DynSeqList *L) {
L->elem = (ElemType*)malloc(INIT_SIZE*sizeof(ElemType));
if(!L->elem) exit(OVERFLOW);
L->length = 0;
L->capacity = INIT_SIZE;
return OK;
}
Status ListInsert(DynSeqList *L, int i, ElemType e) {
// 需要扩容的情况
if (L->length >= L->capacity) {
ElemType *newbase = (ElemType*)realloc(L->elem,
(L->capacity + INCREMENT)*sizeof(ElemType));
if (!newbase) exit(OVERFLOW);
L->elem = newbase;
L->capacity += INCREMENT;
}
// ... 后续插入逻辑相同
}
合并两个有序顺序表是经典面试题,核心是双指针法:
c复制void MergeList(SqList La, SqList Lb, SqList *Lc) {
int i=0, j=0, k=0;
while (i<La.length && j<Lb.length) {
if (La.data[i] <= Lb.data[j])
Lc->data[k++] = La.data[i++];
else
Lc->data[k++] = Lb.data[j++];
}
// 处理剩余元素
while (i<La.length) Lc->data[k++] = La.data[i++];
while (j<Lb.length) Lc->data[k++] = Lb.data[j++];
Lc->length = k;
}
时间复杂度分析:O(m+n),其中m和n分别是两个表的长度。
现代CPU对内存访问有对齐要求,结构体定义时应考虑:
c复制typedef struct {
int data[MAXSIZE];
int length;
char _reserved[4]; // 填充字节保证对齐
} AlignedSqList;
顺序表的局部性原理使其天然具有缓存优势,但使用时仍需注意:
健壮的顺序表实现需要完善的错误检测:
c复制typedef enum {
OK,
ERROR,
OVERFLOW,
UNDERFLOW,
INVALID_INDEX
} Status;
Status ListInsert(SqList *L, int i, ElemType e) {
if (L == NULL) return INVALID_PTR;
if (i < 1 || i > L->length+1) return INVALID_INDEX;
// ...
}
| 操作 | 顺序表 | 链表 |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 头部插入 | O(n) | O(1) |
| 尾部插入 | O(1) | O(1) |
| 中间插入 | O(n) | O(1) |
| 空间利用率 | 高 | 低 |
| 内存连续性 | 连续 | 分散 |
症状:程序崩溃或数据异常
排查步骤:
动态顺序表特有的问题:
检测工具:
当顺序表操作变慢时检查:
cpp复制template<typename T>
class Vector {
private:
T* _data;
size_t _size;
size_t _capacity;
void resize(size_t new_cap) {
T* new_data = new T[new_cap];
std::copy(_data, _data+_size, new_data);
delete[] _data;
_data = new_data;
_capacity = new_cap;
}
public:
void push_back(const T& value) {
if (_size >= _capacity) {
resize(_capacity == 0 ? 1 : _capacity * 2);
}
_data[_size++] = value;
}
// ... 其他接口
};
典型问题:移除有序数组中的重复元素
python复制def removeDuplicates(nums):
if not nums: return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
最大连续子数组和问题:
java复制public int maxSubArray(int[] nums) {
int maxSum = nums[0];
int currentSum = nums[0];
for (int i = 1; i < nums.length; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
矩阵旋转问题(要求O(1)空间):
c复制void rotate(int** matrix, int n) {
// 对角线翻转
for(int i=0; i<n; i++) {
for(int j=i+1; j<n; j++) {
swap(&matrix[i][j], &matrix[j][i]);
}
}
// 水平翻转
for(int i=0; i<n; i++) {
for(int j=0; j<n/2; j++) {
swap(&matrix[i][j], &matrix[i][n-1-j]);
}
}
}
测试工具:Google Benchmark
测试数据:随机生成的100万条记录
测试指标:操作耗时、缓存命中率、内存占用
| 操作 | 顺序表 | 链表 |
|---|---|---|
| 随机访问 | 3.2 | 420.5 |
| 头部插入 | 580.3 | 12.1 |
| 尾部插入 | 5.8 | 15.3 |
| 遍历求和 | 120.4 | 380.7 |
顺序表的内存优势主要体现在: