目录
[1、头部 / 中间操作效率低](#1、头部 / 中间操作效率低)
[(二) 改进方向:链表的引入](#(二) 改进方向:链表的引入)
一、线性表基础
线性表是 n 个具有相同特性的数据元素的有序、有限序列。
线性表是一种在实际中广泛使用的数据结构,顺序表、链表、栈、队列、字符串均属于线性表。要理解顺序表,需要先掌握线性表的共性。
核心特性可从两个维度分析:
1、逻辑结构
一定是线性的,即元素间呈 "一条线" 的前后关系(如排队的人抽象为线性结构),是人为想象的抽象结构。
2、物理结构
不一定是线性的,即元素在内存中的存储地址可能连续(如数组),也可能不连续(如后续会学的链表)。
二、顺序表
(一)顺序表定义
顺序表是用物理地址连续的存储单元依次存储数据元素的线性结构 ,底层基于数组实现,但通过封装提供更完备的操作接口。
不仅提供存储空间,还预实现增删改查等操作方法,用户无需重复编写基础逻辑。
(二)分类
1、静态顺序表
(1)概念 :用定长数组存储元素,初始化时空间大小固定,无法动态调整。
(2)结构体定义(代码实现)
cpp
// 定义数据类型别名,方便后续修改存储类型(如int→char)
typedef int SLDataType;
// 宏定义数组固定大小
#define N 100
typedef struct SeqList {
SLDataType a[N]; // 定长数组
int size; // 有效数据个数
} SL;
(3)结构解析
**① a[N]:**固定大小的数组,容量由N决定(如N=100则最多存 100 个元素)。
**② size:**关键变量,记录有效数据个数 ------ 如size=5表示数组前 5 个元素(下标 0-4)是有效数据,后续元素(下标 5-99)无意义。
**示例:**若N = 7(数组容量 7),当前存 4 个有效数据,则size = 4,指向数组下标 4 的位置(下一个可插入数据的位置)。
**(4)**特点
**① 优点:**结构简单,内存占用少
② 缺点:空间灵活性差 。给小了不够用,如存储用户数据时,空间不足导致用户注册失败;给大了造成浪费,如给 100 万空间仅用 1 万。
2、动态顺序表
(1)概念 :通过指针动态申请内存,空间不足时可扩容,适用于大多数实际场景。
(2)结构体定义
cpp
typedef int SLDataType;
typedef struct SeqList {
SLDataType* a; // 动态数组指针(指向堆区内存)
int size; // 有效数据个数
int capacity; // 当前已申请的空间容量
} SL;
(3)结构解析
**① a:**指针,初始为NULL,后续通过malloc/realloc申请内存,指向存储数据的连续空间。
**② size:**有效数据个数,如size=3表示当前存了 3 个数据。
**③ capacity:**当前总容量,如capacity = 5表示已申请 5 个空间(可能用了3个,空闲2个)。
(4)特点
**① 优点:**按需扩容,空间浪费可控(如初始 10 个空间,满了扩到 20 个)。
**② 缺点:**需额外维护 capacity,结构稍复杂。
三、工程结构设计
需创建 3 个文件,分工明确,便于维护和调试,后续学链表、栈等结构也可复用此结构。头文件为 SeqList.h,实现文件为 SeqList.c,测试文件为 test.c。
头文件 的核心作用,首先是引用依赖的库、然后定义结构体 、再声明要实现的函数。
实现文件 首先引用头文件(#include"SeqList.h"),然后完成对声明函数的具体实现。
测试文件 首先引用头文件(#include"SeqList.h"),然后测试函数的正确性,函数的调用都是在这里完成的。

下面是一个工程里面,三个文件的具体展示。你要用到的头文件和结构体都先往 SeqList.h 里面去写,然后你要实现一个功能,就先往 SeqList.h 里面去写声明,先暂时确定参数与返回值。
声明完成之后,就在 SeqList.c 文件里面去实现,具体实现的期间可能存在返回值与参数的适当修改,此时同时也要修改头文件。
当头文件与实现文件的内容确认无误后,我们就在 Test.c 文件里面调用函数测试即可。
四、动态表核心方法的实现
你要用任何一个数据结构,第一步肯定是初始化,最后一步肯定是销毁 ,中间的操作步骤无非就是增、删、改、查。
对于顺序表而言:
① 在增的方面,有尾插、头插、指定位置之前插入数据、指定位置之前插入批量数据;
② 在删的方面,有尾删、头删、删除指定位置数据、删除指定位置向后的批量数据;
③ 在改的方面,就是对任意位置的数据进行修改;
④ 在查的方面,就是查找顺序表,这个数据是否存在。
这个思路,不仅仅是顺序表,其他数据结构的也是大差不差的。
(一)初始化动态表
将动态顺序表初始化为 "空状态",避免野指针(未初始化的指针)导致程序崩溃。
1、头文件(SeqList.h)
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//顺序表的结构
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size; //有效空间大小
int capacity; // 空间大小
}SL;
//typedef struct SeqList SL;
//初始化
void SLInit(SL* ps);
2、实现文件(SeqList.c)
cpp
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
3、测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
}
int main()
{
test01();
return 0;
}
(二)增
在下面的代码中,为了不重复书写头文件与定义结构体,从而使代码变得冗余,头文件我仅展示具体的函数声明 ,实现文件我仅展示具体的函数实现 ,测试文件我将使用目前所实现的函数进行测试。
具体的函数代码已经上传,可于文章头部下载。
1、空间检查与扩容
插入数据前必调用,判断当前空间是否充足;不足则扩容,确保插入操作能正常执行。
因为我不确定每次增加的数据是,如同尾插、头插、定点插入,只插入一个数据;还是会批量插入,所以我需要通过加法计算得到总容量。
然后在下面将总容量与新容量进行对比,决定是否进行扩容。
若每次只增加一个空间,会存在频繁扩容 ,这样程序执行效率低,时间效率低;若每次增加的空间较大,会存在空间的浪费。
所以一般增容成倍数去增加,通常是 2 倍,5个空间满了,就扩容成10个。
因为这个函数是实现文件的一个辅助函数,用于辅助实现文件里面函数的实现,而不是在测试文件中进行测试。
(1)头文件
传递的是结构体的指针,使用传址调用,间接传递整个结构体。
cpp
//判断空间是否足够
void SLCheckCapacity(SL* ps);
(2)实现文件
cpp
//判断空间是否足够
void SLCheckCapacity(SL* ps, int needSize) {
assert(ps);
// 计算需要的总容量
int requiredCapacity = ps->size + needSize;
if (requiredCapacity <= ps->capacity) {
return; // 容量足够,无需扩容
}
// 计算新容量(确保能容纳所有元素)
int newCapacity;
if (ps->capacity == 0)
newCapacity = 4;
else
newCapacity = ps->capacity
while (newCapacity < requiredCapacity)
newCapacity *= 2;
// 扩容操作
SLDataType* temp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (temp == NULL) {
perror("realloc fail");
exit(EXIT_FAILURE);
}
// 更新顺序表的指针和容量
ps->arr = temp;
ps->capacity = newCapacity;
}
2、打印
无论是增、删、改,都需要通过测试知道,是否执行成功。而反馈是否执行成功,最直观的就是将顺序表打印出来,而不是去一步步地调试。
如果打印失败,又无法确切发现问题在哪,才需要去调试解决。
(1)头文件
cpp
//打印数据
void Print(SL* ps);
(2)实现文件
cpp
//打印
void Print(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
printf("%d\n", ps->arr[i]);
}
3、尾插
在顺序表末尾追加一个数据,无需挪动元素,效率最高(时间复杂度 O (1))
(1)头文件
传递的是结构体的指针,使用传址调用,间接传递整个结构体;同时传递需要插入的元素。
cpp
//尾插
void SLPushBack(SL* ps, SLDataType x);
(2)实现文件
cpp
//尾插(这个代码只能一个一个地插入)
//时间复杂度为O(1)
void SLPushBack(SL* ps, SLDataType x)
{
// 判断结构体是否为空,同时
assert(ps);
SLCheckCapacity(ps, 1);
ps->arr[ps->size] = x;
ps->size++;
//ps->arr[ps->size++] = x;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPushBack(&sl, 6);
SLPushBack(&sl, 7);
SLPushBack(&sl, 8);
Print(&sl);
}
int main()
{
test01();
return 0;
}

4、头插
在顺序表头部插入一个数据,需先将现有元素整体后移一位,避免数据被覆盖,时间复杂度为 O (n)。
(1)头文件
cpp
void SLPushFront(SL* ps, SLDataType x);
(2)实现文件
cpp
void SLPushFront(SL* ps, SLDataType x)
{
// 断言检测是否为空指针,下面两种测试也可以
assert(ps);
// assert(ps!=NULL);
//if (ps == NULL)
// return 0;
// 步骤1:检查空间
SLCheckCapacity(ps,1);
// 步骤2:现有元素整体后移
int i = 0;
for (i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
// 步骤3:头部插入新数据,同时有效数据个数+1
ps->arr[0] = x;
ps->size++;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLPushFront(&sl, 6);
SLPushFront(&sl, 7);
SLPushFront(&sl, 8);
Print(&sl);
}
int main()
{
test01();
return 0;
}

5、任意位置之前插入数据
在指定下标 pos 的 "前面" 插入数据(如pos=2,则插入到下标 2 的位置,原下标 2 及后续元素后移),是头插、尾插的通用场景。
(1)头文件
cpp
//指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x);
(2)实现文件
cpp
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
// 步骤1:校验pos的合法性
assert(pos >= 0 && pos <= ps->size);
// 步骤2:检查空间
SLCheckCapacity(ps, 1);
// 步骤3:pos及后续元素整体后移
int i = 0;
for (i = ps->size; i > pos; i--)
ps->a[i] = ps->a[i - 1];
// 步骤4:在pos位置插入数据
ps->a[pos] = x;
// 步骤5:有效数据个数+1
ps->size++;
}
**关键校验:**assert(pos >= 0 && pos <= ps->size) ------ 若pos = -1(越界左)或pos = size + 1(越界右),程序立即报错,避免非法插入。
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLPushFront(&sl, 6);
SLPushFront(&sl, 7);
SLPushFront(&sl, 8);
SLInsert(&sl, 2, 100);
Print(&sl);
}
int main()
{
test01();
return 0;
}

6、任意位置之前批量插入元素
在指定下标 pos 的 "前面" 插入任意数量的数据(如pos=2,count =3 ,则从到下标为 2 的位置开始插入 3 个元素,原下标 2 及后续元素后移)。
(1)头文件
cpp
// 任意位置批量插入
// pos: 插入位置(0 <= pos <= size)
// data: 待插入的数据数组
// count: 插入的元素数量
void SLBatchInsert(SL* ps, int pos, const SLDataType* data, int count);
(2)实现文件
这里有一个关键的点,一旦要添加的数据的话,那么覆盖一定是从后往前,即先确定最后的一位的数据。因为如果覆盖从前往后,那前面的数据被覆盖了,就无法对后面的数据进行覆盖。
实现了这个函数,其实也可以任意插入一个元素,这是包含了上面函数的实现的。
cpp
// 指定位置之前批量插入数据
void SLBatchInsert(SL* ps, int pos, const SLDataType* data, int count)
{
assert(ps && data && count > 0);
assert(pos >= 0 && pos <= ps->size); // 确保插入位置合法
// 1. 提前扩容,确保有足够空间容纳count个新元素
SLCheckCapacity(ps, count);
// 2. 从后往前移动元素,腾出插入位置
// (需要移动size - pos个元素,每个元素向后移动count个位置)
int i = 0;
for (i = ps->size - 1; i >= pos; i--)
ps->arr[i + count] = ps->arr[i];
// 3. 批量插入新元素
for (int i = 0; i < count; i++)
ps->arr[pos + i] = data[i];
// 4. 更新元素总数
ps->size += count;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
const int arr[10] = { 11,12,13,14,15,16,17,18,19,20 };
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLBatchInsert(&sl, 2, arr, 5);
Print(&sl);
}
int main()
{
test01();
return 0;
}

(三)删
删除顺序表末尾的有效数据,无需挪动元素,仅修改 size,时间复杂度为 O (1) 。
1、尾删
(1)头文件
cpp
//尾删
void SLPopBack(SL* ps);
(2)实现文件
cpp
void SLPopBack(SL* ps)
{
assert(ps);
// 校验顺序表非空(size > 0),空表无法删除
assert(ps->size > 0);
// 关键操作:有效数据个数-1(原末尾数据变为"无效数据",后续插入会覆盖)
ps->size--;
}
**逻辑说明:**现有数据[1,2,3,4](size=4),尾删后size=3------ 此时a[3]的4仍在内存中,但size=3表示 "仅前 3 个元素有效",后续插入(如尾插5)会覆盖a[3],无需手动 "删除" 物理数据。
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLPopBack(&sl);
Print(&sl);
}
int main()
{
test01();
return 0;
}

