目录
一、顺序表
顺序表使用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
顺序表可以分为:
1、静态顺序表:使用定长数组存储元素。
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType a[N];
int size;//数据个数
}SL;
2、动态顺序表:使用动态开辟的数组存储。
typedef int SLDataType;
typedef struct SeqList
{
SLDataType *a;
int size;//数据个数
int capacity;//容量
}SL;
由以上两个结构,我们可以看出,静态顺序表开辟的空间是固定的,而实际实现中,我们往往不清楚我们有多少个数据元素,空间开辟大了,浪费空间,开辟小了,空间不足,这样就显得不够灵活。因此,我们往往使用动态顺序表(本篇文章就使用了动态顺序表)-------当有一个数据时,我们就开辟一个空间。
二、顺序表各个接口的实现
在此之前,我先给出测试代码,以便大家更好的去测试各个接口。
void TestSeqList1()
{
SL s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPrint(&s1);
SeqListPopBack(&s1);
SeqListPrint(&s1);
SeqListPushFront(&s1, 40);
SeqListPrint(&s1);
SeqListPopFront(&s1);
SeqListPrint(&s1);
SeqListInsert(&s1, 1, 500);
SeqListPrint(&s1);
SeqListErase(&s1, 1);
SeqListPrint(&s1);
SeqListErase(&s1, 1);
SeqListPrint(&s1);
}
int main()
{
TestSeqList1();
return 0;
}
1、顺序表的初始化
刚开始时,顺序的元素个数和容量都为0,所以指针指向空。
void SeqListInit(SL* ps)
{
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
2、顺序表空间扩容
我们使用的是动态顺序表,当我们的空间不够时,我们需要对空间进行扩容,那么什么时候空间会不够?是不是当我们的元素个数等于空间容量时,即size==capacity。size==capacity有两种情况,一种是顺序表要插入第一个元素,这个时候size和capacity都为0,我们就分配4个空间给顺序表;另一种是顺序表已经有了元素,即size和capacity都不为0且相等,我们就让空间扩大两倍,即capacity*2。当size!=capacity时,说明空间足够,这个函数就什么事情也不干。
void SeqListCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = (ps->capacity == 0) ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
printf("realloc fail'\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}
2、顺序表尾插
在顺序表的尾部插入一个元素,即在下标为size的位置上插入一个元素。凡是插入元素,都有两种情况:一种是空间足够时,我们就直接插入元素;另一种是空间不够时,我们就需要扩容。因此在插入函数之前,我们首先调用扩容函数
//尾插顺序表
void SeqListPushBack(SL* ps, SLDataType x)
{
//检查空间是否足够
SeqListCheckCapacity(ps);
//空间足够
ps->a[ps->size] = x;
ps->size++;
}
3、顺序表打印
打印顺序表时,我们从下标为0的位置上开始访问,访问到小于size的位置上。
void SeqListPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
4、顺序表销毁
我们只需要free掉a所指向的空间,将size和capacity置空。
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
5、顺序表尾删
我们只需要让size--就行,因为我们打印顺序表时,只打印到小于size的位置上(参考3、顺序表打印),即使原来的数据还存在,我们已经访问不到它了。但是在此之前,我们得判断顺序表要有元素,即要求size>0。
void SeqListPopBack(SL* ps)
{
//方法1
//if (ps->size > 0)
//{
// ps->size--;
//}
//方法2
assert(ps->size > 0);//为真什么事情也不干,为假报错
ps->size--;
}
6、顺序表头插
头插顺序表时,我们需要把所有的数据元素都往后挪一个位置。若需扩容,则扩容。
void SeqListPushFront(SL* ps, SLDataType x)
{
SeqListCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
7、顺序表头删
头删顺序表时,我们从0开始到size-1结束,让后一个元素覆盖前一个元素,覆盖完成后size--。
由这个图我们可以看出,size--后,下标为size的位置上,仍然有数据,但是我们访问时(参考参考3、顺序表打印)只访问到小于size的数据,所以即使size的位置上有数据,我们也访问不到,因此没有关系。
void SeqListPopFront(SL* ps)
{
assert(ps->size > 0);
int start = 0;
while (start < ps->size-1)
{
ps->a[start] = ps->a[start + 1];
start++;
}
ps->size--;
}
8、销毁顺序表
销毁顺序表要求销毁整个空间,这个函数实现较简单,因为结构体的指针a(即SLDataType *a)指向了动态内存所开辟的空间,我们只需要free(a),然后将size和capacity为0就行
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
9、查找函数
在顺序表中查找某个值,如果找到了,返回这个值的下标,如果没有找到,返回-1。
int SeqListFind(SL* ps, SLDataType x)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
10、指定pos下标插入数据
完成这个接口,(1)我们要保证pos这个下标是有效的(即pos <= ps->size || pos >= 0);(2)要考虑是否扩容;(3)要挪动pos及pos之后的数据。
void SeqListInsert(SL* ps, int pos, SLDataType x)
{
assert(pos <= ps->size || pos >= 0);
SeqListCheckCapacity(ps);
//挪动数据
int i = 0;
for (i = ps->size - 1; i >= pos; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[pos] = x;
ps->size++;
}
11、指定pos下标位置删除
完成这个接口,(1)我们要保证pos这个下标是有效的(即pos <= ps->size || pos >= 0);(2)删除pos下标的数据,我们只需要让pos后面一个数据覆盖pos,然后不断地迭代,迭代到下标为size-1的位置上。
void SeqListErase(SL* ps, int pos)
{
assert(pos >= 0 || pos < ps->size);
while (pos < ps->size - 1)
{
ps->a[pos] = ps->a[pos + 1];
pos++;
}
ps->size--;
}
三、结言
为了更好的理解这些接口,希望大家结合测试代码在编译器走一遍,我相信大家会有更大的收获。若有不正确的地方,希望大家多多指正。