目录
⭐️ (5)、有了尾插自然就有尾删法:顾名思义就是在尾部删除数据代码实现如下:
🍁2.1 线性表的介绍
🌕2.1.1、线性表的定义
(1)、线性表是n(n>=0)个具有相同特性的数据元素的有限序列,其中n表示线性表的长度,即数据元素个数。如下图:
🌕2.1.2、线性表的相关性质
(1)、n=0时表示空表,n>0时通常记为(a1,a2,a3......an),a1表示线性表首元素,an表示尾元素
(2)、其中,a(m-1)是a(m)的直接前驱,a(m)是a(m-1)的直接后继。
(3)、除了a1只有唯一后后继,an只有唯一前驱外,其余元素均只有唯一的前驱和后继。
(4)、线性表的逻辑结构示意图:
(5)、每个数据元素的具体含义在不同的线性表中各不相同,它可以是一个数,或是一个符号,也可以是一个记录,甚至是其他更为复杂的信息。
🍁2.2 线性表的顺序存储结构——顺序表
🌕2.2.1、顺序表的概念
(1)、在内存中开辟一段连续的存储空间,用一组连续的存储单元依次存放数据元素,这种存储方式叫做线性表的顺序存储结构,简称顺序表。一般用数组存储,在数组上完成数据的增删查找等操作。
(2)、顺序存储结构的特点:在逻辑上相邻的数据元素,它们的物理位置也是相邻的。
(3)、由于线性表中的数据元素具有相同的特性,所以很容易确定表中第i个元素的存储地址,
假设表中每个元素占用m个存储单元,并以所占的第一个存储单元的存储地址作为数据元素的存储地址,则表中第i个数据的存储位置是:
LOC(ai)=LOC(a1)+(i-1)*m
其中: LOC(a1)是表的第一个数据元素a1的存储位置,通常称为线性表的起始位置或基地址.
注:因为线性表只要确定了基地址和一个数据元素的大小m,那么表中任一元素的存储地址都可根据上述公式计算,这样就可以随机存取顺序表中的任意元素。因此,线性表的顺序存储结构是一种随机存取的存储结构。
(4)、顺序表与数组的区别:数组是一片连续空间,但可以随便在某一空间放值,但顺序表不同,顺序表也是一片连续的空间,但存放数据必须依次存放。
🌕2.2.2、顺序表的优缺点
(1)、优点:
①:随机存取元素容易实现,根据上述定位公式容易确定表中每个元素的存储位置,所以要指定i个结点很方便。
②:简单、直观。
(2)、缺点:
①:插入和删除结点困难:由于表中的结点是依次连续存放的,所以插入和删除一个结点是时,必须将插入点以后的结点依次向后移动,或者删除点以后的结点依次向前移动。②:扩展不灵活(静态):建立表时,若估计不到表的最大长度,就难以确定的分配空间,影响扩展。
③:容易造成浪费:分配空间过大时,会造成预留空间浪费。
🌕2.2.3、顺序表的分类及代码定义(静态和动态)
顺序表一般分为两类,如下:
(1)、静态顺序表:
代码定义如下:
#define N 100 typedef int SLDatatype; struct Seqlist { SLDatatype q[N];//开辟的连续空间 int size;//有效数据个数 };
解释:
①:N为所开辟的总空间,为使之方便改变,所以用define宏定义。
②:为使方便存放不同类型的数据,所以用了typedef对类型重命名。
③:数组q是所开辟的一份连续空间。
④:size为有效数据个数,即当前表中所存在的元素个数。
注:静态顺序表有几个缺点:像静态顺序表这样定义就写死了,N给100可能不够用。N给200又可能只用到10,严重浪费空间。所以我们一般使用下面一种定义方式——动态顺序表。
(2)、动态顺序表:
代码定义如下:
typedef int SLDatatype; typedef struct Seqlist { SLDatatype* q;//指向动态开辟的数组 int size;//有效数据个数 int capicity;//容量空间大小 }SL;
解释:
①:类型重命名同上。
②:这里定义的指针q是用来指向动态开辟的数组
③:size是有效数据个数,即顺序表现有的数据个数。
④:capicity是总容量空间大小,一是记录当前最大空间,二是方便后续扩充空间。
🌕2.2.4、顺序表的基本操作(重点)
⭐️ (1)、初始化函数
代码实现如下:
void SLInit(SL* ps) { ps->capicity = 5; ps->size = 0; ps->q = (SLDatatype*)malloc(sizeof(SLDatatype) * 5); if (ps->q == NULL) { perror("malloc failed"); exit(-1); } }
解释:
①:形参是结构体指针,用于接收结构体对象的地址,因为形参是实参的临时拷贝,所以我们整个操作都是传址调用。
②:初始化为空表,所以size置0,capicity初始化应该和初始开辟的空间等大,因为需要用于判断是否表满,然后还可以用来扩充顺序表空间大小,扩充完后,capicity又应该和新的空间等大。
③:动态顺序表顾名思义是动态开辟一块连续的空间,这里使用到malloc函数动态开辟一块空间,并使指针q指向它,初始空间可根据实际大致估算预留空间。
④:通常malloc函数使用后,我们会去检验一下开辟空间是否成功,malloc开辟失败会返回空,所以作为判断条件。具体小伙伴们可自行了解。
⭐️ (2)、释放空间(销毁)函数:
因为我们是动态开辟的空间,使用完后应该将空间还给操作系统。
代码实现如下:
void Sldestroy(SL* ps) { free(ps->q); ps->q = NULL; ps->capicity = ps->size = 0; }
接下来该是插入操作,这里插入我们分为头插法和尾插法,同理删除也为头删法和为尾删法。
这里值得注意,插入数据的时候,我们应当先判断顺序表有没有满的情况,又因为有尾插法和头插法都要判断,所以将此单独创建一个函数,如下:、
⭐️ (3)、判断是否表满函数:
代码实现如下:
void SLCheckCapicity(SL* ps) { if (ps->size == ps->capicity) { SLDatatype* tmp = (SLDatatype*)realloc(ps->q, ps->capicity * 2 * sizeof(SLDatatype)); if (tmp == NULL) { perror("realloc failed"); exit(-1); } ps->q = tmp; ps->capicity *= 2; } }
解释:
①:顺序表涉及数组,数组就涉及下标,所以关于线性表的个个知识,小编建议大家多多画图分析,如下图:
可以看出,顺序表表满的时候,size==capicity,所以可用作 if 判断条件。
②:当判断条件成立,即表满时,我们就需要扩充空间,这里用到realloc函数(建议小伙伴们自行去学习一下realloc函数,各个参数代表什么,有什么功能,返回值是什么)。
前面提到的capicity有扩充空间的功能就用在于此,一般我们扩充一次空间就扩充为两倍的capicity,即原来空间的两倍,同时应该capicity*=2,用于记录扩充后的空间大小。
③:至于这里为什么是新创建一个指针变量tmp用于扩充空间,然后将tmp直接赋值给指针q,这得益于realloc函数的功能,大家可自学一下realloc函数。
⭐️ (4)、尾插法:顾名思义就是在尾部插入元素.
代码实现如下:
void SLPushBack(SL* pa, SLDatatype x) { SLCheckCapicity(pa); pa->q[pa->size++] = x; }
解释:
①:形参中指针pa同上,用于接收对象地址,x即要插入的元素。
②:不管什么插入方法,首先都要判断是否表满,所以调用上述我们实现的SLCheckCapicity函数进行判断。
③:接着就是插入,因为size是现有元素个数,而下标从0开始,所以如下图,我们只需要将x放在数组q的下标为size处,即q[size]=x,然后再将size自增一下,即可完成尾插法。
⭐️ (5)、有了尾插自然就有尾删法:顾名思义就是在尾部删除数据
代码实现如下:
void SLPopBack(SL* pa) { //判断是否为空表 if (pa->size == 0) { assert(pa->size > 0); } pa->size--; }
解释:
①:不管是什么删除元素的方法,刚开始肯定要先判断表中是否有元素,有的话才能删除。这里判断的处理方法小编给有两种,一种是事例上面用一个名为“断言”的函数assert,形参为一个表达式,若表达式为假,则assert函数会直接报错。另一种是直接return,使删除函数结束。
②:思路:因为是顺序表,所以尾删法直接使size自减一下就行了,这样就访问不到尾元素了,下次再增加数据也可以直接覆盖掉原有数据。
⭐️ (6)、头插法:顾名思义就是在头部插入数据
代码实现如下:
void SLPushFront(SL* pa, SLDatatype x) { SLCheckCapicity(pa); int end = pa->size - 1; //从后往前移动数据 while (end >= 0) { pa->q[end + 1] = pa->q[end]; end--; } pa->q[0] = x; pa->size++; }
解释:
①:同样是插入数据,所以开始先调用SLCheckCapicity函数判断是否表满。
②:思路;然后因为要在头部插入,所以在空间足够的情况下,我们只需将原表中的数据依次向后移动一个存储空间,然后再将数据x放入首元素q[0]的位置处,插入成功后size自增一下即可。
⭐️ (7)、头删法:顾名思义就是在头部删除数据。
代码实现如下:
void SLPopFront(SL* pa) { if (pa->size == 0) { assert(pa->size > 0); } int left = 1; while (left <= pa->size - 1) { pa->q[left-1] = pa->q[left]; left++; } pa->size--; }
解释:
①:删除数据,同理先判断是否表空。
②:思路:因为是在删除元素,所以只需将首元素后面size-1个数据依次向前移动一个存储空间,将首元素覆盖掉,然后size自减一下即可。注意是从前往后向前移动,防止丢失数据。
⭐️ (8).在下标为pos的位置插入数据x:
代码实现如下:
void SLInsert(SL* ps, int pos, SLDatatype x) { assert(pos <= ps->size && pos >= 0);//判断插入位置是否无效 SLCheckCapicity(ps); int end = ps->size-1; while (end >=pos) { ps->q[end + 1] = ps->q[end]; end--; } ps->q[pos] = x; ps->size++; }
解释:
①:形参中指针ps用于接收对象地址,pos为要插入数据的位置(下标),x为要插入数据的值。
②:思路:将原pos位置及pos位置之后的值依次向后一个储存空间,切记从后向前移动,然后再把x放到下标为pos的位置。
③:用到库函数断言assert,用于判断插入下标pos是否合法,pos必须大于等于0且小于等于size.
④:依旧需要判断是否表满,调用函数SLCheckCapicity。
⑤:移动涉及循环,数组涉及下标,所以一定要画图分析各个下标,可以达到事半功倍的效果:
最后再将x放到下标为pos的位置,然后size自增一下即可。
⭐️ (9)、当有了第(9)种操作后,我们会发现如下:
①:pos==size时,即为尾插算法,所以尾插可复用此函数,即:
//尾插 void SLPushBack(SL* pa, SLDatatype x) { //正常方法: /*SLCheckCapicity(pa); pa->q[pa->size++] = x;*/ //复用函数SLInsert SLInsert(pa, pa->size, x); }
②:pos==0时,即为头插算法,同理如下:
//头插 void SLPushFront(SL* pa, SLDatatype x) { //正常方法: //SLCheckCapicity(pa); //int end = pa->size - 1; 从后往前移动数据 //while (end >= 0) //{ // pa->q[end + 1] = pa->q[end]; // end--; //} //pa->q[0] = x; //pa->size++; //复用函数SLInsert SLInsert(pa, 0, x); }
大家会发现是不是大量减少了代码量。
⭐️ (10)、删除pos位置的数据:
代码实现如下:
//删除pos位置的数据,下标为pos-1 void SLErase(SL* ps, int pos) { assert(ps->size != 0 && pos >= 0 && pos < ps->size); //直接将pos位置覆盖 int left = pos + 1; while (left < ps->size) { ps->q[left-1] = ps->q[left]; left++; } ps->size--; }
解释:
①:形参中。pos为要删除的数据的下标。
②:然后需要判断一下下标pos的值是否有效,这里依然用到库函数assert。
③:思路:只需要将下标为pos的位置及其pos之后的数据依次向前移动一个存储空间,将要删除的数据覆盖即可达到删除的目的,最后size自减一下即可。
④:依旧需要画图分析,小伙伴们可自行尝试。
⭐️ (11)、当有了第10种操作后,我们会发现如下:
①:pos==0时,即为头插算法。
//本章知识尚未结束,后续小编会持续更新!