2、头删
删除顺序表头部的有效数据,需将后续元素整体前移一位,覆盖头部数据,时间复杂度 O (n)。
(1)头文件
cpp
void SLPopFront(SL* ps);
(2)实现文件
cpp
void SLPopFront(SL* ps) {
assert(ps);
assert(ps->size > 0); // 空表校验
// 步骤1:后续元素整体前移(从第二个元素开始,覆盖前一个元素)
int i = 0;
for (i = 0; i < ps->size - 1; i++)
ps->a[i] = ps->a[i + 1];
// 步骤2:有效数据个数-1
ps->size--;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLPopFront(&sl);
Print(&sl);
}
int main()
{
test01();
return 0;
}

3、指定位置删除
删除指定下标 pos 的有效数据,pos 后续元素前移覆盖(头删、尾删的通用场景)。
(1)头文件
cpp
//指定位置删除数据
void SLEraes(SL* ps, int pos);
(2)实现文件
cpp
void SLErase(SL* ps, int pos) {
assert(ps);
assert(ps->size > 0); // 空表校验
// 校验pos合法性(0 ≤ pos < size,pos=size无有效数据)
assert(pos >= 0 && pos < ps->size);
// 步骤1:pos后续元素整体前移
int i = 0;
for (i = pos; i < ps->size - 1; i++)
ps->a[i] = ps->a[i + 1];
// 步骤2:有效数据个数-1
ps->size--;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLEraes(&sl, 0);
SLEraes(&sl, 2);
Print(&sl);
}
int main()
{
test01();
return 0;
}

