一,顺序表的概念
顺序表是使用一段连续地址来依次存储数据元素的线性结构,一把情况下采用数组存储。
举个不恰当的例子,假设你所在的班有30人,而每个宿舍可以住6人。一般来说,在分宿舍时,学院会将同一个班的学生分在一起,即比如将A,B,C,D,E五个宿舍分给了你们班,这五个宿舍是挨着的,就好比顺序表一样,使用一段连续空间来存放数据。
二,顺序表的分类
顺序表可以分为静态顺序表和动态顺序表。
(一)静态顺序表:使用定长数组来存储数据
#define N 7
typedef int SLDataType
//定长数组,不方便使用
typedef struct SeqList
{
SLDataType array[N];
size_t size;
}SList;
(二)动态顺序表:使用动态开辟的数组来存储
//动态顺序表
typedef int SLDataType
typedef struct SeqList
{
SLDataType * array; //这个是指向数组的指针
size_t size;
size_t capacity; //这个是容量,当不够时我们需要扩容
}SL;
三,动态顺序表的实现
每一种结构的实现,我们都需要完成增删查改几个功能。
顺序表要包含以下的功能:
// 创建/销毁顺序表
void SLInit(SL* ps);
void SLDestory(SL* ps);
// 一个数据结构的四大功能:增删查改
// 增删数据:头插/头删/尾插/尾插/任意位置插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
// 查找和修改
int SLFind(SL* ps, SLDataType x);
void SLModify(SL* ps, int pos, SLDataType x);
//辅助功能:检查容量,扩容操作/打印操作
void SLCheckCapacity(SL* ps);
void SLPrint(SL* ps);
(一)初始化顺序表
设计函数SLInit实现对顺序表的初始化
由于初始化时,这个顺序表中还没有存储任何的数据,所以我们需要使顺序表中的指针指向NULL的同时让size和capacity也同时等于0。
初始化时,应当使用传址调用。若传值调用,由于形参的改变不会影响实参,所以无法将NULL和0初始化给相对应的数据。后面的几乎所有的代码都是要传地址的。
void SLInit(SL * ps)
{
assert(ps);
ps -> a = NULL;
ps -> size = ps -> capacity = 0;
}
(二)顺序表的销毁
设计函数SLDestory实现对顺序表的销毁
在扩容的时候由于使用了realloc函数,所以我们应该使用free函数来释放空间。
void SLDestory(SL * ps)
{
assert(ps); //判空检查
//如果不为空的话就释放空间并将指针置为空,size,capacity全部置为0
if(ps->a != NULL)
{
free(ps->a);
ps->a=NULL;
ps->size=ps->capacity=0;
}
}
(三)顺序表的扩容
设计函数CheckCapacity实现对顺序表的扩容
对容量空间进行检查:没空没满啥也不干,若空则开辟,若满则扩容。
当为空时,size = capacity == 0 ;当顺序表装满时,此时size = capacity != 0。所以,当size==capacity时,我们就需要扩容,size==capacity即是我们判断是否需要扩容的条件。
假设当其为空时,我们安排开辟四个,如果不为空时,每次空间使用完之后都扩容成为上一次空间的两倍。
void CheckCapacity(SL * ps)
{
assert(ps);
//这里的作用是检查容量,由上面的分析可知
//判断是否申请或扩容的标准是size是否等于capacity
if(ps->size==ps->capacity)
{
//这个代码用于确定顺序表需要的容量
int newcapacity = (ps->capacity==0?4:ps->capacity*2);
//当size==capacity==0时,我们需要的是开辟数据
//当size==capacity!=0时,我们需要的是在原有空间上扩大二倍
//这个语句也是可以用if语句写的,但是三目写起来是更简便的
//这个代码用于动态开辟空间
SLDataType* tmp =(SLDataType*)realloc(ps->a,newcapacity*sizeof(SLDataType));
//void* realloc (void* ptr, size_t size)
//我们需要的空间大小就是 对象个数*对象大小,其中对象个数就是我们的capacity
//这个代码用于判断我们的动态开辟是否成功。如果不成功的话就会直接结束。
if(tmp==NULL)
{
printf("realloc fail!!");
return;
}
//如果成功开辟了,我们就把新空间给到ps->a,新容量给到ps->capacity
ps->a=tmp;
ps->capacity=newcapacity;
}
}
(四)顺序表的打印
设计函数SLPrint实现对顺序表的打印
由size可以知道顺序表存储数据的多少,所以我们用size来判断终止的标志
void SLPrint(SL * ps)
{
assert(ps);
for(int i=0 ; i<ps->size ; i++)
//定义i从0开始,只要i小于size,就打印a[i]的内容
{
printf("%d ",ps->a[i]);
}
printf("\n");
}
(五)顺序表的尾插
设计函数SLPushBack实现对顺序表的尾插
顺序表尾插的时间复杂度为O(1),是比较快速的
尾插时,我们要讨论三种情况:
1,若初始化后马上尾插,此时的size和capacity均为0
2,size==capacity!=0时,说明此时顺序表已经满了,要扩容
以上两种情况告诉我们,在进行插入操作之前,我们要对容量进行检查。
3,没满,这时正常插入即可
void SLPushBack (SL* ps,SLDataType x)
{
assert(ps);
//对容量空间进行检查,如果为空或满,就会自动扩容
SLCheckCapacity(ps);
//当尾插时,我们想插入位置的下标一定是size
ps->a[ps->size]=x;
//扩容之后要给size++
ps->size++;
}
(五)顺序表的尾删
设计函数SLPopBack实现对顺序表的尾删
尾插时,队后一个位置的元素设置为多少其实并不重要,只要直接--size就可以。
原因就是:因为顺序表是按顺序排列的,size表示有效元素的个数。只要--size,即令最后一个元素失效。就等效于删除数据。
由于容量不会改变,所以不必给capacity进行任何操作。
void SLPopBack(SL* ps)
{
//尾删时要注意,当我们删到没有数据,再次进行尾删时,
//会导致size<0,即越界访问!!
//所以我们应当对size进行检查,只有size大于0时才可以进行尾删。
assert(ps!=NULL);
assert(ps->size>0);
ps->size--;
}
(六)顺序表的头插
设计函数SLPushFront实现对顺序表的头插
头插时,当我们给第一个位置放入了一个新的数据时,原先第一个位置的顺序就会被顶到第二个位置,以此类推,所有的数据都会向后挪动一位。
并且我们要注意,再插入数据前,我们要判断顺序表是否是满的,如果顺序表是满的,则需要为新数据扩容。
顺序表头插的时间复杂度为O(n),相较尾插是比较慢的。
挪动数据是也是有讲究的,挪动数据时要从后往前挪,否则会遮盖原本的数据。
挪动的大体思路如图:
对应该图的代码如下:
void SLPushFront(SL* ps,SLDataType x)
{
assert(ps);
//检查容量是否已满
SLCheckCapacity(ps);
//定义一个结尾位置end,便于我们从后向前挪动数据
int end = ps->size-1;
while(end>=0)
{
ps->a[end+1]=ps->a[end];
end--;
}
//当循环结束后直接把想插入的数据给到ps->a[0];
ps->a[0]=x;
ps->size++;
}
(六)顺序表的头删
设计函数SLPopFront实现对顺序表的头删
头删时,数据如同尾删一样,也是要挪动的。但是尾插是从后向前挪动,头插是从前向后挪动。
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size>0)
int begin = 0;
while(begin<ps->size)
{
ps->a[i]=ps->a[i+1];
begin++;
}
ps->size--;
}
(七)顺序表任意位置的插入
设计函数SLInsert实现对顺序表的任意位置的插入。
这个插入函数,可以服用在头插和尾插之中。
插入数据以后,就像尾插一样,数据要从后面向前挪动,再在pos位置处检查。并且对插入的位置也是有要求的!!如下图。
首先是插入位置1,该位置比capacity都大,一定不是合法位置。
其次是插入位置2,由于顺序表的定义是连续存储,所以这个位置也不是合法的。
之后是插入位置3,这个位置就相当于尾插,是可以的。
最后是插入位置4,这个位置不用问就是合法的。
所以,综上所述,可以插入的位置是:pos>=0并且pos<=ps->size。
代码写作:
void SLInsert(SL* ps,int pos,SLDataType x)
{
assert(ps);
assert(pos<=ps->sizee&&pos>=0);
SLCheckCapacity(ps);
//挪动数据
int end=ps->size-1;
while(end>=pos)
{
ps->a[end+1]=ps->a[end];
--end;
}
ps->a[pos]=x;
ps->size++;
}
实现这个代码之后,头插和尾插可以这样写:
void SLPushFront(SL* ps,SLDataType x)
{
SLInsert(ps,0,x);
}
void SLPushBack (SL* ps,SLDataType x)
{
SLInsert(ps,ps->size,x);
}
(八)顺序表任意位置的删除
设计函数SLErase实现对顺序表的任意位置的删除。
同之前一样,删除时数据从前往后移,这个函数同时也可以实现头删,尾删。
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos>=0&&pos<ps->size);
//此时不可以等于size,这样写还顺便检查了size为空的情况
int begin = pos+1;
while(begin<ps->size)
{
ps->a[begin-1]=ps->a[begin];
++begin;
}
ps->size--;
}
实现这个代码之后,头删和尾删可以这样写:
void SLPopFront(SL* ps)
{
SLErase(ps, 0);
}
void SLPopBack(SL* ps)
{
SLErase(ps, ps->size-1);
}
(九)顺序表查找数据,返回查找到数据的下标
设计函数SLFind实现对顺序表数据的查找。
int SLFind(SL* ps,SLDataType x)
{
assert(ps);
for(int i=0;i<ps->size;i++)
{
//当找到值为x的元素时就返回下标
if(ps->a[i]==x)
return i;
}
//当没有找到时,就返回-1,由于一定没有下标为负的元素
//所以当返回值为-1就证明没有找到
return -1;
}
(十)顺序表修改pos位置的元素的值
设计函数SLModify实现对顺序表pos位置元素数据的修改。
void SLModify(SL* ps,int pos,SLDataType x)
{
assert(ps);
assert(pos>=0&&pos<ps->size);
ps->a[pos]=x;
}
四,顺序表的优缺点
优点:作为数组,有着连续的物理空间,方便组织数据。
支持下标的随机访问。
缺点:要求物理空间连续,可能会造成空间浪费,性能消耗。
头插/头删与中间插入/删除效率低下O(N),仅尾插/尾删效率较高O(1)。
知识小结: