1.什么线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串等…
在讲顺序表之前,我们先大致了解一下线性表。线性表在逻辑上是一种线性的数据结构。可以把线性表看成一条连续的直线。但是在物理结构上不一定是连续的,线性表在存储时,通常是以数组或者链式结构的形式存储的。
2.什么是顺序表
顺序表的概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表的特点:存储数据的数组可以动态调整,数据在内存中是连续存储的。
3.顺序表的实现
3.1 顺序表的定义
静态顺序表的内容为存放数据的数组和记录数据个数的变量。静态顺表的缺点比较明显,因为数组开辟必须一次性开辟完成,所以对于空间的利用率不佳。动态顺序表的内容为一个指向动态开辟空间的指针、记录数据个数的变量和记录当前顺序表容量的变量。动态顺序表对于空间的利用率较好,但是数据的增删查改效率一般。
静态版本定义
//静态顺序表
#define N 100
typedef int SeqLTDataType;
typedef struct SeqList
{
SeqLTDataType data[N];//数据
int size;//记录当前顺序表元素个数
}SeqList;
动态态版本定义
//动态顺序表
typedef int SeqLTDataType;
typedef struct SeqList
{
SeqLTDataType* data;//指向动态开辟的连续空间
int size;//记录当前顺序表元素个数
int capacity;//记录当前空间容量
}SeqList;
3.2 动态顺序表初始化
初始化动态顺序表接口,首先我们要开辟一块空间来存放数据,这里我就默认开辟4*sizeof(数据类型)字节空间来存放数据。所以容量初始化成4。由于还没存放数据,所以将size初始化成0。
#define DEF_CAPACITY 4
void SeqListInit(SeqList* SL)
{
assert(SL);
SeqLTDataType* tmp = (SeqLTDataType*)malloc(sizeof(SeqLTDataType) * DEF_CAPACITY);
if (NULL == tmp)
{
perror("malloc fail");
return;
}
SL->data = tmp;
SL->size = 0;
SL->capacity = DEF_CAPACITY;
}
3.3 动态顺序表销毁
销毁顺序表需要释放动态申请的空间和将定义的变量清0。
void SeqListDestroy(SeqList* pSL)
{
assert(pSL);
free(pSL->data);
pSL->data = NULL;
pSL->capacity = 0;
pSL->size = 0;
}
3.4动态顺序表打印
顺序表的打印就是遍历一遍顺序表,打印每个数据类型指针偏移量位置相对应的数据。
void SeqListPrint(SeqList* pSL)
{
assert(pSL);
for (int i = 0; i < pSL->size; i++)
{
printf("%d ",pSL->data[i]);
}
printf("\n");
}
3.5 动态顺序表检查当前容量
因为默认开辟的数据存放大小是4*数据类型大小的字节的空间,所以当我们的数据个数等于当前容量标记时,我们需要对申请的数据空间进行扩容。因为每次插入数据前都要进行检查,每次扩容当前容量的2倍大小的新容量,所以我们直接封装一个函数来实现该功能。
void CapacityCheck(SeqList* pSL)
{
if (pSL->size == pSL->capacity)
{
//先创建个临时变量存放扩容后的地址
SeqLTDataType* tmp = (SeqLTDataType*)realloc(pSL->data, sizeof(SeqLTDataType) * pSL->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pSL->data = tmp;
pSL->capacity *= 2;
}
}
我们应该考虑realloc异地扩容的问题,我们不能直接在原数组上直接realloc重新申请空间。每次容量乘2倍,虽然一开始可能realloc扩容后,还是在原空间后申请到连续的空间。但是,当申请的连续空间过大,可能无法满足在申请前空间后接续扩容,realloc函数会找到一块符合要求的新空间,并把数据拷贝到新空间中。
3.6 动态顺序表头插数据
头插数据的本质是将头插前的数据整体向后移动一个数据字节大小的偏移量,然后在起始地址处插入新数据。实现思路如下,应该从后往前移动数据,即将最后一个数据即,pSL->data[pSL->size]向后移动一位,从后往前遍历整个数据进行后移操作。将新数据插入pSL->data[0]中。在每次插入时要检查当前容量是否充足,若不足就扩容。
void SeqListPushFront(SeqList* pSL, SeqLTDataType x)
{
assert(pSL);
CapacityCheck(pSL);
for (int i = pSL->size; i > 0; i--)
{
pSL->data[i] = pSL->data[i-1];
}
pSL->data[0] = x;
pSL->size++;
}
3.7动态顺序表头删数据
头删数据就是从第二个数据后向后遍历数组,依次覆盖前一个位置的数据达到头删的效果。将当前的数据赋给前一个位置,所以循环必须从第二个数据位置开始。
void SeqListPopFront(SeqList* pSL)
{
assert(pSL);
assert(pSL->size);
for(int i = 1; i < pSL->size; i++)
{
pSL->data[i - 1] = pSL->data[i];
}
pSL->size--;
}
3.8动态顺序表尾插数据
尾插数据就是在size为偏移量的位置处插入数据,然后size++即可。
void SeqListPushBack(SeqList* pSL, SeqLTDataType x)
{
assert(pSL);
CapacityCheck(pSL);
pSL->data[pSL->size++] = x;
}
3.9动态顺序表尾删数据
尾删数据的本质就是让size–,然后下一个数据便可以覆盖未尾删前的数据了。
void SeqListPopBack(SeqList* pSL)
{
assert(pSL);
assert(pSL->size);//空表不可删
pSL->size--;
}
3.10查找某个数据的位置
既然是查找就要遍历数据,这里我们返回的是数组的下标位置。若找不到返回-1。
int SeqListFind(SeqList* pSL, SeqLTDataType x)
{
assert(pSL);
for (int i = 0; i < pSL->size; i++)
{
if (pSL->data[i] == x)
return i;
}
return -1;
}
3.11修改pos位置数据
前面查找函数的返回值就可以做为我们设计修改函数的参数。修改顺序表数据的本质就是修改对应下标数据的值。
void SeqListModify(SeqList* pSL, int pos, SeqLTDataType x)
{
assert(pSL);
assert(pos >= 0 && pos <= pSL->size);//判断下标合法性
pSL->data[pos] = x;
}
3.12 pos位置插入数据
这就类似于头插数据,需要先将pos位置到size位置的数据整体后移一个偏移量。然后插入新的数据
void SLInsert(SL* ps, int pos, SeqListtype x)
{
assert(ps && (pos >= 0 && pos < ps->size));
for (int i = ps->size; i > pos; i--)
{
ps->data[i] = ps->data[i - 1];
}
ps->data[pos] = x;
ps->size++;
}
3.13 pos位置删除数据
void SLErase(SL* ps, int pos)
{
assert(ps && (pos >= 0 && pos < ps->size) && ps->size > 0);
for (int i = pos; i < ps->size; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}