一、线性表
首先我们必须要了解的一个概念是线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表就是数组,他的内存是连续的
下面就是一个链表的结构,当然或许你现在并不知道链表是什么,但是没有关系, 我们在后面的文章中将会花费大量的笔墨去详细讲解链表,这里你只需要大概看一眼就可以了,下图什么都看不懂是没有任何关系的。
二、顺序表的实现(概念以及静态顺序表)
对于顺序表的实现,我们采用分文件的形式来展现,在前面我们也已经利用了两个小游戏来让大家熟悉了分文件的写法,这里就不在赘述。如果还有不懂的地方,可以去C语言专栏去找到对应的文章。同样数据结构部分,为了规范我们的写法,我们全部采用大驼峰的命名风格
1.创建三个工程文件
如下图所示,我们创建好三个文件,分别是SeqList.c 和SeqList.h 和Test.c
SeqList.c用于顺序表的实现
SeqList.h用于顺序表的声明
Test.c用于测试顺序表
2.顺序表的概念
我们得先了解顺序表的概念,才能继续完成下去
顺序表是一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改
顺序表一般可分为
静态顺序表:使用定长数组进行存储
动态顺序表:使用动态开辟的数组进行存储
3.顺序表的定义
我们先看静态顺序表是如何定义的,如下图所示,我们知道顺序表他也是用来存储数据的,既然是用来存储数据的,那么他必然要有一个数组来存储,然后还要有一个size来确定目前存储了一个数据。将这两个合并到一起就是一个结构体
虽然说这可以当作一个静态顺序表来进行定义,但是要真是这样定义,还是会出现很多问题,比如说哪一天想要存储100个数据了,那么我们上面这个就是写死了,根本没法改。所以我们可以使用define来进行优化,如下图所示
这样的话,确实,解决了一些后续无法增加数据的问题,但是如果突然不想要整型类型了,他想要char类型的话,那这个顺序表又废了,所以我们还得再次进行优化,我们可以使用一个typedef来重定义类型,如下图所示
当然这样其实还不够好,我们如果想要使用这个结构体的话,这个结构体类型太长了,我们写着写着都快烦死了。所以我们也可以对结构体进行typedef
如下图所示,我们让这个结构体类型变成了SL,当然这个有两种写法,一种是使用注释中的写法,另外一种就是在定义结构体的时候就typedef
最终代码如下(SeqList.h这个文件)
#pragma once #define N 10 typedef int SQDateType; typedef struct SeqList { SQDateType arr[N]; int size; }SL; //等价于typedef struct SeqList SL;
4.初始化顺序表
我们在说顺序表的概念的时候就提到过,顺序表是要实现增删查改四大功能的,但是在这之前,我们得先初始化顺序表
于是我们在头文件先进行一次初始化函数的声明
在SeqList.c文件中,我们就要实现这个功能了,如下图所示,我们使用一个memset函数,在这里可能有些人还不熟悉或者不太了解这个函数,我们可以现场学习一下 ,如下图所示,他是将传入的一个指针,从他开始的前num个字节都设置为value
所以下面中的意思就是将这个数组的全部元素设置为0,并且将size也就是目前的元素个数也设置为0
而对应这个函数也有他自己的头文件,我们为了方便,可以直接全部放在SeqList.h中
既然如此,那么我们就开始来测试一下这个函数究竟对不对
我们打完断点以后,直接使用F5,跳转至断点处,并打开调试的监视窗口,如下图所示,我们已经将顺序表给初始化完成了
我们开始进入我们的函数,我们发现,这块并没有将这个顺序表给初始化啊。这是为什么呢?
其实这块已经很多人发现了这个问题,因为我们传的其实是形参,形参的改变不会影响实参。我们就不该使用这个传值调用了,我们应该使用传址调用,我们现在将我们的代码都改为指针
而此时就能够完成我们的目标了,实现初始化顺序表
顺序表初始化接口代码为
//顺序表初始化 void SeqListInit(SL* ps) { memset(ps->arr, 0, sizeof(SQDateType) * N); ps->size = 0; }
5.静态顺序表的尾插
在我们将顺序表给初始化以后,我们现在需要的就是试下顺序的表的增删查改,当然我们在讲述顺序表的概念的时候,提到了这两个关键点:
1.连续的物理存储地址------数组
2.数据必须是从头开始,依次存储------不能在数组的第一个元素插入一个,然后第二个又跑到了第三个元素上
那么我们就先来定义一下我们的接口函数,如下图所示,这里我们的命名与c++的标准库一致,让我们的程序更加规范
不过既然有尾插,那么与之对应的就有头插,尾删,以及头删,不妨我们也直接定义好他们的接口吧,如下图所示
那么我们先来实现尾插,如何实现尾插呢?我们可以试着画图理解一下,如下图所示,我们假设数组已经存储了5个元素了,此时他的size就是5,那么这个5刚好就是下一个数据的下标,所以我们就想到了,有了下标了,那不就是直接插入数据了吗。插入完之后再让size++就行了吗,确实是这样的。
我们来实现一下我们的想法,如下图所示
但是这样存在一个问题,如果顺序表满了呢?我们可以实验一下
我们进行调试,如下图所示,顺序表已经初始化完成
我们使用F10不进入函数内部进行调试,我们可以发现现在即将越界
此时已经越界了,我们发现arr[10]他与size进行同步变化,size直接变成了12,而不是变成了11,这个问题,在我们之前讲解调试的时候已经提到过了,这就是数组越界所带来的风险。size的数据已经被破坏了。所以这个顺序表已经废了
那么我们该怎么办呢?我们可以在顺序表满的时候提示一句顺序表满了,然后就直接返回,直接拦截这个错误的信息,不让他插入进去,所以实现如下
这个时候我们再次调试一下,此时我们发现,拦截成功了。
其实到这块我们也发现一个问题,那就是静态顺序表不太好,要么消耗的空间太大,要么空间不够。总之很麻烦。而我们一般下也不会去使用静态的顺序表,我们都是使用动态的顺序表,可以进行扩容的顺序表。
所以我们在这里就不在继续进行实现静态的顺序表的,我们转而实现动态的顺序表