【数据结构】顺序表的实现
在前面,我们把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创建结构体
需要在头文件中创建结构体,并将结构体初始化
#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打印顺序表
当实现完前面顺序表的核心内容,剩下的就是打印顺序表和销毁顺序表了,我们先看如何打印顺序表
//打印顺序表
void SLPrint(SL ps);
//打印顺序表
void SLPrint(SL sp)
{for (int i = 0; i < sp.size; i++){printf("%d ", sp.arr[i]);}printf("\n");
}
//打印
SLPrint(sl);
SeqList.h文件
#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文件
#include"SeqList.h"//顺序表的初始化
void SLInit(SL* s)
{s->arr = NULL;s->size = s->capecity = 0;
}
test.c文件
#include"SeqList.h"
void SLTest01()
{SL sl;//初始化SLInit(&sl);
}int main()
{SLTest01();return 0;
}
2.5空间大小判断
利用动态内存分配,就要合理的管理内存大小,当内存不够时去申请合理的大小,当内存过大时,就调整内存。
//判断内存够不够
void SLCheckCapacity(SL* ps);
//判断内存够不够
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尾插的实现
先把尾插的代码掌握好,后面的头插、头删、尾删就很好理解了
//尾插
void SLPushBack(SL* ps, SLDataType x);
//尾插
void SLPushBack(SL* ps, SLDataType x)
{//判断是否为空指针assert(ps);//判断内存够不够SLCheckCapacity(ps);//尾插入ps->arr[ps->size++] = x;
}
//尾插
SLPushBack(&sl, 1);
- 在写尾插时,每次输进一个新元素,size也要往后移一位
2.6.2头插的实现
相对于尾插,头插就相对简单很多了。也就是把元素往后移一位,然后再把新的元素插入开头。
//头插
void SLpushFront(SL* ps, SLDataType x);
//头插
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++;
}
//头插
SLpushFront(&sl, 5);
2.7关键知识讲解
2.7.1头插和尾插
- 动态内存核心:用realloc扩容,首次扩充到4个元素,之后翻倍,效率更高,避免频繁扩容
- 头插注意点:必须先把已有元素往后挪,从最后一个元素开始挪,不然会覆盖数据
- 内存安全:用完顺序表一点要释放内存,不然内存会泄漏
- 断言用法:assert(sp)防止传空指针,调试阶段很有用,正式项目可根据需求关闭
2.8尾删和头删
2.8.1尾删的实现
尾删就是把指向数组末尾的size,往前挪移了一位,将后面打印不到的舍弃
//删除最后一个元素
void SLPopBack(SL* ps);
//删除最后一个元素
void SLPopBack(SL* ps)
{assert(ps);assert(ps->size > 0);//有效元素减1,后面的空间相当于“废弃”ps->size--;
}
//删除最后一个元素
SLPopBack(&sl);
2.8.2头删的实现
头删就是将所有元素往前挪移一位
//删除第一个元素
void SLPopFront(SL* ps);
//删除第一个元素
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];}//有效元素减1ps->size--;
}
//删除第一个元素
SLPopFront(&sl);
2.9关键知识讲解
- 尾删为啥这么简单?
尾删不用动数据!只要把 size 减1,原来的尾元素就不在“有效元素范围”内了,下次插入会直接覆盖,效率超高!
- 头删的坑千万别踩!
头删必须从前往后挪元素,要是从后往前挪,会把前面的有效数据覆盖掉!比如先挪 data[0] = data[1] ,再挪 data[1] = data[2] ,依次类推。
- 空表删除防护!
用 assert(sl->size > 0) 防止用户在空表时执行删操作,新手很容易忽略这个场景,一删就崩!实际项目中也可以用返回值提示错误,不用断言直接退出。
- 内存要不要释放?
这里不用手动释放删元素的内存!因为动态内存是按“容量”管理的, size 只是标记有效元素,扩容时才会调整内存大小,频繁释放小块内存反而效率低。
2.10任意插入和任意删除和查找
顺序表本质就是数组套了层“管理壳”,就是数组的连续空间
2.10.1任意插入
插入的核心逻辑就一句话:先把插入位置后面的数据“往后挪一位”,再把新数据塞进去。但必须先检查两件事:表满了没?位置合法不?
//插入指定位置
void SLInsert(SL* ps, int pos, SLDataType x);
//随机插入元素
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++;
}
//测试指定位置之前插入数据
SLInsert(&sl, 0, 99);
SLPrint(sl);//99 1 2 3 4
2.10.2任意删除
删除和插入反过来:先把要删的数据“记下来”,再把后面的数据“往前挪一位”,覆盖掉要删除的。同样要先检查:表空了没?位置合法不?
//删除指定位置
void SLErase(SL* ps, int pos);
//删除指定位置
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--;
}
//测试指定位置删除
SLErase(&sl, 0);
SLPrint(sl);//1 2 3 4
2.10.3元素查找
查找就简单了,从第一个数据开始逐个对比,找到就返回位置,没找到就说找不到。顺序表时“线性查找”,效率一般,但是胜在简单。
//查找
int SLFind(SL* ps, SLDataType x);
//查找
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;
}
//测试顺序表的查找
int find = SLFind(&sl, 4);
if (find < 0)
{printf("没有找到!\n");
}
else
{printf("找到了!下标为%d!!", find);
}SLDestory(&sl);
2.11关键知识总结
- 核心就一个“挪”字
- 边界检查是“保命符”
- 下标别把‘1’和‘0’搞混
2.12销毁申请的空间
前面我们用realloc申请了我们想要的空间,当我们用完时,要记得把内存还回去
//销毁地址
void SLDestory(SL* ps);
//顺序表的销毁
void SLDestory(SL* ps)
{if (ps->arr){free(ps->arr);}ps->arr = NULL;ps->size = ps->capecity = 0;
}
//销毁地址
SLDestory(&sl);
2.13完整代码1
SeqList.h头文件
#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文件
#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];}//有效元素减1ps->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文件
#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文件
#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文件
#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];}//有效元素减1ps->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文件
#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);
}
#endifvoid 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 4SLInsert(&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,如果是结构体且大小动态变化,用顺序表会很麻烦。
- 需要频繁插入/删除到中间位置:比如在元素中间频繁添加或删除数据,每次都要移动大量元素,效率远不如链表。
