在前面,我们把C语言的全部基础知识学完了,现在正式开始我们的顺序表!!
顺序表的实现
- 1.顺序表是什么呢?
- 2顺序表的实现
-
- 2.1顺序表变量的命名规则
- 2.2创建结构体
- 2.3初始化结构体
- 2.4打印顺序表
-
- SeqList.h文件
- SeqList.c文件
- test.c文件
- 2.5空间大小判断
- 2.6头插和尾插
-
- 2.6.1尾插的实现
- 2.6.2头插的实现
- 2.7关键知识讲解
-
- 2.7.1头插和尾插
- 2.8尾删和头删
- 2.8.1尾删的实现
- 2.8.2头删的实现
- 2.9关键知识讲解
- 2.10任意插入和任意删除和查找
-
- 2.10.1任意插入
- 2.10.2任意删除
- 2.10.3元素查找
- 2.11关键知识总结
- 2.12销毁申请的空间
- 2.13完整代码1
-
- SeqList.h头文件
- SeqList.c文件
- test.c文件
- 2.14完整代码2
-
- SeqList.h文件
- SeqList.c文件
- test.c文件
- 3.顺序表的作用与运用场景
-
- 3.1顺序表的核心作用
- 3.2顺序表的典型运用场景
1.顺序表是什么呢?
顺序表就像"规整的货架",数据排得整整齐齐,取数据快,但调整开头或中间的货物就很麻烦。它的核心价值在于随机访问高效、实现简单,适合数据量不算大、以尾插/尾删个随机访问为主的场景。
掌握顺序表是基础中的基础,后续学数据结构与算法、应对专升本考上和编程竞赛,都会经常和它打交道
2顺序表的实现
2.1顺序表变量的命名规则
顺序表经常进行以下命名,可根据自己的命名习惯来命名
- 顺序表:SeqList 简化:SL
- 头插:SLpushFront
- 头删:SLPopFront
- 尾插:SLPushBack
- 尾删:SLPopBack
- 空间大小判断:SLCheckCapacity
- 打印顺序表:SLPrint
- 销毁空间:SLDestory
2.2创建结构体
需要在头文件中创建结构体,并将结构体初始化
C
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
int* arr;
int size;//有效数据个数
int capecity;//空间大小
}SL;
- 用typedef将结构体缩写为SL,这方便后续写简洁的代码
- 上面将int修改为SLDataType,方便后续的切换成其他类型,只需要把int类型改成其他类型
2.3初始化结构体
刚开始,需要把结构体中的任何数据初始化为0,然后在后期进行赋值处理
2.4打印顺序表
当实现完前面顺序表的核心内容,剩下的就是打印顺序表和销毁顺序表了,我们先看如何打印顺序表
C
//打印顺序表
void SLPrint(SL ps);
C
//打印顺序表
void SLPrint(SL sp)
{
for (int i = 0; i < sp.size; i++)
{
printf("%d ", sp.arr[i]);
}
printf("\n");
}
C
//打印
SLPrint(sl);
SeqList.h文件
C
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;//方便后续类型的替换
//动态顺序表
typedef struct SeqList
{
int* arr;
int size;//有效数据个数
int capecity;//空间大小
}SL;
//初始化
void SLInit(SL* ps);
SeqList.c文件
C
#include"SeqList.h"
//顺序表的初始化
void SLInit(SL* s)
{
s->arr = NULL;
s->size = s->capecity = 0;
}
test.c文件
C
#include"SeqList.h"
void SLTest01()
{
SL sl;
//初始化
SLInit(&sl);
}
int main()
{
SLTest01();
return 0;
}
2.5空间大小判断
利用动态内存分配,就要合理的管理内存大小,当内存不够时去申请合理的大小,当内存过大时,就调整内存。
C
//判断内存够不够
void SLCheckCapacity(SL* ps);
C
//判断内存够不够
void SLCheckCapacity(SL* ps)
{
if (ps->capecity == ps->size)
{
//第一种情况,都为0
//用三目运算符
int NewCapecity = ps->capecity == 0 ? 4 : ps->capecity * 2 * sizeof(SLDataType);
//第二种情况,空间不够
//开辟新空间,tmp来接受
SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapecity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
//空间申请成功
ps->arr = tmp;
ps->capecity = NewCapecity;
}
}
- 空间是顺序表的核心,空间主要分为两种情况:一种是没有空间,另一种是空间不够
- 第一种:用三目运算符判断,若没有空间,则给4个字节。若有空间,将字节乘2
- 第二种:当内存不够时,开辟新的空间
- 当上面两种情况完成后,用新的数据代替旧的数据
2.6头插和尾插
2.6.1尾插的实现
先把尾插的代码掌握好,后面的头插、头删、尾删就很好理解了
C
//尾插
void SLPushBack(SL* ps, SLDataType x);
C
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//尾插入
ps->arr[ps->size++] = x;
}
C
//尾插
SLPushBack(&sl, 1);
- 在写尾插时,每次输进一个新元素,size也要往后移一位
2.6.2头插的实现
相对于尾插,头插就相对简单很多了。也就是把元素往后移一位,然后再把新的元素插入开头。
C
//头插
void SLpushFront(SL* ps, SLDataType x);
C
//头插
void SLpushFront(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//往后各挪一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
C
//头插
SLpushFront(&sl, 5);
2.7关键知识讲解
2.7.1头插和尾插
- 动态内存核心:用realloc扩容,首次扩充到4个元素,之后翻倍,效率更高,避免频繁扩容
- 头插注意点:必须先把已有元素往后挪,从最后一个元素开始挪,不然会覆盖数据
- 内存安全:用完顺序表一点要释放内存,不然内存会泄漏
- 断言用法:assert(sp)防止传空指针,调试阶段很有用,正式项目可根据需求关闭
2.8尾删和头删
2.8.1尾删的实现
尾删就是把指向数组末尾的size,往前挪移了一位,将后面打印不到的舍弃
C
//删除最后一个元素
void SLPopBack(SL* ps);
C
//删除最后一个元素
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//有效元素减1,后面的空间相当于"废弃"
ps->size--;
}
C
//删除最后一个元素
SLPopBack(&sl);
2.8.2头删的实现
头删就是将所有元素往前挪移一位
C
//删除第一个元素
void SLPopFront(SL* ps);
C
//删除第一个元素
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);//防止空表删除,直接报错提醒!
//从第二个元素开始,依次往前挪移一位,覆盖第一个元素
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
//有效元素减1
ps->size--;
}
C
//删除第一个元素
SLPopFront(&sl);
2.9关键知识讲解
- 尾删为啥这么简单?
尾删不用动数据!只要把 size 减1,原来的尾元素就不在"有效元素范围"内了,下次插入会直接覆盖,效率超高! - 头删的坑千万别踩!
头删必须从前往后挪元素,要是从后往前挪,会把前面的有效数据覆盖掉!比如先挪 data[0] = data[1] ,再挪 data[1] = data[2] ,依次类推。 - 空表删除防护!
用 assert(sl->size > 0) 防止用户在空表时执行删操作,新手很容易忽略这个场景,一删就崩!实际项目中也可以用返回值提示错误,不用断言直接退出。 - 内存要不要释放?
这里不用手动释放删元素的内存!因为动态内存是按"容量"管理的, size 只是标记有效元素,扩容时才会调整内存大小,频繁释放小块内存反而效率低。
2.10任意插入和任意删除和查找
顺序表本质就是数组套了层"管理壳",就是数组的连续空间
2.10.1任意插入
插入的核心逻辑就一句话:先把插入位置后面的数据"往后挪一位",再把新数据塞进去。但必须先检查两件事:表满了没?位置合法不?
C
//插入指定位置
void SLInsert(SL* ps, int pos, SLDataType x);
C
//随机插入元素
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos <= ps->size && pos >= 0);
//插入数据:空间够不够
SLCheckCapacity(ps);
//让pos及之后的数据整体往后挪移1位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[pos + 1] = arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
C
//测试指定位置之前插入数据
SLInsert(&sl, 0, 99);
SLPrint(sl);//99 1 2 3 4
2.10.2任意删除
删除和插入反过来:先把要删的数据"记下来",再把后面的数据"往前挪一位",覆盖掉要删除的。同样要先检查:表空了没?位置合法不?
C
//删除指定位置
void SLErase(SL* ps, int pos);
C
//删除指定位置
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
SLCheckCapacity(ps);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//arr[size - 2] = arr[size - 1]
}
ps->size--;
}
C
//测试指定位置删除
SLErase(&sl, 0);
SLPrint(sl);//1 2 3 4
2.10.3元素查找
查找就简单了,从第一个数据开始逐个对比,找到就返回位置,没找到就说找不到。顺序表时"线性查找",效率一般,但是胜在简单。
C
//查找
int SLFind(SL* ps, SLDataType x);
C
//查找
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->arr[i])
{
//找到了
return i;
}
}
//没有找到
return -1;
}
C
//测试顺序表的查找
int find = SLFind(&sl, 4);
if (find < 0)
{
printf("没有找到!\n");
}
else
{
printf("找到了!下标为%d!!", find);
}
SLDestory(&sl);
2.11关键知识总结
- 核心就一个"挪"字
- 边界检查是"保命符"
- 下标别把'1'和'0'搞混
2.12销毁申请的空间
前面我们用realloc申请了我们想要的空间,当我们用完时,要记得把内存还回去
C
//销毁地址
void SLDestory(SL* ps);
C
//顺序表的销毁
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capecity = 0;
}
C
//销毁地址
SLDestory(&sl);
2.13完整代码1
SeqList.h头文件
C
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义顺序表的结构
//#define N 100
//
////静态顺序表
//struct SeqList
//{
// int arr[N];
// int size;//有效数组个数
//};
typedef int SLDataType;//方便后续类型的替换
//动态顺序表
typedef struct SeqList
{
int* arr;
int size;//有效数据个数
int capecity;//空间大小
}SL;
//typedef struct SeqList SL;
//判断内存够不够
void SLCheckCapacity(SL* ps);
//初始化
void SLInit(SL* ps);
//插入数据
void SLPushBack(SL* ps, SLDataType x);
//头插
void SLpushFront(SL* ps, SLDataType x);
//销毁地址
void SLDestory(SL* ps);
//打印顺序表
void SLPrint(SL ps);
//删除第一个元素
void SLPopFront(SL* ps);
//删除最后一个元素
void SLPopBack(SL* ps);
SeqList.c文件
C
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
//顺序表的初始化
void SLInit(SL* s)
{
s->arr = NULL;
s->size = s->capecity = 0;
}
//判断内存够不够
void SLCheckCapacity(SL* ps)
{
if (ps->capecity == ps->size)
{
//第一种情况,都为0
//用三目运算符
int NewCapecity = ps->capecity == 0 ? 4 : ps->capecity * 2 * sizeof(SLDataType);
//第二种情况,空间不够
//开辟新空间,tmp来接受
SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapecity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
//空间申请成功
ps->arr = tmp;
ps->capecity = NewCapecity;
}
}
//头插
void SLpushFront(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//往后各挪一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
//删除第一个元素
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);//防止空表删除,直接报错提醒!
//从第二个元素开始,依次往前挪移一位,覆盖第一个元素
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
//有效元素减1
ps->size--;
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//尾插入
ps->arr[ps->size++] = x;
}
//删除最后一个元素
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//有效元素减1,后面的空间相当于"废弃"
ps->size--;
}
//顺序表的销毁
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capecity = 0;
}
//打印顺序表
void SLPrint(SL sp)
{
for (int i = 0; i < sp.size; i++)
{
printf("%d ", sp.arr[i]);
}
printf("\n");
}
test.c文件
C
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
void SLTest01()
{
SL sl;
//初始化
SLInit(&sl);
//尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
//打印
SLPrint(sl);
//头插
SLpushFront(&sl, 5);
SLpushFront(&sl, 6);
//打印
SLPrint(sl);
//删除最后一个元素
SLPopBack(&sl);
SLPrint(sl);
//删除第一个元素
SLPopFront(&sl);
SLPrint(sl);
//销毁地址
SLDestory(&sl);
}
int main()
{
SLTest01();
return 0;
}
2.14完整代码2
SeqList.h文件
C
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义顺序表的结构
//#define N 100
//
////静态顺序表
//struct SeqList
//{
// int arr[N];
// int size;//有效数组个数
//};
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
int* arr;
int size;//有效数据个数
int capecity;//空间大小
}SL;
//typedef struct SeqList SL;
//判断内存够不够
void SLCheckCapacity(SL* ps);
//初始化
void SLInit(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//头插
void SLPushFront(SL* ps, SLDataType x);
//销毁地址
void SLDestory(SL* ps);
//打印顺序表
void SLPrint(SL ps);
//删除第一个元素
void SLPopFront(SL* ps);
//插入指定位置
void SLInsert(SL* ps, int pos, SLDataType x);
//删除指定位置
void SLErase(SL* ps, int pos);
//删除最后一个元素
void SLPopBack(SL* ps);
//查找
int SLFind(SL* ps, SLDataType x);
SeqList.c文件
C
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
//顺序表的初始化
void SLInit(SL* s)
{
s->arr = NULL;
s->size = s->capecity = 0;
}
//判断内存够不够
void SLCheckCapacity(SL* ps)
{
if (ps->capecity == ps->size)
{
//第一种情况,都为0
//用三目运算符
int NewCapecity = ps->capecity == 0 ? 4 : ps->capecity * 2 * sizeof(SLDataType);
//第二种情况,空间不够
//开辟新空间,tmp来接受
SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapecity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
//空间申请成功
ps->arr = tmp;
ps->capecity = NewCapecity;
}
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//往后各挪一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
//删除第一个元素
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);//防止空表删除,直接报错提醒!
//从第二个元素开始,依次往前挪移一位,覆盖第一个元素
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
//有效元素减1
ps->size--;
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//判断是否为空指针
assert(ps);
//判断内存够不够
SLCheckCapacity(ps);
//尾插入
ps->arr[ps->size++] = x;
}
//删除最后一个元素
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//有效元素减1,后面的空间相当于"废弃"
ps->size--;
}
//顺序表的销毁
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capecity = 0;
}
//打印顺序表
void SLPrint(SL sp)
{
for (int i = 0; i < sp.size; i++)
{
printf("%d ", sp.arr[i]);
}
printf("\n");
}
//随机插入元素
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos <= ps->size && pos >= 0);
//插入数据:空间够不够
SLCheckCapacity(ps);
//让pos及之后的数据整体往后挪移1位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[pos + 1] = arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
//删除指定位置
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
SLCheckCapacity(ps);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//arr[size - 2] = arr[size - 1]
}
ps->size--;
}
//查找
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->arr[i])
{
//找到了
return i;
}
}
//没有找到
return -1;
}
test.c文件
C
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
#if 0
void SLTest01()
{
SL sl;
//初始化
SLInit(&sl);
//尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
//打印
SLPrint(sl);//1234
//头插
SLPushFront(&sl, 5);
SLPushFront(&sl, 6);
//打印
SLPrint(sl);//561234
//删除最后一个元素
SLPopBack(&sl);
SLPrint(sl);//56123
//删除第一个元素
SLPopFront(&sl);
SLPrint(sl);//6123
//销毁地址
SLDestory(&sl);
}
#endif
void SLTest02()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPrint(sl);//1 2 3 4
//测试指定位置之前插入数据
SLInsert(&sl, 0, 99);
SLPrint(sl);//99 1 2 3 4
SLInsert(&sl, sl.size, 88);
SLPrint(sl);//99 1 2 3 4 88
SLInsert(&sl, 2, 77);
SLPrint(sl);//99 1 77 2 3 4 88
//测试指定位置删除
SLErase(&sl, 0);
SLPrint(sl);//1 77 2 3 4 88
//测试顺序表的查找
int find = SLFind(&sl, 4);
if (find < 0)
{
printf("没有找到!\n");
}
else
{
printf("找到了!下标为%d!!", find);
}
SLDestory(&sl);
}
int main()
{
//SLTest01();
SLTest02();
return 0;
}
3.顺序表的作用与运用场景
3.1顺序表的核心作用
- 动态管理数据:相比固定大小的数组,顺序表能通过扩容自动调整存储空间,解决了数组"存少了不够用、存多了浪费"的痛点,比如存储用户输入的不确定数量的数据。
- 高效随机访问:因为数据存在连续的动态数组里,能通过下标直接访问元素(时间复杂度 O(1)),比如要快速获取第 n 个元素,直接
sl->data[n]就能搞定,这是它的核心优势。 - 简化数据操作:把尾插、头删等常用操作封装成函数,后续使用时直接调用,不用重复写代码,比如编程竞赛中频繁需要添加/删除元素时,能节省大量时间。
3.2顺序表的典型运用场景
- 编程竞赛中的基础场景
- 简单数据存储与处理:比如题目要求读取一组整数,进行排序、去重、统计频率等操作,顺序表能轻松承载数据,配合算法完成需求。
- 模拟队列(尾插+头删):虽然顺序表头删效率不高(O(n)),但在数据量不大的竞赛题目中,用顺序表模拟简单队列能快速实现功能,不用复杂的数据结构。
- 实际开发中的应用
- 底层数据结构支撑:很多高级数据结构的底层会用到顺序表,比如栈(用尾插和尾删实现,效率 O(1))、动态数组(比如 C++ 的 vector、Java 的 ArrayList 底层逻辑和顺序表类似)。
- 数据缓存场景:比如系统中需要缓存最近访问的 100 条数据,用顺序表存储,满了之后删除头部元素(最早访问的),尾部添加新元素,简单高效。
- 批量数据处理:比如读取文件中的批量数据(如学生成绩、商品信息),先存入顺序表,再进行筛选、排序、汇总等操作,方便后续处理。
- 不适合用顺序表的场景(避坑提醒!)
- 频繁头插/头删且数据量大:头插/头删需要移动所有元素(O(n) 时间复杂度),如果数据量达到 10 万级,效率会极低,此时应该用链表。
- 数据元素大小不固定:顺序表适合存储相同类型的固定大小元素,比如 int、char,如果是结构体且大小动态变化,用顺序表会很麻烦。
- 需要频繁插入/删除到中间位置:比如在元素中间频繁添加或删除数据,每次都要移动大量元素,效率远不如链表。