0. 前言
小伙伴们大家好,从今天开始,我们就开始学习《数据结构》这门课程~
首先想给大家讲讲什么是数据结构?
0.1 数据结构是什么?
数据结构是由“数据”和“结构”两词组合⽽来。
什么是数据?
比如常⻅的数值1、2、3、4..... ;平时我们学校教务系统⾥保存的用户信息(姓名、性别、年龄、学历等等)凡是我们在网页在肉眼可以看到的信息(⽂字、图⽚、视频等等),这些都是数据。
那何为结构呢?
当我们想要使⽤⼤量使⽤同⼀类型的数据时,通过⼿动定义⼤量的独⽴的变量对于程序来说,可读性⾮常差,我们可以借助数组这样的数据结构将⼤量的数据组织在⼀起,结构也可以理解为组织数据的⽅式。
就好像:
在草原上,想找到名叫“咩咩”的⽺很难,
但是如果我们从⽺圈⾥找到1号⽺就很简单,⽺圈这样的结构有效将⽺群组织起来。
概念:
数据结构是计算机存储、组织数据的⽅式。数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
总结:
1)能够存储数据(如顺序表、链表等结构)
2)存储的数据能够⽅便查找
0.2 为什么需要数据结构?
如图中所⽰,我们生活中无论去公共场所,或者在火车站买票等等场景,我们都需要排队。
如果不借助排队的⽅式来管理客户,会导致客户感受差、等待时间⻓等情况。
同理,程序中如果不对数据进⾏管理,可能会导致数据丢失、操作数据困难、野指针等情况。
通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏
增删改查等操作。
最基础的数据结构:数组。
我们思考一下,有了数组,为什么还要我们学习其他的数据结构呢?
比如你遇到这样一个问题:
假定数组有10个空间,已经使⽤了5个,向数组中插⼊数据步骤:
求数组的⻓度,求数组的有效数据个数,向下标为数据有效个数的位置插⼊数据(注意:这⾥是否要判断数组是否满了,满了还能继续插⼊吗).....
假设数据量⾮常庞⼤,频繁的获取数组有效数据个数会影响程序执⾏效率。
结论:最基础的数据结构能够提供的操作已经不能完全满⾜复杂算法实现。
🌟🌟有什么办法可以完成数组完不成的任务呢?
这就是我们要学习的第一个数据结构——顺序表
1、顺序表的概念及结构
1.1 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
2、顺序表分类
2.1 顺序表和数组的区别:
顺序表实质上就是对数组的封装,完成了对数组的增删改查的操作。
下面有一张图,可以帮助大家理解他俩的关系哦:
我们可以把数组和顺序表想象成两家餐厅,
一家是苍蝇馆子这样的普通餐厅,一家是米其林餐厅这样高档的五星级餐厅。
在普通餐厅能吃到炒西蓝花、玉米羹等等这样的菜,在高档的五星级餐厅也能吃到。
只不过五星级餐厅的厨师会把同样的菜做的更加细致,无论从食材的选择,料汁的调配,加上好看的摆盘,配上好听的名字,让这些菜变得更加档次。
所以简单来说,数组经过增加数据、删除数据、修改数据、查找数据等等的操作,摇身一变,就变成了顺序表。
2.2 顺序表分类:
顺序表分为静态顺序表和动态顺序表
2.2.1 静态顺序表
概念:使⽤定⻓数组存储元素
静态顺序表缺陷:空间给少了不够⽤,给多了造成空间浪费
2.2.2 动态顺序表
3. 接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
基本增删查改接口
//对数据管理 --- 增删查改
void SLInit(SL* ps); //初始化
void SLDestory(SL* ps); //释放
void SLPrint(SL* ps); //打印
void SLCheakCapacity(SL* ps); //检查容量 -- 扩容
//头插头删 尾插尾删
void SLPushBack(SL* ps, SLDateType x); //尾插
void SLPopBack(SL* ps); //尾删
void SLPushFront(SL* ps, SLDateType x);//头插
void SLPopFront(SL* ps); //头删
//返回下标,没找到返回-1
int SLFind(SL* ps, SLDateType); //查找元素,返回下标
//在pos位置插入x
void SLInsert(SL* ps, int pos, SLDateType x); //任意位置插入
//在pos位置删除x
void SLErase(SL* ps, int pos); //任意位置删除
void SLModify(SL* ps, int pos, SLDateType x);//修改
3.1 创建项目
由于在实际工程中,项目的实现都是采用模块化进行实现的。
所以在此处我也采用了模块化的方式进行实现。
3.2 定义动态顺序表结构
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;//存储数据的底层结构
int capacity;//记录顺序表的空间大小
int size;//记录顺序表当前有效的数据个数
}SL;
为了后续好修改类型数据,我们可以使用typedef将结构体类型struct SeqList 重新命名为SL。
在后续对顺序表操作中,为了用户更好的输入数据,一般我们会将输入数据的数据类型重命名为SLDataType。
采用typedef将其数据类型int重命名为SLDataType
3.3 初始化与销毁
函数声明:
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
函数实现:
//初始化顺序表
void SLInit((SL* ps)//传入链表地址便于修改
{
ps->arr= NULL;
ps->size = ps->capacity = 0;
}
动态顺序表是动态开辟的空间,结束时需要进行释放,避免造成内存泄漏
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
代码解读:
assert(ps):这是一个断言语句,用于检查 ps 是否为非空指针。如果ps为空,
程序会在运行时中断并报错。
if (ps->arr):检查 ps 结构体中的 arr 成员是否不为空。
free(ps->arr)`:如果 arr 不为空,使用 free 函数释放其占用的内存空间。
ps->arr = NULL:将 arr 成员指针设置为空,以避免悬空指针。
ps->capacity = ps->size = 0:将 capacity 和 size 成员都设置为 0。
3.4 顺序表容量检查
注意:
- 每当要增加数据时,都需要考虑空间是否使用完毕
- 如果使用完毕则需要考虑增容,增容为原来的两倍(避免频繁扩容)
- 增容后更新记录容量大小
注:这里我们考虑到有许多地方要检查是否增容,为了方便将它封装成一个函数
函数声明:
//扩容
void SLCheckCapacity(SL* ps);
函数实现:
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity *
sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
//扩容成功
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
代码解读:
判断结构体中的当前元素数量 size 是否等于容量 capacity 。如果相等,说明需要进行扩容操作。计算新的容量。如果当前容量为 0 ,则新容量设置为 4 ;否则新容量设置为当前容量的 2 倍。使用 realloc 函数尝试为数组重新分配内存,新的内存大小为新的容量乘以每个元素的大小。检查 realloc 是否成功。如果 realloc 失败(返回 NULL ),则:
打印错误信息。退出程序。如果扩容成功:更新结构体中的数组指针,使其指向新分配的内存。更新结构体中的容量值。
3.5 打印
函数声明:
//顺序表打印
void SLPrint(SL* ps);
上述函数定义完成后,我们通常需要测试打印以下相关数据,来判断相关函数定义是否成功.
代码实现:
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
3.6 尾插
函数声明:
//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x);
我们来分析一下:
尾插是在尾部插入一个数据。
但是在数据的尾部插入一个数据时,
我们需要考虑一个问题:原有空间是否可以容纳新的数据,是否需要扩容。
所以我们在插入数据时,要先调用 SLCheakCapacity函数来检查是否需要扩容。
代码实现:
//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x)
{
//对于顺序表为空,可以有两种判断方式
//①断言--粗暴的解决方式
//assert(ps != NULL);
assert(ps);
//②if判断--温柔的解决方式
//if (ps == NULL)
//{
// return;
//}
//空间不够,扩容
SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size++] = x;
}
3.7尾删
函数声明:
//顺序表的尾部删除
void SLPopBack(SL* ps);
我们来分析一下:
尾删:删除尾部最后的一个元素。
但尾删同样也要考虑一个问题,空间中是否还有数据给我们删除。
所以在进行尾删时,我们可以采用assert函数断言空间中还有数据。
代码实现:
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size >= 0);//断言空间中还有元素
ps->size--;//下标减1
}
在删除数据时,我们不用将原有数据删除。只需要下标减1即可。
原因在于我们时根据下标来使用数据的,当下标减1后,尾部最后一个数据便无法进行访问。
3.8 头插
函数声明:
//顺序表头部插⼊
void SLPushFront(SL* ps, SLDataType x);
我们来分析一下:
头插:在数据最开始地方插入数据。
比如,在0前面插入100
我们可以这样做:
同样,头插也要调用 SLCheakCapacity函数来检查空间是否足够,是否需要扩容。
代码实现:
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];//ps->arr[1] = ps->arr[0]
}
ps->arr[0] = x;
ps->size++;
}
3.9 头删
函数声明:
//顺序表的头部删除
void SLPopFront(SL* ps);
我们来分析一下:
头删:删除数据最开始的元素。
思路和头插类似,只要下标从1开始,所有数据依次向前移动1位,再把有限个数减1即可。
同时头删也需要使用assert函数断言原有空间中还有数据可以删除。
代码实现:
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);//空间中还有数据可以删除
//后面的数据往前挪动一位
for (int i = 0; i < ps->size; i++)
{
ps->arr[i-1] = ps->arr[i];//ps->arr[1] = ps->arr[0]
}
ps->size--;
}
3.10 指定位置之前插入数据
函数声明:
//指定位置之前插⼊
void SLInsert(SL* ps, int pos, SLDataType x);
我们分析一下:
看到插入两个字,我们就要考虑是否需要扩容。这一点很重要。还有我们要多pos这个参数进行判断,看是否在顺序表指定的范围中,因为顺序表是连续的,我们任意位置插入要合理,所以要对参数进行合理性判断:
assert(pos >= 0 && pos <= ps->size);
先给大家来画个图分析:
代码实现:
void SLInsert(SL* ps, int pos, SLDataType x)
{
//顺序表为空
assert(ps);
assert(pos >= 0 && pos <= ps->size);
// 空间够不够?是否扩容?
SLCheckCapacity(ps);
//pos及之后的数据往后挪动一位
for (int i = ps->size; i> pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
代码解读:
assert(ps) :确保 ps 指针非空。
assert(pos >= 0 && pos <= ps->size) :确保指定的插入位置 pos 是有效的,即在 0 到当前元素数量 ps->size 的范围内。
SLCheckCapacity(ps) :在插入元素之前,检查并可能进行容量的扩展,以确保有足够的空间来插入新元素。
循环部分:
通过 for 循环方式,将位置 pos 及之后的元素向后移动一位,为新元素腾出位置。
ps->arr[pos] = x :将新元素 x 插入到指定位置 pos 。
ps->size++:增加元素数量,表示成功插入了一个新元素。
3.11 删除任意位置数据
函数声明:
//删除指定位置数据
void SLErase(SL* ps, int pos);
【代码思路】:和插入任何位置数据思想类似。首先我们要检查输入下标pos是否合法。之后从输入下标开始,后一个元素拷贝到前一个元素空间。
代码实现:
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//pos以后的数据往前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
//ps->arr[i-2] = ps->arr[i-1];
}
ps->size--;
}
3.12 查找
函数声明:
//在顺序表中查找x
int SLFind(SL* ps, SLDataType x);
【代码思路】:要查找某个元素。由于这里只是最简单的查找,我们直接暴力查找,遍历整个数组返回下标即可。更为复杂的数据查找,会有更高阶的数据结构来实现。
代码实现:
int SLFind(SL* ps, SLDateType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->a[i])
return i;
}
return -1;
}
4. 所有代码
SeqList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//静态顺序表
//#define N 100
//typedef int SLDataType;
//
struct SeqList
{
SLDataType a[N];
int size;
};
//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;//存储数据的底层结构
int capacity;//记录顺序表的空间大小
int size;//记录顺序表当前有效的数据个数
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);//保证接口的一致性
//顺序表头部 尾部插⼊
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
//顺序表的头部 尾部删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
//指定位置之前插入数据
//删除指定位置数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
//在顺序表中查找x
int SLFind(SL* ps, SLDataType x);
SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
//扩容成功
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
//顺序表的头部/尾部插入
void SLPushBack(SL* ps, SLDataType x) {
//断言--粗暴的解决方式
//assert(ps != NULL);
assert(ps);
//if判断--温柔的解决方式
//if (ps == NULL) {
// return;
//}
//空间不够,扩容
SLCheckCapacity(ps);
//空间足够,直接插入
ps->arr[ps->size++] = x;
//ps->size++;
}
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];//ps->arr[1] = ps->arr[0]
}
ps->arr[0] = x;
ps->size++;
}
//顺序表的头部 尾部删除
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
ps->size--;
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
//后面的数据往前挪动一位
for (int i = 0; i < ps->size; i++)
{
ps->arr[i-1] = ps->arr[i];//ps->arr[1] = ps->arr[0]
}
ps->size--;
}
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
//指定位置之前插入数据
//删除指定位置数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
//pos及之后的数据往后挪动一位
for (int i = ps->size; i> pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
//pos以后的数据往前挪动一位
for (int i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
//ps->arr[i-2] = ps->arr[i-1];
}
ps->size--;
}
//在顺序表中查找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;
}
测试代码:Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLTest01()
{
SL sl;
SLInit(&sl);
//测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);// 1 2 3 4
SLPrint(&sl);
//SLPushBack(&sl, 5);
//SLPrint(&sl);
//头插
/*SLPushFront(&sl, 5);
SLPushFront(&sl, 6);
SLPushFront(&sl, 7);*///7 6 5 1 2 3 4
//SLPrint(&sl);
//尾删
//SLPopBack(&sl);
//SLPopBack(&sl);
//SLPopBack(&sl);
//SLPopBack(&sl);
//SLPopBack(&sl);
/*SLPopFront(&sl);
SLPopFront(&sl);
SLPrint(&sl);
SLPopFront(&sl);
SLPrint(&sl);*/
//指定位置插入
//SLInsert(&sl, 0, 100);
//SLPrint(&sl);//100 1 2 3 4
//SLInsert(&sl, sl.size, 200);
//SLPrint(&sl);//100 1 2 3 4 200
//SLInsert(&sl, 100, 300);
//SLPrint(&sl);//100 1 2 3 4 200
//SLErase(&sl, 0);
//SLPrint(&sl);
//SLErase(&sl, sl.size-1);
//SLPrint(&sl);
SLErase(&sl, 1);
SLPrint(&sl);
}
void SLTest02()
{
SL sl;
SLInit(&sl);
//测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);// 1 2 3 4
SLPrint(&sl);
//测试查找
int ret = SLFind(&sl, 30);
if (ret < 0) {
printf("数据不存在,查找失败!\n");
}
else
{
printf("数据找到了, 在下标为%d位置\n", ret);
}
}
void SLTest03()
{
SL sl;
SLInit(&sl);
//测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);// 1 2 3 4
SLPrint(&sl);
//测试销毁
SLDestroy(&sl);
SLPrint(&sl);
}
int main()
{
//SLTest01();
//SLTest02();
SLTest03();
return 0;
}
大家可以根据上述思路和提供的源码,自己练习哦~
后续数据结构的学习,大家需要多画图,多练习代码,这样在实现中能够得心应手~
那么本期博客就讲到这里,如果对你有所帮助~ 别忘了收藏点赞哦
有疑问的,可以随时在评论区骚扰我哟~