4、指定位置批量删除元素
删除指定下标 pos 及以后的 Number 个有效数据,pos+Number-1 的后续元素前移覆盖
(1)头文件
cpp
//批量删除
void SLBatchDelete(SL *ps, int pos, int Number);
(2)实现文件
cpp
// 批量删除
void SLBatchDelete(SL* ps, int pos, int Number)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
assert(Number > 0);
assert(pos + Number <= ps->size);
int i;
for (i = pos; i < ps->size - Number; i++)
ps->arr[i] = ps->arr[i + Number];
ps->size -= Number;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLBatchDelete(&sl, 1, 3);
Print(&sl);
}
int main()
{
test01();
return 0;
}

(四)改
1、修改指定位置数据
修改 pos 位置的数据,将其修改为 value。
(1)头文件
cpp
void SLModify(SL* ps, int pos, SLDataType value);
(2)实现文件
cpp
void SLModify(SL* ps, int pos, SLDataType value)
{
// 检查指针有效性
assert(ps);
// 检查位置合法性:必须在有效元素范围内
assert(pos >= 0 && pos < ps->size);
// 直接通过索引修改数据
ps->arr[pos] = value;
}
(3)测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
SLModify(&sl, 1, 5);
SLModify(&sl, 2, 55);
Print(&sl);
}
int main()
{
test01();
return 0;
}

