hello,大家好,今天的内容是关于顺序表的,其实之前也发过文章,但是那个时候水平还是差了一点,有些地方不是很详细,这次会把每个点都讲清楚,也当给自己在复习一遍。
顺序表在本质上就是数组,顺序表是连续的,我们的数组在内存上也是连续存储的,所以我们可以认为他们在物理结构上是一样的。
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储
简单来讲其实线性表就是零个或者多个元素的有限序列。
线性表有链表和顺序表,我们今天来讲的就是最简单的顺序表。
2.顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
顺序表也可以分为动态顺序表和静态的顺序表,静态的意义不是很大,使用场景受到权限,所以我们这里一般用动态顺序表。我们今天讲的是动态的,但是也会提到静态是怎么个样子,会写动态的,静态的肯定也会。
如果我们要定义一个静态的话。
这个就是我们静态的顺序表的结构体,但是静态的是存在缺陷的,比如我们如果要存9个数据,这样就存不下来,如果我们这个N给的太大,我们假设这里有十万个数据,那我们这来存一万个数据的话,这就是铁铁的浪费,所以我们用动态开辟的方法来实现才是最好的。
动态写的方式
size就是我们有多少个数据,capacity就是有多是空间,我们这里在进行一些优化,比如我们要存储的是double的类型,我们这里需要改很多东西,结构体的名字太长了,我们也简化一下。
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;//指向动态开辟的空间
int size;//数据个数
int capacity;//容量
}SL;
这里引出一个问题,我们数据结构顺序表中每次开空间都是二倍的开吗?
答案:不是,二倍是一个比较合理的开辟内存大小的倍数,当然我们也可以1.5倍的开辟,数据结构中没有规定一次性开多少空间,只有合理和不合理之说。这里我们开空间就是以二倍的方式进行开辟。
完成了这个之后,我们得先写一个初始化,这个是必须的,我们后面也一定要写初始化,当然在C++中就可以不用,我们有我们的构造函数。
void SLInit(SL psl);
我们这样写初始化是对的吗,我们都知道,在C语言中,形参是实参的一份临时拷贝,改变形参是不会对实参进行改变的,我们要对外面的内容进行修改,就得用指针,所以我们需要传他们的地址。
那正确的写法就是void SLInit(SL* psl);
我们实现一下。
void SLInit(SL* psl)
{
psl->a = NULL;
psl->capacity = psl->size = 0;
}
我们初始化其实有两种方法,在初始化的时候就给空间,当然也可以在我们扩容的时候给空间,我们今天这里就不在初始化的时候给空间。
我们会开空间,也就代表我们还需要写一个函数来释放我们动态开辟的空间。
void SLDestory(SL* psl)
{
if (psl->a != NULL)
{
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
}
那像我们的通讯录是一样的我们对数据进行处理的话,需要增删查改,那这里顺序表也是一样,我们这里就先对顺序表进行尾插。
首先我们要思考,尾插是不是就是在数据的末尾插入一个空间,那我们需要考虑的问题最主要的就是我们的内存空间够吗,比如我们的capacity是4个空间容量大小,我们这个数据个数刚刚好就是4个,那我们还要在插入一个数据的话,是不是满了,这个时候动态顺序表的作用就出来了,我们在开辟二倍的空间进行插入,这个时候我们的空间容量就是8个,在插入的时候就是没有问题。
那我们在尾插的时候是不是就要写一个检查空间的函数。
void SLCheakCapacity(SL* psl)
{
if (psl->capacity == psl->size)
{
int NewCapacity = psl->capacity == 0 ? 4 :psl->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(psl->a, sizeof(SLDateType) * NewCapacity);
if (tmp == NULL)
{
perror("realloc fail\n");
exit(-1);
}
psl->a = tmp;
psl->capacity = NewCapacity;
}
}
这里我们其实要注意的有两点,一个就是我们在初始化函数的时候我们是没有进行空间开辟的,所以我们用一个三目操作符来进行判断,还有一个就是realloc的理解问题,大家这里可能多少有点疑问,首先我认为在这里使用realloc的原因一就是当我们的地址第一个参数为空的时候就是malloc,然后因为我们顺序表是连续开辟空间的,所以用realloc更好,realloc的特点是如果后面的空间是足够的,我们之间原地扩容,但是如果后面的空间是不够的,那这个时候我们就会采取另一种方式,异地扩容,找一个空间大的,然后扩容,并返回该地址,还会释放之前的空间,所以realloc更好。
那我们实现检查空间后,我们尾插是怎样的呢,我们都知道顺序表的本质就是数组,所以顺序表的尾插就可以相当于在数组末尾插入一个数据。
void SLPushBack(SL* psl, SLDateType x)
{
SLCheakCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
那我们可以在写一个printf的函数来看我们的结果,当然也可以通过其他调试窗口来看我们的结果,这里我们就简单一点,通过我们打印来看。
打印函数
void SLPrint(SL* psl)
{
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
测试代码
int main()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPrint(&s);
return 0;
}
测试结果
尾插写完之后,我们就马上来写我们的头插,头插就是在我们的数组下标为0的位置进行插入,那我们要保持后面的数据整体往后移动,整体移动的时间复杂度就是O(N),所以这里的问题就是头插的效率慢。那插入数据就要看空间是不是满足,所以扩容之前我们一定要检查空间是不是够的。
void SLPushFront(SL* psl, SLDateType x)
{
SLCheakCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[0] = x;
psl->size++;
}
我们再来测试一下看看结果和测试代码
测试代码
#include"Seqlist.h"
void test1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPrint(&s);
SLPushFront(&s, 10);
SLPushFront(&s, 20);
SLPushFront(&s, 30);
SLPushFront(&s, 40);
SLPrint(&s);
}
int main()
{
test1();
return 0;
}
我们后面马上来实现尾插是怎样的,尾插这里我们主要考虑两个问题,一个是删完了怎么办,还有一个就是删的这个位置怎么处理。
void SLPopBack(SL* psl)
{
assert(psl->size > 0);
psl->size--;
}
其实仔细想想就这一点代码,如果我们的元素只有0个的时候,我们就不能在对顺序表进行删除,所以我们这里要的就是断言一下,还有就是我们因为后面操作其实会覆盖,那我们就不需要考虑这么多,直接删除就完事了。
我们来测试看看。
测试代码
void test2()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPrint(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
}
int main()
{
//test1();
test2();
return 0;
}
那如果我们再删两个看看。
就被提醒了,所以这里断言我们就暴力一点,当然也可以温柔一点的给个if,
在这里小编推荐暴力一点的写法。
就直接用assert
这里我们可以看到其实我们尾删和尾插都是特别高效的,但是头插入和头删除,因为我们要移动数据,所以变得不是特别高效
下一个就是怎么实现我们得头删,首先啥都不管,我们先要考虑得就是我们不能为空得顺序表,一个数据都没有我们也就不要再继续删了,其次我们删除头部得数据,但是还有保持其他数据是按顺序的,所以这里我们就要考虑覆盖的问题。
void SLPopFront(SL* psl)
{
assert(psl->size > 0);
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
begin++;
}
psl->size--;
}
测试代码和结果
void test2()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPrint(&s);
SLPushFront(&s, 10);
SLPushFront(&s, 20);
SLPushFront(&s, 30);
SLPushFront(&s, 40);
SLPrint(&s);
SLPopFront(&s);
SLPopFront(&s);
SLPrint(&s);
}
int main()
{
//test1();
test2();
return 0;
}
下面我们还有一个随机插入和随机删除,这两个的插入删除我们就需要注意一点东西了,比如我们插入和删除是不是还是要保持顺序表是线性结构,是线性结构我们就需要他们保持连续,所以一些位置的删除和插入需要我们注意,还有就是我们还需要主要随机插入和删除覆盖数据的顺序,是从前覆盖还是从后覆盖,这都需要我们注意,那我们写写一个随机插入吧。
void SLInsert(SL* psl, int pos, SLDateType x)
{
assert(pos >= 0 && pos <= psl->size);
SLCheakCapacity(psl);
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos] = x;
psl->size++;
}
自信的男人不需要测试(这里大家还是需要不断进行调试的,因为如果我们后面出错的话很麻烦,如果你水平很高的话,当我没说,小编是弱鸡)
那我们这里也来测试和调试一下吧
测试代码
void test2()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPrint(&s);
SLPushFront(&s, 10);
SLPushFront(&s, 20);
SLPushFront(&s, 30);
SLPushFront(&s, 40);
SLPrint(&s);
SLPopFront(&s);
SLPopFront(&s);
SLPrint(&s);
SLInsert(&s, 1, 100);
SLInsert(&s, 1, 100);
SLInsert(&s, 0, 100);
SLPrint(&s);
}
还有一个就是随机删除。
void SLErase(SL* psl, int pos)
{
assert(pos >= 0 && pos < psl->size);
assert(psl->size > 0);
int begin = pos;
while (begin < psl->size - 1)
{
psl->a[begin] = psl->a[begin + 1];
begin++;
}
psl->size--;
}
随机删除就要看数据有没有,是不是顺序表为空,还要判断我们删除的位置是不是正确,我们不能在psl->size位置进行删除,这个大家可以通过画图来思考,下面我们在写一个查找,查找其实就是遍历数组.
int SLFind(SL* psl, SLDateType x)
{
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
找到了我们就返回当前的位置,没找到就返回-1。
那今天我们的学习就到这里了,我们后面接着干,我要补作业了家人们。