目录
引言
生活中其实有很多的数据结构的影子
-
停车场的车位->顺序表
-
超市货架上的“紧密排列商品”->顺序表
-
教室里的固定座位->顺序表
数据结构的必要性
-
数据需要“容器”来组织
-
不同的操作需要不同的容器
-
性能优化的基石
学习路线图
┌─────────────┐
│ 基础概念 │
│ (是什么) │
└──────┬──────┘
↓
┌─────────────┐
│ 实现原理 │
│ (怎么做) │
└──────┬──────┘
↓
┌─────────────┐
│ 对比选择 │
│ (什么时候用) │
└──────┬──────┘
↓
┌─────────────┐
│ 实战应用 │
│ (用在哪) │
└─────────────┘
什么是顺序表
顺序表是一种线性表的顺序存储结构,是数据结构中最基础的线性结构之一。它的核心特征是用一段连续的物理内存空间,依次存储线性表中的元素,使得元素之间的逻辑顺序与物理存储顺序完全一致。
顺序表的核心定义与结构
-
物理存储:元素在内存中连续排列,相邻元素的物理地址差固定(等于单个元素的大小)。
-
逻辑结构:元素之间是 “一对一” 的线性关系(除首元素外,每个元素有唯一前驱;除尾元素外,每个元素有唯一后继)。
-
访问方式:支持随机访问(通过元素的位置索引,直接计算出内存地址,时间复杂度
O(1))。
顺序表的优缺点总结
优点:
-
随机访问高效:通过索引直接定位元素,适合频繁访问元素的场景。
-
存储密度高:无需额外空间存储元素间的逻辑关系(如链表的指针),空间利用率高。
-
实现简单:基于数组实现,逻辑清晰,容易理解和编码。
-
CPU缓存命中率高
CPU缓存命中率:
- 在计算机中存在的地址总线(Address Bus)、控制总线(Control Bus)、数据总线(Data Bus) —— 三者合称 “系统总线”,是 CPU 与内存、I/O 设备(如硬盘、显卡)之间传输信息的 “三条核心通道”,共同支撑计算机的基本运算。
缓存的设计核心是平衡 CPU 运算速度(GHz 级)与主存访问速度(百 ns 级)的巨大差距(两者速度差约 100 倍):当 CPU 需要数据时,会优先从缓存读取(耗时 <10ns);若缓存中没有目标数据(称为 “缓存缺失”),才通过系统总线从主存加载数据(耗时约 100ns),外存(硬盘 / SSD)的数据需先加载到主存,再由主存传入缓存,缓存不直接与外存交互。
缓存的 “批量加载” 机制:缓存行与突发传输
CPU 缓存的最小存储单元是 64 字节的缓存行(现代主流 CPU 标准,与 32/64 位数据总线宽度无关):
数据总线宽度(32 位 = 4 字节、64 位 = 8 字节)仅决定 CPU 与主存单次总线传输的位数,而缓存加载数据时会采用 “突发传输”(Burst Transfer),一次性从主存加载整个缓存行(64 字节)到缓存中,而非仅传输数据总线宽度的少量数据。
例如对于
char arr[10](char 占 1 字节,数组在内存中连续存储),当 CPU 首次访问arr[0]时,缓存会加载arr[0]所在的 64 字节缓存行(包含arr[0]~arr[9]及后续相邻内存数据,若数组后无足够连续内存则加载至对齐边界)。
顺序表的连续存储特性完美匹配缓存的 “空间局部性” 原理 —— 当访问内存中某个地址时,其相邻地址的数据极可能在近期被访问(如数组遍历、顺序读取结构体字段):
首次访问
arr[0]:缓存缺失,CPU 通过地址总线发送arr[0]的内存地址,控制总线发送 “读命令”,主存将目标缓存行通过数据总线传输到缓存;后续访问
arr[1]~arr[9]:数据已提前加载到缓存中,直接从缓存读取(无需再次访问主存),即 “缓存命中”,此时缓存命中率接近 100%。
// 物理存储 vs 逻辑结构
物理地址:0x1000 0x1004 0x1008 0x100C 0x1010
逻辑索引:[0] [1] [2] [3] [4]
元素值: 10 20 30 40 50
// 为什么连续存储快?
1. CPU缓存预取机制 - 一次加载整块内存
2. 地址计算简单 - base_address + index * element_size
3. 空间局部性 - 相邻元素很可能被连续访问
缺点:
-
插入 / 删除效率低:当在中间位置操作时,需移动大量后续元素,时间复杂度
O(n)。 -
动态扩容开销大:扩容时需重新分配内存并拷贝原有元素,可能导致性能波动。
-
连续空间限制:需要大片连续的物理内存,当数据量较大时,可能无法分配足够空间。
顺序表的两种实现方式
顺序表的实现就只有静态的和动态的之分
// 静态实现
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int length;
} StaticList;
// 动态实现(重点)
typedef struct {
int *data; // 指向堆内存
int capacity; // 当前容量
int size; // 当前大小
} DynamicList;
顺序表的静态版本
存储空间固定后续无法再增加存储空间,容易造成给少了不够,给多了浪费的情况
顺序表的动态版本
存储大小按需分配,存储空间不够可以在自动扩容,但是一次扩容的大小一般位1.5倍或者2倍,也容易导致空间浪费的情况,反复扩容存储空间影响性能
顺序表的实现方式
1. 静态分配(固定大小)
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE]; // 静态数组
int length; // 当前长度
} StaticSeqList;
2. 动态分配(可扩容)
typedef struct {
int *data; // 指向动态分配数组的指针
int maxSize; // 最大容量
int length; // 当前长度
} DynamicSeqList;
顺序表的基本操作及代码实现
1. 初始化
// 动态顺序表初始化
void InitSeqList(DynamicSeqList *L, int initSize) {
L->data = (int *)malloc(initSize * sizeof(int));
if (!L->data)
{
perror("malloc fail");
exit(-1);// 内存分配失败
}
L->maxSize = initSize;
L->length = 0;
}
2. 插入操作
// 在位置i插入元素e
int InsertSeqList(DynamicSeqList *L, int i, int e) {
// 参数检查
assert(i >=1 1 && i <= L->length + 1);
//条件为假就执行,这里的i是下标但是为了迎合人类的习惯,所以这里用i = 1,来代表0下标
if (L->length >= L->maxSize)
{
// 扩容:通常扩容为原来的2倍
int newSize = L->maxSize * 2;
int *newData = (int *)realloc(L->data, newSize * sizeof(int));
if (!newData)
{
perror("realloc fail");
exit(-1);// 扩容失败
{
L->data = newData;
L->maxSize = newSize;
}
// 将第i个位置及之后的元素后移
for (int j = L->length; j >= i; j--)
{
L->data[j] = L->data[j - 1];
}
// 插入新元素
L->data[i - 1] = e;
L->length++;
return 1;
}
3. 删除操作
// 删除位置i的元素,并用tmp返回其值
int DeleteSeqList(DynamicSeqList *L, int i, int *e) {
assert(i >= 1 && i <= L->length);
*tmp = L->data[i - 1]; // 保存被删除元素的值
// 将第i个位置之后的元素前移
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
L->length--;
return tmp;
}
4. 查找操作
// 按值查找(顺序查找)
int LocateElem(DynamicSeqList *L, int e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1; // 返回位置(从1开始)
}
}
return 0; // 未找到
}
// 按位置查找
int GetElem(DynamicSeqList *L, int i, int *e) {
assert(i >= 1 && i <= L->length);
*tmp = L->data[i - 1];
return tmp;
}
5. 遍历操作
void TraverseSeqList(DynamicSeqList *L) {
printf("顺序表内容(长度=%d,容量=%d):\n", L->length, L->maxSize);
for (int i = 0; i < L->length; i++)
{
printf("data[%d] = %d\n", i, L->data[i]);
}
}
6. 销毁操作
void DestroySeqList(DynamicSeqList *L) {
free(L->data);
L->data = NULL;
L->length = 0;
L->maxSize = 0;
}
时间复杂度分析
| 操作 | 最好情况 | 最坏情况 | 平均情况 | 说明 |
|---|---|---|---|---|
| 访问元素 | O(1) | O(1) | O(1) | 直接通过下标访问 |
| 插入元素 | O(1) | O(n) | O(n) | 尾部插入为O(1),头部/中间插入需要移动元素 |
| 删除元素 | O(1) | O(n) | O(n) | 尾部删除为O(1),头部/中间删除需要移动元素 |
| 按值查找 | O(1) | O(n) | O(n) | 目标在开头/末尾区别 |
| 按位置查找 | O(1) | O(1) | O(1) | 直接通过下标访问 |
应用场景
适合使用顺序表的场景:
-
频繁访问、很少增删:如学生成绩表、员工信息表等
-
元素数量可预估:如月份(12个)、星期(7个)等
-
需要高效随机访问:如二分查找、快速排序等算法的基础结构
不适合使用顺序表的场景:
-
频繁插入删除:如实时交易系统、聊天记录等
-
元素数量变化大:如社交网络的好友列表
-
内存空间有限:嵌入式系统等
总结
顺序表是最基础、最常用的线性存储结构,它的核心特点是物理位置连续。虽然它在插入删除操作上效率不高,但其随机访问的特性使得它在许多场景下仍具有不可替代的优势。理解顺序表是学习其他更复杂数据结构(如栈、队列、字符串等)的基础。在实际应用中,应根据具体需求选择使用顺序表还是链表。
909

被折叠的 条评论
为什么被折叠?