(五)查
1、查找顺序表中是否存在对应数据
(1)头文件
cpp
// 查找
int SLFind(SL* ps, SLDataType x);
(2)实现文件
在顺序表中查找,将数据 x 与顺序表中数据进行比较;找到了就返回下标,未找到,返回无效下标,如 -1 (下标无负数值,用于标识 "未找到")。
cpp
// 查找
int SLFind(SL* ps, SLDataType x)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
return i;
}
return -1;
}
**(3)**测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLPushFront(&sl, 3);
SLPushFront(&sl, 4);
SLPushFront(&sl, 5);
int ret = SLFind(&sl, 11);
if (ret < 0)
printf("未找到\n");
else
printf("找到了\n");
}
int main()
{
test01();
return 0;
}

(六)销毁动态顺序表
释放动态顺序表在堆区申请的内存,避免内存泄漏(程序结束前必调用)。
1、头文件
cpp
// 销毁
void SLDesTroy(SL* ps);
2、实现文件
cpp
void SLDestroy(SL* ps)
{
assert(ps);
// 仅当数组指针非空时释放(避免重复释放导致崩溃)
if (ps->arr != NULL)
free(ps->arr); // 释放堆区内存
ps->arr = NULL; // 指针置空,避免野指针(释放后指针仍指向原地址,需置空)
ps->size = 0; // 重置有效数据个数
ps->capacity = 0; // 重置容量
}
3、测试文件
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void test01()
{
SL sl;
SLInit(&sl);
SLPushFront(&sl, 1);
SLPushFront(&sl, 2);
SLDesTroy(&sl);
}
int main()
{
test01();
return 0;
}
(七)总结
里面代码涉及的一些思路其实是很简单的:
增就是部分数据后移,把一块空间放出来,再插入数据,然后调整有效数据个数;
删就是部分数据前移,把需要删除的数据覆盖掉,然后调整有效数据个数;
改就是先遍历找到这个元素,再去覆盖即可;
查就是是遍历找到这个元素。
五、顺序表的问题与反思
(一)核心缺陷
1、头部 / 中间操作效率低
(1)问题
头插、头删、中间插入、中间删除均需挪动大量元素,时间复杂度为 O (n)------ 若顺序表有 100 万数据,头插需挪动 100 万次,效率极低。
(2)场景限制
不适用于 "频繁操作头部或中间" 的场景(如实现队列、频繁插入日志等)。
2、扩容消耗大
扩容时需执行 "申请新空间→拷贝旧数据→释放旧空间" 三步操作,频繁扩容会严重降低程序效率。
3、空间浪费不可避免
(1)问题
2 倍扩容策略虽减少扩容次数,但会导致 "闲置空间"------ 如容量从 100 扩到 200,仅存 105 个数据,浪费 95 个空间;容量从 1 万扩到 2 万,仅存 1.1 万个数据,浪费 9000 个空间。
(2)本质
用 "部分空间浪费" 换 "更少的扩容次数",是一种 "权衡",但无法完全避免浪费。
(二) 改进方向:链表的引入
顺序表的缺陷可通过 "链表" 解决:
① 链表的头部 / 中间插入删除时间复杂度为 O (1)(无需挪动元素,仅修改指针指向)。
② 链表无需预分配空间,插入一个数据申请一个空间,无扩容消耗和空间浪费。
以上即为 一篇文章掌握"顺序表" 的全部内容,麻烦三连支持一下呗~
