👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:数据结构
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注
目录
一、概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
二、结构
顺序表一般可以分为:
1. 静态顺序表:使用固定长度的数组来存储元素。
#define N 4
typedef int SLDataType;
typedef struct SeqList
{
SLDataType a[N]; //固定长度的数组
int size; //有效的数据个数
}SeqList;
但静态顺表表有个缺点,当它开的长度太少,会导致不够用;当长度开的过长,会导致空间浪费。因此一般都使用动态顺序表。
2. 动态顺序表:使用动态开辟的数组存储
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; // 指向动态开辟的数组
int size; //有效数据的个数
int capacity; //容量空间的大小
}SeList;
几个问题
- 为什么要将
int
类型重命名为SLDataType
?
原因是:当内存中不是存储整型类型的数据,假设存储的是double
,如果不嫌麻烦就要把每个接口的int
改为double
。将类型重命名,就会省去很多麻烦。- 为什么还要定义
capacity
容量大小?
原因是:这是一个动态顺序表,当有效数据的个数大于容量空间的大小,就要扩容。
三、代码实现
3.1 准备工作
为了方便管理,我们可以创建多个文件来实现
- test.c - 测试代码逻辑 (源文件)
- SeqList.c - 动态的实现 (源文件)
- SeqList.h - 存放函数的声明 (头文件)
3.2 实现内容
在开头的概念中说了,顺序表一般是在数组上完成数据的增删查改。这里我给出常见的接口实现内容。
【SeqList.h】
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
int size;
int capacity;
}SeqList;
//接口
//数组初始化
void SeqListInit(SeqList* ps);
//内存释放
void SeqListDestroy(SeqList* ps);
//打印数组内容
void SeqListPrint(SeqList* ps);
//数组的尾插
void SeqListPushBack(SeqList* ps, SLDateType x);
//数组的头插
void SeqListPushFront(SeqList* ps, SLDateType x);
//数组的头删
void SeqListPopFront(SeqList* ps);
//顺序表之尾删
void SeqListPopBack(SeqList* ps);
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);
3.3 顺序表之结构体初始化
注:以下代码都在
SeqList.c
中实现
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = (SLDateType*)malloc(sizeof(SLDateType) * 4);
if (ps->a == NULL)
{
perror("ps->a :: malloc");
return;
}
ps->size = 0;
ps->capacity = 4;
}
【笔记总结】
3.4 顺序表之内存空间释放
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a == NULL;
ps->size = 0;
ps->capacity = 0;
}
【笔记总结】
- 为什么要释放内存空间?
因为在结构体初始化中,malloc向内存申请了空间,如果由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。(内存泄漏)- 动态内存开辟函数回顾 —> 动态内存管理
3.5 顺序表之打印
void SeqListPrint(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
}
3.6 顺序表之尾插
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps);
//扩容
if (ps->size == ps->capacity)
{
//扩容原来容量的2倍
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->size] = x;
ps->size++;
}
3.7 顺序表之头插
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//头插也需要扩容
if (ps->size == ps->capacity)
{
//扩容原来容量的2倍
SeqList* tmp = (SeqList*)realloc(ps->a, sizeof(SeqList) * 2 * ps->capacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
//头插
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
讲讲头插过程
end
一定要从最后一个数据开始往后挪动。
从头开始往后挪数据会造成后面数据被前一个数据覆盖了(错误演示)
3.8 顺序表之头删
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//头删
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
【笔记总结】
- 首先要保证顺序表不能为空,空的顺序表是不能删数据的,所以要加个断言
assert(ps->size > 0)
- 头删过程动图展示(
begin
从下标1开始)
3.9 顺序表之尾删
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
ps->size--;
}
【笔记总结】
- 空顺序表也不能尾删,所以要断言一下.
- 尾删时不需要让尾数据赋值成0,因为顺序表中的数据也有可能为0。只需要让
size--
,因为顺序表下标的访问都依靠size
3.10 顺序表之查找
int SeqListFind(SeqList* ps, SLDateType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
//找到返回下标
return i;
}
}
//循环跳出就代表找不到
//假设找不到返回-1
return -1;
}
3.11 顺序表之插入
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
assert(ps);
assert(pos >= 0 && ps <= ps->size);
//插入也可能需要扩容
if (ps->size == ps->capacity)
{
//扩容原来容量的2倍
SLDateType* tmp = (SLDateType*)realloc(ps->a, sizeof(SLDateType) * 2 * ps->capacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
//插入
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
【学习笔记】
- 插入pos的范围必须在
[0,size]
,pos = size
相当于尾插- 插入动图展示
3.12 顺序表之删除
void SeqListErase(SeqList* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
【学习笔记】
- 首先pos的范围是
[0,size)
,原因是以size
为下标是没有元素的,而空顺序表是不能够删除的- 动图展示
四、总结
- 顺序表的好处是在于它尾插和尾删的时间复杂度是
O(1)
,但头删和头插时间复杂度是O(N)
。- 增容需要申请新的空间,拷贝数据还要释放空间,都会有不少的消耗。
- 空间不够时,需要扩容。而扩容其实也会造成一定的空间浪费。例如,当前的空间是5,满了以后扩容到10(因为上面规定扩容要是当前空间的2倍),而后面只需要再插入1个数据,那么就会浪费4个数据空间。