【数据结构——顺序表】

顺序表的引入

顺序表有着连续且有限的存储空间,像一个单排的储物盒,一个接着一个的存储单元,每个空间元素在逻辑上具有唯一前驱和后继(首元素无前驱,末元素无后继),如下图。

在这里插入图片描述
存储特性:逻辑相邻则物理相邻。

以第一个存储空间的地址为索引向下连续申请至第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指向新地址),系统自动完成以下操作:

  1. 将旧数据拷贝至新空间(tmp指向的区域)
  2. 释放旧空间(table->data原指向的内存)
  3. 将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)。
最后不要忘记手动释放申请的内存空间。

应用场景

适合元素数量稳定、频繁查询但插入删除较少的场景,如静态数据存储、矩阵运算等。
至此,第一篇博客完结撒花~ 若有不足还请多多包容、提提建议,主包还是很听劝滴!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值