一、顺序表的结构
对于顺序表来说,它的本质就是数组,通过一个结构体定义一个指向数组的指针,其中结构体中还包括顺序表的当前的下标和整体的容量大小。
知道了顺序表的本质为数组后,相比于之前的诸多生疏或许就会显得熟络起来。顺序表在内存中是连续存储的一片空间,访问元素是通过下标来实现,所以顺序表的特点之一就是随机访问元素。
此外,顺序表还有静态和动态之分,对于静态顺序表则是和普通数组并无两样,只是建立后不可改变顺序表的长度,而动态顺序表是通过"malloc"、"calloc"、"realloc"等函数建立的,所以当顺序表的当前下标等于容量时就代表顺序表已满,如果继续向顺序表中存储数据,就要再次开辟空间。
二、顺序表的特点
顺序表在内存中是连续的一片空间,本质为数组,那么对于它的一些特点为:
1、优点:
(1)可以随机访问任意元素。
(2)存储密度大,适合存储较为稠密的数据。
(3)对于表尾元素的插入和删除较为快捷。
2、缺点:
(1)对于非表尾元素的插入和删除所要牵连的元素过多,处理效率低。
(2)对于静态顺序表,空间大小不可改变,具有局限性。
三、顺序表的结构定义(本章以动态顺序表为例)
cs
typedef int DataType;//类型定义为int
typedef struct SQLIST
{
DataType* arr;//指向数组的指针
int size;//当前顺序表的下标,初始为0
int capacity;//顺序表的空间大小
}sqlist;
四、顺序表的基本操作
4.1顺序表的初始化
cs
//顺序表初始化
void SqlistInit(sqlist* pf)
{
assert(pf);
DataType* tmp = (DataType*)malloc(sizeof(DataType) * 4);//建立4个空间大小
if (tmp == NULL)//判断空间是否开辟成功
{
perror("malloc");
return;
}
pf->arr = tmp;
pf->size = 0;
pf->capacity = 4;
}
顺序表的初始化主要是对结构体内定义的变量和指针进行赋予初始值。
开始时,顺序表尚未建立,所以指向数组的指针初始为NULL,顺序表的空间和下标也都一并置为0,说明顺序表尚未建立,同时为了确保传入的pf结构体指针有效,在进入函数时需要"assert"断言一下。
4.2顺序表的插入
顺序表的插入通常分别为三种:头插、尾插、任意位置插入,三种插入方式,接下来依次为大家梳理三种插入的思想。
三种插入的统一思想都是对顺序表元素的移动。
4.2.1头插
对于头插,顾名思义就是在顺序表的表头插入元素,但对于顺序表原先的元素来说,表头已经有元素,所以要想将新的元素放到表头,需要将从第二个位置也就是下标为"1"及之后的元素整体向后移动一个位置,但是在插入元素之前,我们要首先判断顺序表的空间是否能存储加入之后的元素个数,所以我们要在插入之前写一个"判断顺序表空间大小"的函数。
cs
//判断顺序表空间大小,如果空间不足,则要先行扩容空间
void SqlistPlace(sqlist* pf)
{
assert(pf);
if (pf->capacity == pf->size)
{
//开辟比之前大两倍的空间(没有特别的限制,根据具体情况而定)
DataType* tmp = (DataType*)realloc(pf->arr, sizeof(DataType) * pf->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
pf->arr = tmp;
pf->capacity *= 2;//顺序表的空间扩大为原来的两倍,根据自己开辟的空间进行改变capacity的大小
}
}
判断完毕后,就可以直接进行移动数据了,在移动数据时,可以采用循环的方式,也可以使用"memmove"内存函数进行移动。
cs
//头插
void SqlistPushHead(sqlist* pf, DataType n)
{
assert(pf);
SqlistPlace(pf);//判断空间大小
memmove(&(pf->arr[1]), &(pf->arr[0]), sizeof(DataType) * pf->size);//移动元素
pf->arr[0] = n;//将新元素插入到顺序表的第一个位置
pf->size++;//顺序表的下标加一
}
4.2.2尾插
顺序表的尾插就非常好理解的,直接将新的数据放到表尾即可,不多说,看代码。
cs
//尾插
void SqlistPushTail(sqlist* pf, DataType n)
{
assert(pf);
SqlistPlace(pf);//判断空间大小
pf->arr[pf->size] = n;//将新元素插入到顺序表的最后一个位置
pf->size++;//顺序表的下标加一
}
4.2.3任意位置插入
从任意位置插入元素,只需知道所要插入元素的下标和所要移动的字节个数即可。
如果我们要插入一个位置,那么下标我们是必然知晓的,那么要移动多少元素应该怎么计算呢?
其实在我们对顺序表里面的数据进行增加和删除时,都会不可避免的用到"pf->size"这个定位当前下标的变量,而每当增加一个元素,需要将元素直接放入它指向的位置空间中,然后再自增1。所以我们可以理解为pf->size的大小始终是比当前最后一个数据下标加一的,所以要想得到所要移动的元素个数,只需用((pf->size)-1)即可。
cs
//任意位置插入
//pf为传入的结构体指针,i为要插入的小标位置,n为要插入的数据
void SqlistPushRandom(sqlist* pf, int i, DataType n)
{
assert(pf);
SqlistPlace(pf);//判断空间大小
memmove(&(pf->arr[i + 1]), &(pf->arr[i]), (pf->size - i) * sizeof(DataType));
pf->arr[i] = n;
pf->size++;
}
到这里,插入的操作就完成了,我们可以根据实际情况来进行所需的操作。
4.3顺序表的删除
顺序表的删除操作与插入极其相似,也同样分为3个情况,分别为:头删、尾删、任意位置删除。
4.3.1头删
从表头删除元素,所要移动的元素是最多的,需要把从第二个位置到最后的所有元素都要向前移动一个位置,所以要移动元素的个数为((pf->size)-1)个。
cs
//头删
void SqlistHeadDelete(sqlist* pf)
{
assert(pf);
if (pf->size == 0)//判断顺序表是否为空,如果为空则报错
{
perror("pf->size empty fail");
return;
}
memmove(&(pf->arr[0]), &(pf->arr[1]), (pf->size - 1) * sizeof(DataType));
pf->size--;//下标减一
}
4.3.2尾删
尾删的思想极为简单,只需将pf->size也就是顺序表的下标减一即可。
cs
//尾删
void SqlistTailDelete(sqlist* pf)
{
assert(pf);
if (pf->size == 0)//判断顺序表是否为空,如果为空则报错
{
perror("pf->size empty fail");
return;
}
pf->size--;
}
4.3.2任意位置删除元素
任意位置删除元素,与插入的思想相似,只是所要移动元素的个数和位置有所不同。
任意位置删除元素需要将所要删除元素后一个以后的整体数据向前移动一位覆盖掉要删除的元素,移动个数为(pf-size-i-1)(i为要删除元素的下标)
cs
//任意位置删除
void SqlistDeleteRandom(sqlist* pf, int i)
{
assert(pf);
if (pf->size == 0)//判断顺序表是否为空,如果为空则报错
{
perror("pf->size empty fail");
return;
}
memmove(&(pf->arr[i]), &(pf->arr[i + 1]), (pf->size - i - 1) * sizeof(DataType));
pf->size--;
}
4.4顺序表元素的修改
对顺序表内元素的修改是非常简单的,只需传入想要修改元素的下标和新的元素,就可以直接访问目标位置,进行修改。
cs
//元素修改
void SqlistDataModify(sqlist* pf, int i, DataType n)
{
assert(pf);
if (pf->size == 0)//判断顺序表是否为空,如果为空则报错
{
perror("pf->size empty fail");
return;
}
if (i > pf->size)
{
printf("输入的下标无效\n");
return;
}
pf->arr[i] = n;
}
需要注意的是,在进行修改数据前,应先判断顺序表是否为空、传入的下标是否小于当前的最大下标,如若有一个为错,则报错再提前返回。
4.5查找顺序表中值为x的下标位置
查找顺序表中值为x的数据,只需从前往后或从后往前遍历一遍即可,如果当前顺序表中有多个为x的值,可以建立一个整形数组,将遍历得到的下标依次存入其中即可,本章以只有一个x的数值为例。
cs
//查找值为x的数据下标
int SqlistFindData(sqlist* pf, DataType x)
{
assert(pf);
if (pf->size == 0)//判断顺序表是否为空,如果为空则报错
{
perror("pf->size empty fail");
return;
}
for (int i = 0; i < pf->size; i++)
{
if (pf->arr[i] == x)
return i;
}
return -1;//返回-1说明顺序表中没有该数据
}
五、总结
对于顺序表的相关操作,大家完全可以将其当作数组去理解,因为顺序表的本质就是数组,只是相比于数组,顺序表的结构体定义有所不同罢了。