hello大家好 欢迎来到小四季豆的博客 
在上一篇博客中我们学习了算法的基础概念与复杂度分析,今天,我们就来认识数据结构中最基础、最核心的成员之一 ------ 顺序表(Sequential List)
一、什么是线性表
在学习顺序表之前,我们需要先理解线性表的概念。
线性表(Linear List):是n个具有相同特性的数据元素组成的有限序列。
- 有序性:数据是一个挨着一个排列的,有先后顺序。
- 一对一关系:除了第一个元素和最后一个元素,中间的每个元素,都有且仅有一个前驱和后继
- 逻辑结构:一定是线性的(思维层面 )
- 物理结构:不一定是线性的(数据在内存中存储是否连续)
我们可以把线性表想象成排队时的队伍,除了第一个人和最后一个人,每个人前面只有一个人(前驱),后面也只有一个人(后继)。
注意:线性表的 "线性",指的是逻辑关系上的一对一顺序结构,而不是指内存里的存储地址必须连续
二、什么是顺序表
1.顺序表的定义
顺序表是线性表逻辑关系的具体实现
顺序表:用一段连续的内存空间,依次存储相同类型数据元素的线性存储结构。
- 相同类型元素:一个顺序表中只能存储同一种数据类型
- 依次存储:数据按照存入的先后顺序排列
- 逻辑结构:线性(一对一关系)
- 物理结构:线性(数据在内存中的存储是连续的,元素相邻)
数组就是顺序表的底层实现
**注意:**数组只是顺序表的底层语法,而顺序表是对数据元素存储和对数据操作(增删改查,扩容,判空)的封装,是一套成熟的数据结构模型。
根据内存分配方式不同,顺序表分为两种:
(1)静态顺序表:长度固定,容量固定
(2)动态顺序表:长度可变,可以灵活调整数据量
2.顺序表的结构定义
(1)静态顺序表
cpp
#define MAXSIZE 10 // 定义最大容量
typedef int SLDataType; // 数据类型,方便修改
//结构体类型重命名
typedef struct SeqList
{
SLDataType data[MAXSIZE]; // 定长数组,容量固定
int size; // 有效数据的个数(当前长度)
} SL;
- 容量:固定不变
- 优点:实现简单,适用于固定数据量的场景
- 缺点:空间给小了容易溢出,空间给大了容易浪费

(2)动态顺序表
cpp
typedef int SLDataType; // 数据类型,方便统一修改
//结构体类型重命名
typedef struct SeqList
{
SLDataType* data; // 指向动态数组的指针(底层是堆上的数组)
int size; // 有效数据的个数
int capacity; // 当前数组的容量
} SeqList;
- *data:指针指向动态申请的数组(与静态顺序表的核心区别)
- 容量:可以根据实际需求进行扩容
- 优点:空间利用率高,灵活性强。适用于数据量不确定的场景
- 缺点:扩容时需要调用realloc,存在时间和空间的开销,可能在堆内存产生碎片
realloc动态内存函数
void * realloc(void*ptr,size_t size);
返回值:开辟成功返回新内存的起始地址(类型要转换成我们所需的结构体类型),开辟失败返回NULL
ptr:想要调整的内存地址
size:调整后新内存的大小(单位是字节)

