顺序表的引入
顺序表有着连续且有限的存储空间,像一个单排的储物盒,一个接着一个的存储单元,每个空间元素在逻辑上具有唯一前驱和后继(首元素无前驱,末元素无后继),如下图。
存储特性:逻辑相邻则物理相邻。
以第一个存储空间的地址为索引向下连续申请至第n个存储空间。那么下面就让我们进入顺序表的学习吧~
顺序表是一种线性表的存储结构,采用连续的内存空间存储元素,通过物理位置的相邻关系表示逻辑上的顺序关系。其核心特点是支持随机访问,但插入和删除操作可能需要移动大量元素(链表更合适)。
描述顺序表的物理存储方式(所占内存的位置连续),通常使用数组实现。讨论静态分配(最好在堆申请空间)和动态分配两种内存管理策略。
静态顺序表:使用固定长度的数组存储数据,容量不可变。
动态顺序表:通过动态分配内存实现容量扩展。
操作复杂度
1.随机访问
通过下标可直接访问元素(按位查找),时间复杂度为 O(1)O(1)O(1)。
2. 存储密度高
仅存储数据元素,无需额外空间维护逻辑关系。
3. 插入/删除效率低
平均时间复杂度为 O(n)O(n)O(n),需移动大量元素
优缺点
优点:
内存连续,空间利用率高。
支持快速随机访问,实现简单。
缺点:
插入/删除效率低,灵活性不足。
栈和堆的区别
-栈:生命周期短;对于空间的利用不够高效;申请空间更有限,易越界(有次写洛谷题需要申请一个很大的空间,提交后测试点没有全部通过,结果将内存的申请放main外就过了…)
-堆:表头一直存在,free之前可重复调用,生命周期长。
建议:大空间放堆上,栈容易溢出。
顺序表简式模型:(方便与后面链表区分)
1.定义表头
//定义数据空间存储类型
typedef int Element_t;
typedef struct {
Element_t* data; //存储表中数据空间首地址
int pos; //待插入的位置,也表示空间数据个数
int capacity; //表最大容量
}SEQTable_t;
//表头在堆上申请,提供给其他函数使用
2.创建顺序表
SEQTable_t* creatSeqTable(int n);//创建表头并申请数据空间(申请 n个数据空间)
SEQTable_t* creatSeqTable(int n) {
SEQTable_t* table = NULL; //初始化
table =(SEQTable_t*) malloc(sizeof(SEQTable_t));//申请表头空间
if (table == NULL) { //申请失败
fprintf(stderr, "SeqTable malloc failed!\n");//stderr:标准错误输出流
return NULL;
}
table->data = (Element_t*)malloc(sizeof(Element_t) * n);//申请数据空间
if (table->data == NULL) {
fprintf(stderr, "SeqTable data malloc failed!\n");
free(table); // 申请失败,释放表头空间
return NULL;
}
table->pos = 0; //申请成功初始化待插入位置和最大容量
table->capacity = n;
return table;
}
fprintf和 printf 的主要区别如下:
• printf:默认输出到标准输出流(stdout),通常是终端或控制台。
• fprintf:可以指定输出到任意文件流(如文件、标准输出stdout、标准错误stderr等)。
总结:
• printf 是 fprintf 的特例,等价于 fprintf(stdout, …)。
• fprintf 更灵活,可以输出到任意文件流。
3.插入元素
pushbackSeqTable(SEQTable_t* table, Element_t value);//在表中插入value这个元素
- 尾插法
int pushbackSeqTable(SEQTable_t* table, Element_t value) {
//判断是否空
if(table == NULL) {
fprintf(stderr, "table is NULL!\n");
return -1;
}
//判断是否满
if (table->pos >= table->capacity&& enlargerTable(table)) {
//扩容成功返回0,0表示假,不符合进入条件,直接往下执行尾插法
//若扩容失败返回-1,表示真,符合进入条件,打印failed
printf("failed!\n");
}
table->data[table->pos++] = value; // 添加并更新位置
return 0;
}
条件判断中的真假
任何表达式的结果在条件判断中会被隐式转换为整数:
非零值视为真。(包括负数!)
零值(包括0、NULL、'\0’等)视为假。
- 指定位置插入元素
int insertPosSeqTable(SEQTable_t* table, int index, Element_t value) {
//1、空间有效性判断:插入的位置index是否越界
if (index<0 || index>table->pos) {
printf("insert error!\n");
return -1;
}
//顺序表进行扩容
//先判断是否达到扩容条件,进一步判断扩容是否成功
//扩容成功返回0表示假,不执行return -1;继续往后执行数据的添加
if (table->pos >= table->capacity && enlargerTable(table)) {
return -1;
}
//2、搬移数据为新数据腾出空间(从后往前)
for (int i = table->data[table->pos - 1]; i >= index; --i) {
table->data[i + 1] = table->data[i];
}
table->data[index] = value;
++table->pos;
return 0;
}
4.扩容
malloc扩容
static int enlargerTable(SEQTable_t* table) {
//再申请一个更大的空间,初始化,把原数据放入新空间
Element_t* tmp = (Element_t*)malloc(sizeof(Element_t) * (table->capacity * 2));
if (tmp == NULL) { //申请失败
printf("enlargerTable malloc failed!\n");
return -1;
}
//用malloc申请新空间后,手动搬运数据
memcpy(tmp, table->data, sizeof(Element_t) * table->pos);
//把数据copy到哪,从哪copy,copy多少个
table->capacity *= 2;
table->data = tmp;
free(table->data); //必须先释放原内存,再赋值新地址(**realloc不用释放**)
table->data = tmp; // 更新表的data指针(和上面的table->data指向不同的内存地址)
return 0;
}
realloc扩容
Element_t* tmp = (Element_t*)realloc(table->data,sizeof(Element_t) * (table->capacity * 2));
if (tmp == NULL) { // 扩容失败,原有 table->data 依然有效
fprintf(stderr, "扩容失败,保留原有内存\n");
return -1;
}
else {
table->data = tmp;//把新分配的内存地址(tmp)赋值给table->data,让它指向新的、更大的空间
table->capacity *= 2;
return 0;
}
用 realloc 扩容时,数据会自动转移,无需手动搬运。
原地扩展情况
当原有内存位置的后方有足够连续未分配空间时,系统会直接扩展当前内存块,无需移动数据。此时tmp(新分配指针)与table->data(原指针)指向同一地址,仅需更新capacity字段。
新分配空间情况
若原位置后方空间不足,realloc会分配新内存块(tmp指向新地址),系统自动完成以下操作:
- 将旧数据拷贝至新空间(tmp指向的区域)
- 释放旧空间(table->data原指向的内存)
- 将tmp赋值给table->data
5.删除元素
- 顺序表按位查找比较简便
- 这里展示的是按值查找再删除
int deleteSeqTable(SEQTable_t* table, Element_t value) {
// 1. 查找value的位置
int index = findSeqTable(table, value);
if (index == -1) {//没找到
printf("Not find %d element!\n", value);
return -1;
}
// 2. 找到则删除这个元素:把[index + 1, pos]往前搬移,后一个覆盖前一个
for (int i = index + 1; i < table->pos; ++i) {
table->data[i - 1] = table->data[i];
}
--table->pos;
return 0;
}
int findSeqTable(const SEQTable_t* table, Element_t value) {
for (int i = 0; i < table->pos; ++i) {
if (table->data[i] == value) {
return i;
}
}
return -1;
}
6.查看顺序表
void showSeqTable(const SEQTable_t* table) {
for (int i = 0; i < table->pos; i++) {
printf("%d\n", table->data[i]);
}
}
7.销毁顺序表
void releaseSeqTable(SEQTable_t* table);//释放表头和表中指向的数据存储空间
void releasSeqTable(SEQTable_t* table) {
if (table) { // 检查表头指针是否非空
if (table->data) { // 检查数据指针是否非空
free(table->data); // 释放数据空间
}
free(table); // 释放表头空间
}
}
小结
顺序表是一种线性表的物理存储结构,其特点是元素在内存中连续存储,通过下标直接访问。
核心操作
插入操作 时间复杂度:最好O(1)(尾插),最坏O(n)(头插),需要移动n个后续元素。
删除操作 时间复杂度:最好O(1)(尾删),最坏O(n)(头删),需要移动n个后续元素。
若当现有内存空间不足时需要进行扩容操作,扩容通常涉及重新分配内存和复制元素,时间复杂度为O(n)。
最后不要忘记手动释放申请的内存空间。
应用场景
适合元素数量稳定、频繁查询但插入删除较少的场景,如静态数据存储、矩阵运算等。
至此,第一篇博客完结撒花~ 若有不足还请多多包容、提提建议,主包还是很听劝滴!