=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.com)
=========================================================================
接上期:
【数据结构初阶】一. 复杂度讲解_高高的胖子的博客-CSDN博客
=========================================================================
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,
常见的线性表:顺序表、链表、栈、队列、字符串...
顺序表示例:
线性表在逻辑上是线性结构,也就说是连续的一条直线。
但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
链表示例:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
顺序表概念及结构:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,
一般情况下采用数组存储,
在数组上完成数据的增删查改,
顺序表一般可以分为 静态顺序表 和 动态顺序表
静态顺序表:使用定长数组存储元素
因为静态顺序表使用定长数组存储元素,
对空间的运用不够灵活,可能造成空间浪费或不够的问题,
所以在实际情况下,静态顺序表并不常用(不够实用)示例:
动态顺序表:使用动态开辟的数组存储元素
静态顺序表只适用于确定知道需要存多少数据的场景。
静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态地分配空间大小
示例:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(详细解释在注释,代码分文件放最后)
数据结构可以管理数据,
通过 增删改查 等操作就可以实现管理数据
现在有了动态顺序表后,
就可以对其进行增删改查等操作来实现动态顺序表
--------------------------------------------------------------------------------------------------------------------------
SLInit函数 -- 顺序表初始化
--------------------------------------------------------------------------------------------------------------------------
SLDestroy函数 -- 顺序表销毁
--------------------------------------------------------------------------------------------------------------------------
SLPrint函数 -- 测试函数
--------------------------------------------------------------------------------------------------------------------------
SLPushBack函数 -- 将值插入顺序表尾部(尾插)
尾插函数SLPushBack测试:
--------------------------------------------------------------------------------------------------------------------------
上面尾插需要考虑空间不够进行扩容,
后面的头插同样也需要考虑,
所以可以在顺序表实现文件中设置一个内部函数SLCheckCapacity,
在需要的时候直接调用该函数进行扩容操作
↓↓↓↓↓
SLCheckCapacity内部函数 -- 进行扩容操作
--------------------------------------------------------------------------------------------------------------------------
SLPopBack函数 -- 将值从顺序表尾部删除(尾删)
尾删函数SLPopBack测试:
--------------------------------------------------------------------------------------------------------------------------
SLPushFront函数 -- 将值插入顺序表头部(头插)
头插函数SLPushFront测试:
--------------------------------------------------------------------------------------------------------------------------
SLPopFront函数 -- 将值从顺序表头部删除(头删)
头删函数SLPopFront测试:
--------------------------------------------------------------------------------------------------------------------------
SLInsert函数 -- 在指定位置(pos)插入想要的值(x)
指定添加函数SLInsert测试:
这里写了指定位置插入SLInsert函数后,
可以想到其实 头插 和 尾插 也是可以用 SLInsert函数 实现的,
所以可以在头插和尾插函数中复用 SLInsert函数 来减少代码量
↓↓↓↓↓
复用SLInsert函数 -- 改写头插函数SLPushFront
复用SLInsert函数 -- 改写尾插函数SLPushBack
--------------------------------------------------------------------------------------------------------------------------
上面SLInsert函数中涉及了插入位置pos下标,
有时在增删改查操作时,需要知道有效元素中某个元素的下标再进行操作,
所以我们可以定义一个函数SLFind来查找该元素的下标
↓↓↓↓↓
SLFind函数 -- 查找x这个值在顺序表中的下标是多少
查找函数SLFind测试:
--------------------------------------------------------------------------------------------------------------------------
SLErase函数 -- 删除指定位置(pos)的值
删除指定位置函数SLErase测试:
完成指定删除SLErase函数后,
头删 和 尾删 函数中也可以进行复用 SLErase函数 来减少代码量
↓↓↓↓↓
复用SLErase函数 -- 改写头删函数SLPopFront
复用SLErase函数 -- 改写尾删函数SLPopBack
--------------------------------------------------------------------------------------------------------------------------
SLModify函数 -- 把某个位置(pos)的值修改为某值(x)
修改函数SLModify测试:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SeqList.h -- 顺序表头文件
#pragma once 定义一个静态顺序表:使用定长数组存储元素 因为静态顺序表使用定长数组存储元素,对空间的运用不够灵活 所以在实际情况下,静态顺序表并不常用(不够实用) // //#define N 1000 //“表”的大小的值 //typedef int SLDataType; //定义顺序表中存储的类型,这里是int类型 // //struct SeqList //{ // SLDataType a[N]; //定义表的大小 // int size; //记录表中存储了多少个有效数据 //}; //将需要的头文件都包含在 SeqList.h 头文件中 #include
#include #include //定义一个动态顺序表:使用动态开辟的数组存储元素 typedef int SLDataType; //定义顺序表中存储的类型,这里是int类型 typedef struct SeqList { //定义一个顺序表类型的指针,指向动态开辟的数组 SLDataType* a; int size; //记录表中存储了多少个有效数据 int capactity; //容量空间的大小 }SL; //数据结构 --> 管理数据 --> 增删查改 //顺序表初始化 -- 头文件中声明 void SLInit(SL* ps); //顺序表销毁 -- 头文件中声明 //内存是动态开辟地,不销毁的话可能会导致内存泄漏) void SLDestroy(SL* ps); //写一个测试函数(声明),方便检查各步骤有没有问题: void SLPrint(SL* ps); //尾插(声明) -- 将值插入顺序表尾部: void SLPushBack(SL* ps, SLDataType x); //尾删(声明) -- 将值从顺序表尾部删除: void SLPopBack(SL* ps); //头插(声明) -- 将值插入顺序表头部: void SLPushFront(SL* ps, SLDataType x); //头删(声明) -- 将值从顺序表头部删除: void SLPopFront(SL* ps); //在指定位置(pos)插入想要的值(x) void SLInsert(SL* ps, int pos, SLDataType x); //查找x这个值在顺序表中的下标是多少: int SLFind(SL* ps, SLDataType x); //返回找到的下标 //删除指定位置(pos)的值: void SLErase(SL* ps, int pos); //把某个位置(pos)的值修改为某值(x) void SLModify(SL* ps, int pos, SLDataType x);
--------------------------------------------------------------------------------------------------------------------------
SeqList.c -- 顺序表实现文件
#define _CRT_SECURE_NO_WARNINGS 1 //包含我们写的 SeqList.h 头文件 #include "SeqList.h" //顺序表初始化 -- 实现 void SLInit(SL* ps) { //assert断言,防止接收空指针: assert(ps); //初始化顺序表类型指针 //初始化时要先开辟一些动态空间: //开辟的空间为顺序表类型,大小为4个顺序表类型的大小 ps->a = (SLDataType*)malloc(sizeof(SLDataType)*4); //顺序表作为一个独立的程序,之后可能会应用于其他程序, //所以要对开辟的动态空间进行检查: if (ps->a == NULL) { //可能开辟的空间过大 或 前面开辟太多空间不够再开辟 perror("malloc failed"); //打印错误 //让程序以异常的形式结束程序 //和 return 不同,return后主函数还会继续进行 exit(-1); } //将顺序表中的有效数据初始化为0: ps->size = 0; //将已动态开辟的容量空间初始化为4: ps->capactity = 4; } //顺序表销毁 -- 实现 //内存是动态开辟地,不销毁的话可能会导致内存泄漏) void SLDestroy(SL* ps) { //assert断言,防止接收空指针: assert(ps); //释放前面开辟的动态空间: free(ps->a); //将释放的指针置为空指针: ps->a = NULL; //将已开辟的动态空间置为0,顺序表有效数据也置为0 ps->capactity = ps->size = 0; } //写一个测试函数(实现),方便检查各步骤有没有问题: void SLPrint(SL* ps) { //assert断言,防止接收空指针: assert(ps); //打印动态顺序表现有的有效数据: for (int i = 0; i < ps->size; i++) { printf("%d ", ps->a[i]); } //打印完当前动态顺序表有效数据后进行换行: printf("\n"); } //在顺序表实现文件中设置一个内部函数SLCheckCapacity, //在需要的时候直接调用该函数进行扩容操作 void SLCheckCapacity(SL* ps) { //assert断言,防止接收空指针: assert(ps); //判断开辟的空间是否已满,满了再开辟: if (ps->size == ps->capactity) //顺序表有效个数 等于 已开辟容量空间 { //使用 realloc函数 进行扩容,每次扩容2倍: SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capactity * 2 * (sizeof(SLDataType))); // realloc 是把空间扩容到第二个参数的大小,而不是直接把空间扩容第二个参数的大小 //同样进行检验: if (tmp == NULL) { //打印错误: perror("realloc failed"); //出现错误直接终止程序: exit(-1); } //realloc函数,有两种扩容情况:一种是原地扩容,另一种是异地扩容 //原地扩容:扩容时看原空间后的内容有没有分配给别人,如果没有则把原空间后面的空间标记出来进行扩容,返回原空间的地址 //异地扩容:如果扩容时原空间后面的内容已被分配,则另找一个足够原空间扩容后的空间进行存放, // 原本的数据都搬到这个空间来,原空间会被释放,返回这个新空间的地址。 //所以使用 realloc函数 扩容后,不要对原空间指针进行释放, //如果realloc执行后是原地扩容返回原空间指针,扩容后后对原空间指针释放就会出问题 //把扩容返回的指针赋给原指针: ps->a = tmp; //将已动态开辟的容量空间置为原来的2倍: ps->capactity *= 2; } } //尾插(实现) -- 将值插入顺序表尾部: void SLPushBack(SL* ps, SLDataType x) { //assert断言,防止接收空指针: assert(ps); 调用内部函数进行扩容操作: //SLCheckCapacity(ps); 空间容量足够后将要插入尾部的值插入: //ps->a[ps->size] = x; 因为下标从0开始,所以size的值会等于现有元素下一位的下标 //ps->size++; //尾部插入元素后,更新顺序表中有效数据个数 //复用SLInsert函数 -- 改写尾插函数SLPushBack SLInsert(ps, ps->size, x); //在size位置插入就相当于尾插 } //尾删(实现) -- 将值从顺序表尾部删除: void SLPopBack(SL* ps) { //assert断言,防止接收空指针: assert(ps); 检查一下:如果有效数据size等于0,则不能继续删除了 防止越界,可能一直删除到空间外了导致越界 // 检查方法一:太“温柔”,返回后不知道自己错了 if (ps->size == 0) { return; //直接返回 即可 } // 检查方法二:使用断言,错误了会直接报错并给出错误信息 //assert(ps->size > 0); //顺序表中有效数据大于0才进行下面的操作 直接有效数据size--即可 尾插是也是用size插入的,所以下次添加时直接就覆盖了原来值 //ps->size--; 不能局部释放,不能说不要末尾的值就把末尾的这个空间直接释放掉 C++有个规定,malloc一次就要free一次,要free释放的话只能整体释放 顺序表的物理内存是连续的,释放空间是不能“分期”的 //复用SLErase函数 -- 改写尾删函数SLPopBack SLErase(ps, ps->size-1); //size-1 就是最末元素下标 } //头插(实现) -- 将值插入顺序表头部: void SLPushFront(SL* ps, SLDataType x) { //assert断言,防止接收空指针: assert(ps); 调用内部函数进行扩容操作: //SLCheckCapacity(ps); 将现有有效数据往后移一位,让出头位置: //int end = ps->size - 1; size-1 有效个数-1,就相当于当前最后一个元素的下标 //while (end >= 0) // //使用while循环循环移动各个有效元素,一直移到下标为0的元素 //{ // ps->a[end + 1] = ps->a[end]; //end下标元素 移到 下一位上 // --end; //改变下标 //} 将要插入头部的元素x插入头部: //ps->a[0] = x; 有效个数加一: //ps->size++; Capacity在扩容函数SLCheckCapacity中就修改好了 //复用SLInsert函数 -- 改写头插函数SLPushFront SLInsert(ps, 0, x); //在0位置插入就相当于头插 } //头删(实现) -- 将值从顺序表头部删除: void SLPopFront(SL* ps) { //assert断言,防止接收空指针: assert(ps); 因为要将头部的有效元素删除, 所以直接把第一个有效元素后面的其他元素往前移一位就行了, 覆盖掉第一个元素 先进行断言检查,防止没有元素还进行删除: //assert(ps->size > 0); //int begin = 1; //从下标为1的元素开始往前移,把下标为0的元素覆盖 //while (begin < ps->size) // //只要begin还小于有效元素个数就继续往前移 // //因为是从下标为1的元素开始移动, // //所以最后就只有下标为0的元素没动 //{ // //把当前begin下标的元素往前挪一位: // ps->a[begin - 1] = ps->a[begin]; // //当前begin下标元素移动后,begin++继续移动下一位: // ++begin; //} // 因为第一个元素被覆盖,所以有效元素size-- //ps->size--; //复用SLErase函数 -- 改写头删函数SLPopFront SLErase(ps, 0); //头元素下标是0 } //在指定位置(pos)插入想要的值(x) void SLInsert(SL* ps, int pos, SLDataType x) { //先使用assert检查pos是否合法: //在pos位置插入一个值后,顺序表还得是连续的 assert(pos >= 0 && pos <= ps->size); //pos等于0 -- 相等于头插 //pos等于size -- 相等于尾插 //使用SLCheckCapacity进行扩容操作: SLCheckCapacity(ps); //定义一个变量end,对应要移动元素的下标 int end = ps->size - 1; //一开始对应最后一个元素的下标 while (end >= pos) //只要end下标对应的元素还在pos下标元素的右边 //就把“end元素”向右移,移到pos下标无元素 { //把“end元素”向右移 ps->a[end + 1] = ps->a[end]; //移动下标进行写一个元素的移动 --end; } //让出位置后,将接收的x插入pos位置: ps->a[pos] = x; //有效元素+1: ps->size++; } //查找x这个值在顺序表中的下标是多少: int SLFind(SL* ps, SLDataType x) //返回找到的下标 { //assert断言,防止接收空指针: assert(ps); //数据量不大的话直接暴力查找吧: for (int i = 0; i < ps->size; i++) //有几个有效元素就循环几次: { if (ps->a[i] == x) //该下标元素等于要找的值x则返回当前下标: { return i; } } return -1; //表未找到该元素下标 } //删除指定位置(pos)的值: void SLErase(SL* ps, int pos) { //同样先使用assert检查pos是否合法: //这里检查条件pos不能像SLInsert一样等于size //因为size空了能插入(尾插),但不能删除 assert(pos >= 0 && pos < ps->size); //指定删除和头删的思路类似, //只要把pos后面的值往前覆盖,覆盖掉pos的值就好了: int begin = pos + 1; //从pos+1的位置往前挪 while (begin < ps->size) //一直移到size为止(不包括size位置) { ps->a[begin - 1] = ps->a[begin]; //往前挪 ++begin; //挪完继续挪下一个 } ps->size--; //覆盖掉pos位置的值后,有效数字减一 } //把pos位置的值修改为x void SLModify(SL* ps, int pos, SLDataType x) { //同样先使用assert断言检查pos是否合法: assert(pos >= 0 && pos < ps->size); //进行修改: ps->a[pos] = x; }
--------------------------------------------------------------------------------------------------------------------------
test.c -- 测试函数
#define _CRT_SECURE_NO_WARNINGS 1 //包含我们写的 SeqList.h 头文件 #include "SeqList.h" //分组测试,测试不同的函数--SLPushBack 和 SLPopBack 函数 void TestSeqList1() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试尾删函数SLPopBack: SLPopBack(&sl); SLPopBack(&sl); //调用测试函数SPrint查看是否“删除”成功: SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLPushFront函数 void TestSeqList2() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试头插函数SLPushFront: SLPushFront(&sl, 10); SLPushFront(&sl, 20); //调用测试函数SPrint查看是否“删除”成功: SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLPopFront函数 void TestSeqList3() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试头删函数SLPopFront: SLPopFront(&sl); SLPopFront(&sl); //调用测试函数SPrint查看是否“删除”成功: SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLInsert函数 void TestSeqList4() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试指定增加函数SLInsert: SLInsert(&sl, 2, 100); //调用测试函数SPrint查看是否“删除”成功: SLPrint(&sl); //int x; //scanf("%d", &x); //int pos = SLFind(&sl, x); //if (pos != -1) //{ // SLInsert(&sl, pos, x * 10); //} //SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLFind函数 void TestSeqList5() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试指定增加函数SLFind: int pos = SLFind(&sl, 2); printf("2在元素中是第%d个元素", pos+1); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLErase函数 void TestSeqList6() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); int x; scanf("%d", &x); //配合SLFind函数,找到顺序表中某个值的下标 int pos = SLFind(&sl, x); //再使用SLErase函数通过下标删除该值 if (pos != -1) { SLErase(&sl, pos); } SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //分组测试,测试不同的函数--SLModify函数 void TestSeqList7() { SL sl; //创建顺序表类型变量 //使用 SLInit函数 初始化顺序表类型变量 //注意传递的是变量的地址,防止形参改变实参不改变 SLInit(&sl); //使用尾插函数SLPushBack: SLPushBack(&sl, 1); SLPushBack(&sl, 2); SLPushBack(&sl, 3); SLPushBack(&sl, 4); //此时再调用会进行扩容: SLPushBack(&sl, 5); SLPushBack(&sl, 6); //调用测试函数SPrint查看是否插入成功: SLPrint(&sl); //测试指定增加函数SLInsert: SLModify(&sl, 2, 100); //调用测试函数SPrint查看是否“删除”成功: SLPrint(&sl); //测试完后使用SLDestroy函数销毁顺序表: SLDestroy(&sl); } //菜单: void menu() { printf("********************************\n"); printf("1、尾插 2、头插\n"); printf("3、头删 4、尾删\n"); printf("7、打印 -1、退出\n"); printf("********************************\n"); } int main() { //先创建一个顺序表: SL sl; //再对其进行初始化: SLInit(&sl); //创建一个变量接收菜单选项: int option = 0; do { //使用菜单: menu(); //打印提示信息: printf("请选择想要进行的操作的序号:>"); //接收序号: scanf("%d", &option); printf("\n"); if (option == 1) { printf("请依次输入你要插入的数据个数:>"); int n = 0; //接收数据个数 scanf("%d", &n); //接收数据个数 printf("\n"); printf("请依次输入你要插入的数据\n"); //知道数据个数后,直接使用for循环循环接收数据 int x = 0; for (int i = 0; i < n; i++) { scanf("%d", &x); SLPushBack(&sl, x); } } else if (option == 7) { SLPrint(&sl); } } while (option != -1); //最后销毁顺序表: SLDestroy(&sl); //TestSeqList1(); //TestSeqList2(); //TestSeqList3(); //TestSeqList4(); //TestSeqList5(); //TestSeqList6(); //TestSeqList7(); return 0; }