三、顺序表的实现
三步走
- **xxx.h 头文件:**结构体定义、宏、类型别名、函数声明(对外接口)
- xxx.c 源文件:.h里所有函数的实现、static内部私有函数
- **test.c 测试文件:**main,创建结构体变量,调用接口测试
1.静态顺序表的实现
项目结构:
StaticSeqlist.h:结构体、宏、函数声明
StaticSeqlist.c:所有功能函数实现
test.c:main 测试逻辑
(1)编写头文件
cpp
//头文件保护 + 头文件引入
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#pragma once:防止头文件重复包含_CRT_SECURE_NO_WARNINGS:消除 VS 安全警告assert.h:用于接口入参合法性断言
cpp
//定义容量宏 + 数据类型重定义
#define MAX_SIZE 10
typedef int SLDataType;
MAX_SIZE:静态数组固定最大长度,改宏即可修改顺序表容量SLDataType:类型封装,后续存其他类型只需改 typedef 后的类型
cpp
//定义静态顺序表结构体
typedef struct
{
SLDataType arr[MAX_SIZE];
int size;
}StaSL;
arr[MAX_SIZE]:固定大小连续数组,存放数据size:当前有效元素个数,空表size=0
cpp
//声明所有对外功能接口
//初始化
void StaSlInit(StaSL* ps);
//尾插
void SlPushBack(StaSL* ps, SLDataType x);
//头插
void SlPushFront(StaSL* ps,SLDataType x);
//任意位置插入
void SlInsert(StaSL* ps,int pos, SLDataType x);
//尾删
void SlPopBack(StaSL* ps);
//头删
void SlPopFront(StaSL* ps);
//指定位置删除
void SlErase(StaSL* ps, int pos);
//查找元素
int SlFind(const StaSL* ps, SLDataType x);
//打印顺序表
void SlPrint(const StaSL* ps);
//清空顺序表
void StaSlClear(StaSL* ps);
- 只声明需要给外部调用的函数,
static内部工具函数不在头文件声明
(2)编写实现文件StaticSeqlist.c
- 引入头文件 、实现内部私有辅助函数
cpp
#include "StaticSeqlist.h"
//static修饰:函数仅本.c文件可用,外部无法调用(静态顺序表容量编译时就确定)
//不需要给外部文件调用的函数,就加 static,收拢访问权限、防重名、便于模块化
//判满:有效元素>=最大容量
static int SL_isFull(const StaSL* ps)
{
return ps->size >= MAX_SIZE;
}
//判空:有效元素为0
static int SL_isEmpty(const StaSL* ps)
{
return ps->size == 0;
}
- 初始化函数StaSlInit
cpp
void StaSlInit(StaSL* ps)
{
assert(ps != NULL); //防止传入空指针
ps->size = 0; //有效元素置0,完成初始化
}
- 尾插 SlPushBack
cpp
void SlPushBack(StaSL* ps, SLDataType x)
{
assert(ps != NULL);
assert(!SL_isFull(ps)); //满了不能插入
ps->arr[ps->size] = x;
ps->size++;
}
- 头插 SlPushFront
cpp
void SlPushFront(StaSL* ps, SLDataType x)
{
assert(ps != NULL);
assert(!SL_isFull(ps));
//所有元素整体后移一位,从最后一个有效元素开始挪
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
- 任意位置插入 SlInsert
cpp
void SlInsert(StaSL* ps, int pos, SLDataType x)
{
assert(ps != NULL);
assert(!SL_isFull(ps));
//pos合法范围:1 ~ size+1(1头插,size+1等价尾插)
assert(pos >=1 && pos <=ps->size + 1);
//插入点之后数据全部后移
for (int i = ps->size; i > pos - 1; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos - 1] = x; //pos转数组下标:pos-1
ps->size++;
}
- 尾删 SlPopBack
cpp
void SlPopBack(StaSL* ps)
{
assert(ps != NULL);
assert(!SL_isEmpty(ps)); //空表不能删
ps->size--; //有效长度-1,原末尾数据逻辑失效
}
- 头删 SlPopFront
cpp
void SlPopFront(StaSL* ps)
{
assert(ps != NULL);
assert(!SL_isEmpty(ps));
//后续元素逐个向前覆盖
for (int i = 0; i < ps->size; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
- 指定位置删除 SlErase
cpp
void SlErase(StaSL* ps, int pos)
{
assert(ps != NULL);
assert(!SL_isEmpty(ps));
assert(pos >= 1 && pos <= ps->size); //删除位置不能越界
//从删除下标开始,后一个元素向前覆盖
for (int i = pos - 1; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
- 按值查找 SlFind
cpp
int SlFind(const StaSL* ps, SLDataType x)
{
assert(ps != NULL);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i + 1; //找到返回位置(pos从1开始)
}
}
return -1; //找不到返回-1
}
- 顺序表打印 SlPrint
cpp
void SlPrint(const StaSL* ps)
{
assert(ps != NULL);
for (int i = 0; i < ps->size; i++)
{
printf("%d->", ps->arr[i]);
}
printf("\n");
}
- 清空顺序表 StaSlClear
cpp
void StaSlClear(StaSL* ps)
{
assert(ps != NULL);
ps->size = 0; //只需size置0,逻辑清空
}
(3)编写测试文件 test.c
cpp
#include "StaticSeqlist.h"
int main()
{
StaSL SL;
StaSlInit(&SL);
//增数据
SlPushBack(&SL, 3);
SlPushFront(&SL,1);
SlInsert(&SL,2, 2);
SlPushBack(&SL, 4);
SlPrint(&SL);
//查找测试
int Num_pos = SlFind(&SL, 3);
if (Num_pos!=-1)
printf("找到这个元素了!在第%d个位置\n", Num_pos);
else
printf("未找到该元素!\n");
//删数据
SlPopBack(&SL);
SlPopFront(&SL);
SlErase(&SL, 2);
SlPrint(&SL);
//清空顺序表
StaSlClear(&SL);
return 0;
}
注意
static函数只能在.c 定义,不能在.h 声明
删除不用擦除原数组数据,修改 size 就行
2.动态顺序表的实现
- 项目结构
SeqList.h:结构体、宏、函数声明
SeqList.c:所有功能函数实现
test.c:main 测试逻辑
(1)编写头文件 SeqList.h
cpp
//头文件保护 头文件引入
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#pragma once:防止头文件被重复包含,避免结构体重复定义报错_CRT_SECURE_NO_WARNINGS:消除 VS 下 scanf/realloc 等函数的安全警告assert.h:提供assert断言,用于接口入参合法性校验、string.h:提供memmove高效内存拷贝函数
cpp
//数据类型重定义,方便后续更换存储类型
typedef int SLDataType;
//定义动态顺序表结构体
typedef struct SeqList
{
SLDataType* arr; // 动态堆内存开辟的数组
int size; // 当前有效数据元素个数
int capacity; // 当前数组总容量(已开辟空间大小)
}SL;
SLDataType:类型封装,如需存储 char/float,仅修改 typedef 后的基础类型即可arr:不固定长度,运行时通过 realloc 动态扩容
cpp
//声明所有对外功能接口
//初始化顺序表
void SLInit(SL* ps);
//判空
int SL_is_Empty(const SL* ps);
//扩容检查
void Check_Sl_Capacity(SL* ps);
//尾插
void SlPushBack(SL* ps, SLDataType x);
//头插
void SlPushFront(SL* ps, SLDataType x);
//任意位置插入(pos从1开始)
void SlInsert(SL* ps, int pos, SLDataType x);
//pos下标后插入新元素
void SlInsertback(SL* ps, int pos, SLDataType x);
//尾删
void SlPopBack(SL* ps);
//头删
void SlPopFront(SL* ps);
//指定下标删除(pos从0开始)
void SlErase(SL* ps, int pos);
//按值查找,返回下标,找不到返回-1
int SlFind(const SL* ps, SLDataType x);
//销毁顺序表
void SlDestory(SL* ps);
//打印遍历顺序表
void SlPrint(const SL* ps);
(2)编写实现文件 SeqList.c
- 初始化顺序表
cpp
#include "SeqList.h"
void SLInit(SL* ps)
{
assert(ps != NULL);
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
- 判断顺序表是否为空
cpp
int SL_is_Empty(const SL* ps)
{
assert(ps);
return ps->size == 0; //size==0返回 1(空表),否则返回 0
}
- 检查是否满容
cpp
void Check_Sl_Capacity(SL* ps)
{
assert(ps != NULL);
if (ps->size == ps->capacity)
{
//空表首次开辟4个空间,非空二倍扩容
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* temp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (temp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = temp;
ps->capacity = newCapacity;
}
}
**注意:**realloc开辟空间单位为字节
- 尾插
cpp
void SlPushBack(SL* ps, SLDataType x)
{
assert(ps != NULL);
Check_Sl_Capacity(ps);
ps->arr[ps->size++] = x;
}
- 头插
cpp
void SlPushFront(SL* ps, SLDataType x)
{
assert(ps != NULL);
Check_Sl_Capacity(ps);
//从后往前挪,防止数据被提前覆盖
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
++ps->size;
}
/*
//memmove优化高效写法
Check_Sl_Capacity(ps);
memmove(ps->arr + 1, ps->arr, ps->size * sizeof(SLDataType));
ps->arr[0] = x;
ps->size++;
*/
- 指定位置之后插入
cpp
void SlInsertback(SL* ps, int pos, SLDataType x)
{
assert(ps != NULL);
assert(pos > 0 && pos <= ps->size);
Check_Sl_Capacity(ps);
for (int i = ps->size; i > pos - 1; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
++ps->size;
}
- 指定逻辑位pos插入(pos:1~size+1)
cpp
void SlInsert(SL* ps, int pos, SLDataType x)
{
assert(ps != NULL);
assert(pos > 0 && pos <= ps->size + 1); //判断pos合法性
Check_Sl_Capacity(ps);
for (int i = ps->size; i > pos - 1; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos - 1] = x;
++ps->size;
}
- 尾删
cpp
void SlPopBack(SL* ps)
{
assert(ps != NULL);
assert(!SL_is_Empty(ps));
--ps->size;
}
- 头删
cpp
void SlPopFront(SL* ps)
{
assert(ps != NULL);
assert(!SL_is_Empty(ps));
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
/*
//memmove优化
memmove(ps->arr, ps->arr + 1, (ps->size - 1) * sizeof(SLDataType));
ps->size--;
*/
memmove内存拷贝函数
void *memmove(void *dest, const void *src, size_t n)
从 src 拷贝 n 字节内存到 dest,支持源、目标内存区域重叠
- 指定位置删除
cpp
void SlErase(SL* ps, int pos)
{
assert(ps != NULL);
if (pos < 0 || pos >= ps->size) return;
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
/*
//memmove高效删除
if(pos < ps->size -1)
memmove(ps->arr+pos, ps->arr+pos+1, (ps->size-pos-1)*sizeof(SLDataType));
ps->size--;
*/
- 按值查找
cpp
int SlFind(const SL* ps, SLDataType x)
{
assert(ps != NULL);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
return i; //找到返回位置
}
return -1; //找不到返回-1
}
- 销毁顺序表
cpp
void SlDestory(SL* ps)
{
assert(ps != NULL);
free(ps->arr); //必须 free 动态开辟的 arr 数组,避免内存泄漏
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
- 打印顺序表
cpp
void SlPrint(const SL* ps)
{
assert(ps != NULL);
for (int i = 0; i < ps->size; i++)
{
printf("%d", ps->arr[i]);
if (i != ps->size - 1) //防止多打印箭头
printf("->");
}
printf("\n");
}
(3)编写测试文件 test.c
cpp
#include "SeqList.h"
int main()
{
SL slist;
SLInit(&slist);
//头插1
SlPushFront(&slist, 1);
//尾插2、3、4、6
SlPushBack(&slist, 2);
SlPushBack(&slist, 3);
SlPushBack(&slist, 4);
SlPushBack(&slist, 6);
printf("初始插入:");
SlPrint(&slist);
//在5号逻辑位后插入5
SlInsertback(&slist, 5, 5);
printf("指定位置插入后:");
SlPrint(&slist);
//删除下标5元素
SlErase(&slist, 5);
printf("删除下标5:");
SlPrint(&slist);
//查找元素2
int pos = SlFind(&slist, 2);
if (pos != -1)
{
printf("找到了,在位置%d\n", pos);
}
//销毁释放内存
SlDestory(&slist);
return 0;
}
**注意:****顺序表的检查容量、按值查找和打印只是调用而不修改指针都应该加const---**表达"只读"
至此我们完整的学习了顺序表的实现,通过代码我们可以总结出一个问题:中间位置插入 / 删除需要大批量挪动元素,时间复杂度 O (N);动态扩容存在内存开销、易产生内存碎片。
那我们要如何解决这个问题?这就要用到即将学习的链表~~
此篇到这里就结束啦~~ 希望本篇内容能帮大家理清思路哦~~ 我们下期再见
如果觉得这篇文章对你有帮助,别忘了点赞收藏哦~~