导言
顺序表这个专题是属于数据结构这一块的,所以一定要有C语言基础。
顺序表的概念
板块一:数据的定义与实例
数据是计算机科学中的基本概念,它代表了在计算机程序中处理的一切信息内容。数据可以是任何形式的信息单元,包括但不限于:
-
数值型数据:如整数1、2、3、4... 或浮点数等,这类数据用于表示数量和度量值。
- 非数值型数据:
- 结构化数据:如教务系统中存储的学生信息记录,每个记录包含了多个数据项,如姓名、性别、年龄、学历等,这些数据项共同描述了一个实体的特征。
- 多媒体数据:网页中展示的信息,如文字内容、图片、视频等,这些都是计算机能够识别和处理的不同形式的数据。
板块二:结构的定义与作用
结构是指组织和管理数据的方式,其目的是为了更有效地存储、检索和操作数据。在计算机科学中:
-
手动定义独立变量的局限性:当需要处理大量同类数据时,若逐个创建独立的变量会导致代码难以理解和维护,不利于高效编程。
-
数据结构的应用:使用数据结构(如数组、链表、树、图等)可以将数据组织成更有条理的形式。例如,通过数组可以连续地存储一组相同类型的数据元素,便于按照下标快速访问。
-
结构化的现实类比:如同羊圈可以有序地组织羊群一样,数据结构提供了一种机制来组织计算机中的数据元素,使得根据特定规则(如编号、名称等)快速定位和处理数据成为可能。
数据结构的综合概念:
数据结构是计算机存储、组织数据的⽅式。数据结构是指相互之间存在⼀种或多种特定关系 的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
就比如,现在吃饭都有一个扫码点单(人多就不容混淆,这样更有逻辑,结构化),就不需要你在前台去点(如果人多容易让老板混淆),
顺序表
顺序表的底层就是数组,你可以理解为他就是模块话的数组,可以实现一写增删查改等功能
顺序表分两种:
静态顺序表:静态顺序表缺陷:空间给少了不够⽤,给多了造成空间浪费
动态顺序表:灵活性更高,可以动态的开辟内存,这样就避免了空间的浪费,空间不够的时候自动增加空间(增容)
代码
接下来看代码的实现,代码的解析我会注释在代码中
text.h
#pragma once
//头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
//#include<vld.h>
#include"Contacts.h"
#include<vld.h>//检查内存的一个头文件,不知道如何安装的可以去看我上一个博客,有我在b站的教程
typedef int SLDataType;
//结构体
typedef struct SeqList
{
SLDataType* arr;//结构体指针
int size;//有效个数
int capacity;//可用空间
}SL;
//函数申明
/*--初始化--*/
void SLInit(SL* ps);
/*--头部插入--*/
void Headinsertion(SL* ps, SLDataType x);
/*--尾部插入--*/
void Tailinsertion(SL* ps, SLDataType x);
/*--头部删除--*/
void Headerremoval(SL* ps);
/*--尾部删除--*/
void Tailremoval(SL* ps);
/*指定位置之前插入*/
void SLInsert(SL* ps, int pos, SLDataType x);
/*指定位置删除*/
void SLErase(SL* ps, int pos);
/*查找数据*/
int SLFind(SL* ps, SLDataType x);
/*输出函数*/
void Printff(SL* ps);
/*--内存销毁--*/
void SLDestroy(SL* ps);
fun.c函数功能的实现
#include "text.h" // 包含自定义的text.h头文件,假设其中定义了SLDataType类型和顺序表结构体SL
// 扩容内存函数
void Expansion(SL *ps) {
assert(ps); // 断言检查输入的顺序表指针是否有效
if (ps->size == ps->capacity) // 当顺序表已满(实际元素数量等于容量)
{
// 根据当前容量计算新的容量:若当前容量为0,则扩容到4;否则扩容至两倍当前容量
int now_capacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
// 使用realloc动态调整顺序表内存大小
SLDataType* tmp = realloc(ps->arr, now_capacity * sizeof(SLDataType));
if (tmp == NULL) // 如果内存分配失败
{
assert(tmp); // 断言失败,输出错误信息并终止程序
exit(1);
}
ps->arr = tmp; // 更新顺序表的数组指针
ps->capacity = now_capacity; // 更新顺序表的容量
}
}
// 初始化顺序表,不预先分配内存
void SLInit(SL* ps) {
ps->arr = NULL; // 将数组指针设置为NULL
ps->size = 0; // 初始化元素数量为0
ps->capacity = 0; // 初始化容量为0
}
// 初始化顺序表并预先分配可容纳10个元素的空间
void SLInit(SL* ps) {
ps->arr = (SLDataType*)malloc(10 * sizeof(SLDataType)); // 分配内存
if (ps->arr == NULL) { // 检查内存分配是否成功
perror("SLInit failed"); // 输出错误信息
exit(EXIT_FAILURE); // 终止程序
}
ps->size = 0; // 初始化元素数量为0
ps->capacity = 10; // 初始化容量为10(无需乘以sizeof(SLDataType),因为已经在malloc中考虑)
/* 注意:通常情况下,上述两个SLInit函数会冲突,建议合并成一个函数并增加参数决定是否预分配内存 */
// 头部插入元素
void Headinsertion(SL* ps, SLDataType x) {
assert(ps);
Expansion(ps); // 先扩容确保有足够的空间
for (int i = ps->size++; i > 0; i--) // 将所有元素向后移动一位
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x; // 在首位插入新元素
}
// 尾部插入元素
void Tailinsertion(SL* ps, SLDataType x) {
assert(ps);
Expansion(ps);
ps->arr[ps->size++] = x; // 直接在尾部新增元素并更新元素数量
}
// 头部删除元素
void Headerremoval(SL* ps) {
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size - 1; i++) // 除了最后一个元素外的所有元素向前移动一位
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--; // 减少元素数量
}
// 尾部删除元素
void Tailremoval(SL* ps) {
assert(ps);
assert(ps->size);
ps->size--; // 直接减少元素数量,不需要移动元素
}
// 在指定位置插入元素
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
Expansion(ps);
for (int i = ps->size; i > pos; i--) // 从最后一个元素开始,将插入位置之后的元素依次后移一位
{
ps->arr[i] = ps->arr[i - 1];
}
ps->size++; // 增加元素数量
ps->arr[pos] = x; // 在指定位置插入新元素
}
// 删除指定位置的元素
void SLErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; i++) // 从删除位置开始,将删除位置之后的元素依次前移一位
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--; // 减少元素数量
}
// 查找数据,返回元素所在位置或-1表示未找到
int SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->arr[i] == x) {
return i;
}
}
return -1;
}
// 销毁顺序表,释放内存
void SLDestroy(SL* ps) {
free(ps->arr); // 释放数组内存
ps->arr = NULL; // 将数组指针置为NULL
ps->capacity = ps->size = 0; // 将容量和元素数量重置为0
}
// 输出顺序表内容
void Printff(SL* ps) {
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->arr[i]); // 输出每个元素
}
printf("\n%d ", ps->size); // 输出顺序表中元素的数量
}
为什么要二倍扩容?
在顺序表(Sequential List)的设计中,选择二倍增容策略的主要原因是为了优化插入操作的效率和内存使用的平衡:
-
避免频繁扩容:
- 如果每次有新的元素插入时只增加固定大小的内存,例如每次增加1个元素的空间,那么在连续插入多个元素的情况下,将会频繁地进行内存的分配和拷贝操作,导致性能下降。
- 使用二倍增容策略可以在一定程度上减缓这种频率,因为每次扩容都会使可用空间翻倍,理论上可以满足更多次的连续插入操作而无需再次扩容。
-
降低平均时间复杂度:
- 扩容时通常需要将原数组中的元素复制到新的更大空间中,这是一个O(n)复杂度的操作。
- 若每次都加倍扩容,虽然在扩容时的开销较大,但在多次插入操作的平均情况下,单次插入的成本相对较低,因为它摊薄到了更多的插入操作上。
-
空间利用率:
- 虽然每次按二倍扩容可能会造成一定的内存浪费(特别是在列表规模较小的时候),但随着列表规模的增长,这种浪费相对于整个数据结构所占用的内存来说占比会逐渐减小。
- 当列表不再增长或接近稳定状态时,额外分配的空间浪费也会趋于稳定,从而达到空间利用和性能之间的折衷。
因此,二倍增容是一种常见的动态数组管理策略,它在实践中被证明是一种较为有效的解决方案,能够在多数情况下兼顾时间和空间效率。当然,具体扩容策略还可以根据应用场景进行调整,比如有的实现可能采用1.5倍扩容或者其他的增长因子。
差不多就这样了,再见