了解数据结构:
数组是最基础的数据结构,但是最基础的数据结构能提供的操作已经不能完全满足复杂的算法实现,所以衍生出了多种多样的数据结构。
顺序表:
1.顺序表的概念与结构
顺序表是线性表的一种。
线性表:是n个具有相同特性的数据元素的有限序列。线性表一种在实际中广泛使用的数据结构,常见的线性表有顺序表,链表,栈,队列,字符串等。
线性表在逻辑上是线性结构,是一条连续的直线,但是物理结构上不一定连续。物理结构就是在计算机存储的空间不一定是连续的。(像顺序表就是开辟了一块连续的空间使用,而链表开辟的空间并不是连续的)。
2.顺序表的分类
顺序表的底层就是数组,它是对数组的分装,实现了常用的增删查改等接口。
顺序表有两个,静态顺序表和动态顺序表。
静态顺序表的意思就是使用一个定长的数组存储元素,也就是提供指定大小的空间。这个有明显的缺陷就是空间给少了不够用,空间给多了还会造成浪费。
动态顺序表就是不提供指定大小数组,而是根据提空的元素个数自己进行扩容操作。一看动态的就比静态的实用所以本文章模拟实现的是动态顺序表。
3.顺序表的实现
我们想要实现顺序表就需要一个基本的结构,我们就需要借助结构体来实现。为了使代码的函数声明与函数实现方便查看,seqlist.h文件放函数的声明,seqlist.c文件放函数的实现,text.c文件则是测试函数是否正常运行。
动态顺序表是按需申请的,所以结构体里面需要有一个数组(不需要提供大小),有效元素个数,以及开辟的空间容量。
//因为顺序表不仅仅能存储一种类型的元素,整形,浮点型,字符型等等。
//我们先使用最熟悉的整形实现,但是为了适配其他类型,方便代码更改。
//将int重命名一下,这样我们想使用其他数据类型将int修改整个代码就更改了
typedef int SLDataType;
struct SeqList
{
SLDataType* a;
int size;//有效元素个数
int capacity;//空间容量
};
这个是顺序表的基本结构,其中数组a是根据capacity开辟空间的大小,也就是根据元素个数开辟空间大小。
顺序表的功能总结就4个字,增删查改(还有别的功能但是,这4个是比较常用的)。我们还需要实现顺序表的初始化和销毁。
下面代码实现都是在seqlist.c当中,函数声明在.h里,上面提到过了。
3.1.初始化和销毁
我们仅仅是创建出了顺序表的结构但是我们需要初始化出来一个顺序表,当我们使用完顺序表后也需要销毁顺序表。
void SLInit(SL* pf)
{
pf->a = NULL;
pf->size = pf->capacity = 0;
}
为什么函数参数时SL*类型的呢?
如果我们使用SL类型,那么这种传参就是传值传参,但是我们知道形参的改变不能改变实参,所以这里需要传地址调用,直接上代码:
如果是传值调用会出现报错,未初始化内存ps。我们就需要传址调用。
通过调试观察传址调用初始化成功。
我们这个初始化是将数组a初始化为空,当然也可以先开辟一块空间,同时capacity也需要改变。方法不唯一。
顺序表的销毁:
void SLDestroy(SL* pf)
{
if (pf->a)
{
free(pf->a);
}
pf->a = NULL;
pf->size = pf->capacity = 0;
}
先判断数组a是否存在,存在就释放,然后置为空,之后将size,capacity置为0。具体free和为什么置为NULL可以看一下动态内存管理。
我们初始化出来一个顺序表了,就可以进行增删查改等操作了。
3.2.插入
顺序表的插入有头插尾插以及指定位置前插入,我们一一实现。
上面我们了解到顺序表的底层就是数组,所以这些操作就是数组的插入以及挪动数据。
3.2.1尾插
尾插是简单的插入,因为没有数据需要挪动。
上代码:
void SLPushBack(SL* pf, SLDataType x)
{
pf->a[pf->size] = x;
++pf->size;
}
直接在数组尾部插入,然后再让size+1(因为数组是从0开始存数据的,所以size是数组内有效元素的下一个位置)。在text.c测试一下。
运行会发现程序崩溃了,调试就会显示上面的错误。这是什么原因导致的呢?不难发现我们初始化了顺序表,但是我们并没有给a开辟空间,没有空间那怎么插入呢?
3.2.2开辟空间与扩容
顺序表如果没给空间就需要自己开辟一块空间,而且动态顺序表比静态顺序表的好处就是数据满了就扩容,所以插入就需要先看顺序表满没满,看是否需要扩容。那么什么时候代表着顺序表满了呢?
答案就是:当size == capacity,有效元素个数与开辟空间能装下元素的个数相等时就代表着顺序表满。
还有一个问题就是扩容一次应该扩多大?直接说结果,一般增容都是成倍数的增加,一般是2倍或3倍(实际是数学推理出来的,2倍或3倍合理,记住就好)。扩容的少了则频繁扩容,程序效率会降低。扩容大了就会造成空间的浪费。
上代码:
void SLCheckCapacity(SL* pf)
{
if (pf->size == pf->capacity)
{
//因为存在数组a为空的情况,我们需要判断数组进行的是开辟空间还是扩容。
//这种简单判断用三目表达式比较简单,如果capacity是0,则证明是需要开空间,则我们可以先给4个SLDataType
//大小的的空间,如果不为空,则需要2倍扩容。
// realloc()以及if(tmp==NULL)判断,想要了解,可以看动态内存管理(c语言)那篇文章
int newcapacity = pf->capacity == 0 ? 4 : 2 * pf->capacity;
SLDataType* tmp = (SLDataType*)realloc(pf->a, sizeof(SLDataType) * newcapacity);
if (tmp == NULL)
{
perror("SLCheckCapacity():realloc()");
exit(-1);
}
pf->a = tmp;
pf->capacity = newcapacity;
}
}
因为插入的方式有很多,我们每种插入都需要判断,所以我们直接写成一个函数。
那么加上这个函数,上面的尾插代码就能正常运行了吗?我们测试一下。
代码正常运行,调试发现插入成功。我们还需要测试一下函数是否能扩容。
当插入5个元素时实现了二倍扩容,而且元素也成功插入了顺序表内。
但是SLCheckCapacity()函数不够稳健,以为如果传过来的是个空指针,空指针是不能解引用操作的,程序就会崩溃,那么我们就需要判断一下。
只需要在if (pf->size == pf->capacity)上面添加一个断言。断言的介绍在C语言指针介绍第一篇当中
assert(ps);
完整的尾插代码是:
void SLPushBack(SL* pf, SLDataType x)
{
SLCheckCapacity(pf);
pf->a[pf->size] = x;
++pf->size;
}
3.2.3头插
头插要比尾插难度高一点,它需要挪动数据了。
想要头插一个数据则需要将数组所有数据往后挪一个位置,实现的方法有很多,可以自己写也可以按照本文方法写。
上代码:
void SLPushFront(SL* pf, SLDataType x)
{
SLCheckCapacity(pf);
for (int i = pf->size; i > 0; i--)
{
pf->a[i] = pf->a[i - 1];
}
pf->a[0] = x;
++pf->size;
}
3.2.4指定位置前插入
我们只需要将目标数字以及后面的数字往后挪一个位置即可。
上代码:
void SLInsert(SL* pf, int pos, SLDataType x)
{
//判断一下插入位置是否合法
assert(pos >= 0 && pos <= pf->size);
SLCheckCapacity(pf);
for (int i = pf->size; i > pos; --i)
{
pf->a[i] = pf->a[i - 1];
}
pf->a[pos] = x;
++pf->size;
}
运行调试一下:
达到了我们想要的目的,但是测试一个数据太单一,而且测试也需要测试特殊情况。这个函数有两个位置需要测试,头和尾 。大家可以测试以下,代码是可以正常运行的。
3.2.5顺序表的打印
上面我们观察顺序表的内容都是通过调试的方法, 是不太方便的,所以我们直接写一个打印顺序表的函数,十分简单。
上代码:
void SLPrint(SL* pf)
{
for (int i = 0; i < pf->size; i++)
{
printf("%d ", pf->a[i]);
}
}
其实就是一个数组打印函数。往下介绍,不多赘述了。
3.3.删除
3.3.1尾删
这个嘎嘎简单,想一想是不是只需要将size往前挪一个位置,就完成了尾删。代码也十分简单。
void SLPopBack(SL* pf)
{
//判断表内是否还有元素,没有元素就报错
assert(pf->size);
--pf->size;
}
3.3.2头删
头删只需要将除第一个元素外所有元素整体往前挪动一个位置就可以完成。
void SLPopFront(SL* pf)
{
assert(pf->size);
for (int i = 0; i < pf->size; i++)
{
pf->a[i] = pf->a[i + 1];
}
--pf->size;
}
3.3.3指定位置前删除
只需要将pos位置元素以及后面元素整体往前挪一个位置就可以完成 指定位置之前删除。
上代码:
void SLErase(SL* pf, int pos)
{
assert(pos >= 0 && pos <= pf->size);
for (int i = pos; i < pf->size; i++)
{
pf->a[i] = pf->a[i + 1];
}
--pf->size;
}
实现头删尾删都是能正常运行的。
3.4.查找
查找顺序表中是否有符合元素,有返回true,没有返回false。顺序表底层是数组,查找就是遍历一遍数组看目标元素是否存在。
bool SLFind(SL* pf, SLDataType x)
{
for (int i = 0; i < pf->size; i++)
{
if (pf->a[i] == x)
return true;
}
return false;
}
本文章使用的是int类型,可以使用==比较,其他类型则使用适用该类型的比较。
4.结尾
顺序表的基本功能都实现完成了,如果有其他想要实现的功能,大家可以自己尝试,上面的代码如果是初学,还是希望大家能够自己独立打一次代码,明白每一个函数的思路以及如何实现。各位未来大佬们下片文章再见。
5.整体代码
.h文件中:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL;
void SLInit(SL* pf);
void SLDestroy(SL* pf);
void SLCheckCapacity(SL* pf);
void SLPushBack(SL* pf, SLDataType x);
void SLPushFront(SL* pf, SLDataType x);
void SLInsert(SL* pf, int pos, SLDataType x);
void SLPrint(SL* pf);
void SLPopBack(SL* pf);
void SLPopFront(SL* pf);
void SLErase(SL* pf, int pos);
bool SLFind(SL* pf, SLDataType x);
seqlist.c文件中:
#include"seqlist.h"
void SLInit(SL* pf)
{
pf->a = NULL;
pf->size = pf->capacity = 0;
}
void SLDestroy(SL* pf)
{
if (pf->a)
{
free(pf->a);
}
pf->a = NULL;
pf->size = pf->capacity = 0;
}
void SLCheckCapacity(SL* pf)
{
assert(pf);
if (pf->size == pf->capacity)
{
//因为存在数组a为空的情况,我们需要判断数组进行的是开辟空间还是扩容。
//这种简单判断用三目表达式比较简单,如果capacity是0,则证明是需要开空间,则我们可以先给4个SLDataType
//大小的的空间,如果不为空,则需要2倍扩容。
// realloc()以及if(tmp==NULL)判断,想要了解,可以看动态内存管理(c语言)那篇文章
int newcapacity = pf->capacity == 0 ? 4 : 2 * pf->capacity;
SLDataType* tmp = (SLDataType*)realloc(pf->a, sizeof(SLDataType) * newcapacity);
if (tmp == NULL)
{
perror("SLCheckCapacity():realloc()");
exit(-1);
}
pf->a = tmp;
pf->capacity = newcapacity;
}
}
void SLPushBack(SL* pf, SLDataType x)
{
SLCheckCapacity(pf);
pf->a[pf->size] = x;
++pf->size;
}
void SLPushFront(SL* pf, SLDataType x)
{
SLCheckCapacity(pf);
for (int i = pf->size; i > 0; i--)
{
pf->a[i] = pf->a[i - 1];
}
pf->a[0] = x;
++pf->size;
}
void SLInsert(SL* pf, int pos, SLDataType x)
{
assert(pos >= 0 && pos <= pf->size);
SLCheckCapacity(pf);
for (int i = pf->size; i > pos; --i)
{
pf->a[i] = pf->a[i - 1];
}
pf->a[pos] = x;
++pf->size;
}
void SLPrint(SL* pf)
{
for (int i = 0; i < pf->size; i++)
{
printf("%d ", pf->a[i]);
}
}
void SLPopBack(SL* pf)
{
assert(pf->size);
--pf->size;
}
void SLPopFront(SL* pf)
{
assert(pf->size);
for (int i = 0; i < pf->size; i++)
{
pf->a[i] = pf->a[i + 1];
}
--pf->size;
}
void SLErase(SL* pf, int pos)
{
assert(pos >= 0 && pos <= pf->size);
for (int i = pos; i < pf->size; i++)
{
pf->a[i] = pf->a[i + 1];
}
--pf->size;
}
bool SLFind(SL* pf, SLDataType x)
{
for (int i = 0; i < pf->size; i++)
{
if (pf->a[i] == x)
return true;
}
return false;
}
text.c文件中:
#include"seqlist.h"
int main()
{
SL ps;
SLInit(&ps);
SLPushBack(&ps, 1);
SLPushBack(&ps, 2);
SLPushBack(&ps, 3);
SLPushBack(&ps, 4);
SLPushBack(&ps, 5);
SLPrint(&ps);
SLDestroy(&ps);
return 0;
}
其他函数可以自己下来尝试。