前言
Hello, 小伙伴们,好久不见,今天作者菌奖紧接上期的内容带大家继续学习链表,上期我们简单介绍了一些链表的概念,这期我们着手于实现链表。
好废话不多说,我们开始今天的内容:
1.顺序表的创建
上期我们提到了顺序表有两种,分别是动态顺序表和静态顺序表,其中我们在实际应用时一般会使用动态顺序表。
为什么呢?
我们先来看看两种顺序表的结构:
可以了解到静态顺序表的数组是定长的,在应用的看法阶段很难预测一款应用的用户使用情况,所以在开始研发的阶段使用静态链表,无论是空间给的太多还是空间给的太少,多有可能造成不可估量的损失!!
所以为了能够灵活地控制空间的使用情况,我们通常都会使用动态链表。
ypedef int SeqListDataType;//定义存放数据的数据的类型
typedef struct SeqList
{
SeqListDataType* arr;
int size;//有效的数据
int capacity;//可以申请的空间容量
}SeqList;//定义动态链表
在这里我们在链表中定义三种数据:
SeqListDataType* arr----> 用于表示存放数据的指针;
问:在这里我们为什么还要多次一举,将int定义为 SeqListDataType呢?
因为我们后续想要存放的数据可能不是int类型而是char 或是其他结构类型的数据,若我们现在在链表中直接使用int, 万一后面我们接到要求要将后面的存储数据改变为“”char“”类型,此时我们就会面对一个十分繁琐的问题。
所以,我们将这里的int,定义一个新的名称,在想要修改存储的数据类型时,只需要修改1行代码就可以解决问题!!!
总结:使代码的更易于修改,使用场景更多样!
在开始时,我们需要创建三个文件
在SeqList.h文件中,我们会进行各种函数的申明,实现各种各样的功能,以及包含各种头文件使写代码的郭晨会更加的流畅,省去许多的冗余步骤:
比如进行链表的初始化使时:
在SeqList.h中申明
这样操作后,在 其他文件中使用到这个函数时,就只需要包含这一个头文件
#include"SeqList.h"
接下来在进行链表初始化函数的实现和功能测试室都只需要这样就行了!!
2.顺序表的初始化
void SeqListInit(SeqList* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
我们为什么要这样初始化我们的链表呢?
首先我们看SeqListInit的定义
viod SeqListInit (SeqList* ps);
在最开始的时候我们链表中没有数据,也还没有开始申请空间,所以这时链表节点的指向空,且有效的数据为0,可使用的空间大小也为0.
接下来我们可以来测试一下这个函数的功能
通过调试可以看出 ,函数达到了我们想要的功能;
3.顺序表的销毁
在函数中涉及到改变实参的值,所以这里进行传地址的操作。
void SeqListDestroy(SeqList* ps);
void SeqListDestroy(SeqList* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->capacity = ps->size = 0;
}
销毁的前提是顺序表中的数据存放个数不为0!
4.顺序表的数据插入
4.1数据的尾插
首先我们先看看尾插函数的申明:
//数据的尾插
void SeqLitsPushBack(SeqList* ps, SeqListDataType x);
首先在函数中还是会涉及到改变实参的值,所以还是进行传址操作。
void BuyCapacity(SeqList* ps)
{
assert(ps);
//申请空间
//增容是用哪个函数呢? malloc calloc realloc 而这三个函数只有realloc函数右增容的概念
int NewCapacity = ps->capacity == 0 ? 8 : 2 * ps->size;
SeqList* temp = (SeqList*)realloc( ps->arr,NewCapacity*sizeof(SeqListDataType));
if (temp == NULL)
{
perror("BuyCapacity: realloc:");
exit(1);
}
ps->arr = temp;
ps->capacity = NewCapacity;
}
void SeqLitsPushBack(SeqList *ps, SeqListDataType x)
{
//在插入数据之前一定要保证空间的容量足够
assert(ps);
if (ps->capacity == ps->size)
BuyCapacity(ps);
ps->arr[ps->size++] = x;
}
我们来仔细的分析一下这个函数,首先:
进行尾插的前提就是顺序表中有足够的空间能支持数据的插入!!
这里我就专门创建了一个函数来进行增容的操作:
这个函数在其他的插入操作中都可以用到!!
void BuyCapacity(SeqList* ps)
当顺序表的有效数据和顺序表的可用空间相等时(ps->capacity == ps->size),顺序表就不再有足够的空间来支持数据的插入操作,就需要增容 。
首先我们要想进行动态的增容操作,就要足够了解我们经常会用到的三个的动态内存申请空间的函数: malloc realloc calloc
而这三个函数只有realloc函数有增容的概念!!
int NewCapacity = ps->capacity == 0 ? 8 : 2 * ps->size;
这行代码就是用于判断是否初始状态下的增容(在初始状态下capacity 和 size都是0),在初始状态下,我们可以 直接给空间的容量赋为8个存储数据的空间大小,如果不是,就让空间容量的申请成顺序表有效数据个数的两倍增加(经数学理论推出,感兴趣的小伙伴可以移步百度搜索)。
SeqList* temp = (SeqList*)realloc( ps->arr,NewCapacity*sizeof(SeqListDataType));
if (temp == NULL)
{
perror("BuyCapacity: realloc:");
exit(1);
}
这一步就是正式的空间申请操作,当申请空间成功后,我们再将申请空间的地址赋给ps->arr
ps->arr = temp;
ps->capacity = NewCapacity;
最后赋值。
然后我们通过调试来看看效果:
此时我们可以看到这里的函数实现了我们需要的功能!!
4.2数据的前插
接下来我们来实现数据在顺序表中的前插:
先看该函数的定义声明
//数据的头插
void SeqListPushFront(SeqList* ps, SeqListDataType x);
再来看看函数的是实现代码:
void SeqListPushFront(SeqList* ps, SeqListDataType x)
{
BuyCapacity(ps);
//先让顺序表的数据整体向后移动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
要实现在顺序表中的前插,我们要怎样做呢?
所以只有采取从后·往前移动数据才能达到目的!!
为了你能够直观的观察到函数的效果,我们先来实现一下顺序表的打印
4.3顺序表的打印
//打印数据
void SeqListPrint(SeqList ps);
因为顺序表的打印并没有涉及到实参值得改变,我们就只需要进行传值操作即可:
void SeqListPrint(SeqList ps)
{
for (int i = 0; i < ps.size; i++)
{
printf("%d ", ps.arr[i]);
}
printf("\n");
}
接下来我们就可以直观的看看顺序表中的数据情况:
5.顺序表数据的删除
5.1数据的头删
函数的申明:
//头删
void SeqListPopFront(SeqList* ps);
函数的实现:
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size);//删除数据的前提是顺序表中有有效的数据可删除
//头删就可以理解为顺序表中的所有数据都向前移动一个位置
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
理解了前面的知识,函数的头删就很简单了,就是从顺序表的第二个数据开始,所有的数据向前移一位
我们来看看测试的效果:
5.2数据的尾删
函数的申明:
//尾删
void SeqListPopBack(SeqList* ps);
函数的实现:
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size);
ps->size--;
}
尾删函数就跟呢个简单了,只要将他排除在有效数据之外就行,什么意思呢?
比如:
这样在打印数据时,就访问不到原来顺序表的数据了,后面增减数据的 操作时也会直接讲将排除在有效数据范围的数据覆盖掉,不会对顺序表的使用产生影响!
我们来测试一下尾删 :
OK,讲完了这些,我们再来讲讲如何在指定的位置增加数据和删除数据
6.指定的位置增加数据和删除数据
//在指定的之前位置插入数据
void SeqListInsert(SeqList* ps, int pos, SeqListDataType x);//在指定位置之前插入数据
void SeqListErase(SeqList* ps, int pos);//在指定位置删除数据
int SeqListFind(SeqList* ps, SeqListDataType x);//查找数据
首先我们来实现数据的在制定位置之前的插入
6.1数据在指定位置之前的插入
函数申明:
assert(ps && pos <= ps->size)
void SeqListInsert(SeqList* ps, int pos, SeqListDataType x);//在指定位置之前插入数据
注:pos指的是顺序表指定的数组下标。
函数的实现:
void SeqListInsert(SeqList* ps, int pos, SeqListDataType x)//在指定位置之前插入数据
{
assert(ps && pos <= ps->size && pos >= 0);
//照旧再插入数据之前,我们还是要保证顺序白哦中有足够的空间供我们插入数据
BuyCapacity(ps);
//在指定的位置之前插入数据就是要将pos及以后的数据往后移一位
for (int i = ps->size; i; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
}
这里解释一下这条语句:
assert(ps && pos <= ps->size)
这里的ps->size必须大于等于0,且小于等于当前顺序表的有效数据个数。
注:当pos=ps->size时,就相当于尾插
写完代码,我们来测验一下:
这里我近似采用了一个 头插的方法:
6.2 指定位置数据的删除
函数的申明:
void SeqListErase(SeqList* ps, int pos);//在指定位置删除数据
函数的实现:
void SeqListErase(SeqList* ps, int pos)//在指定位置删除数据
{
assert(ps&& pos < ps->size && pos >= 0);
//删除指定位置的数据就相当于让pos之后的数据集体向前移一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
删除指定位置的数据就是只需要将pos之后所有的数据都向前移一位,这样就能将原本pos位置的数据覆盖掉。
我们来看看效果如何:
最后我们来实现指定数据位置的查找
6.3 指定数据位置的查找
函数的定义:
int SeqListFind(SeqList* ps, SeqListDataType x);//查找数据
在拉看看函数的实现:
int SeqListFind(SeqList* ps, SeqListDataType x)//查找数据
{
assert(ps && ps->size);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
printf("所查找的数据不存在!!!\n");
return -1;
}
这里我们就只需要进行顺序表的遍历就行。
结语:
好,相信通过顺序表的实现大家一定已经收获了许多的东西,下一次我会为大家讲解顺序表的应用,喜欢的小伙伴一定不要错过哟,好,咱们下期再见,拜拜!!!