一、什么是顺序表
顺序表是一种数据结构,或者说,是数据在内存中存储和管理的一种方式。顺序表要求每个数据要从第一个位置开始,依次挨着放。这就很适合使用C语言中的数组来实现。
很多朋友可能会觉得,那有啥可以讲的?我们创建一个数组,在里面存储数据不就得了?其实没这么简单。这个数组要创建多大?如果创建太小,万一数据太多,不够存咋办?万一创建太大,却没这么多数据,那很多空间不就浪费了?所以,我们需要进行动态的内存管理。比如说,一开始就给4个空间,之后每次放满了就扩容,依次类推。
C语言里提供了动态内存管理的函数,分别是malloc,calloc,realloc和free。这些函数能够帮助我们很好地进行动态内存的管理。以上的这几个函数有一个特点,需要使用一个指针来指向这块动态管理的空间。除此之外,我们还需要两个变量,分别用来记录这块空间的大小,以及存储的有效数据的个数。综上,我们需要声明一个结构体来描述顺序表。
struct SeqList
{
int* a;
int size;
int capacity;
};
其中,我们假设要存储的数据类型是int,所以需要一个int*的指针来管理这块空间。如果需要存储其他类型的数据,并且随时修改存储类型,可以使用typedef,用SLDateType来代替此处和下文中出现的int,这样方便以后修改存储类型。
typedef int SLDataType;
以上结构体中的size代表目前存储的有效数据的个数,一开始是0。而capacity则代表容量,也就是目前最多可以存储的数据个数。当size和capacity相等时,代表顺序表已满,此时就需要扩容。
需要说明的是,如果你嫌每次写struct SeqList很麻烦,也可以typedef一下。
typedef struct SeqList
{
int* a;
int size;
int capacity;
}SL;
以下叙述中,我们假设已经创建了一个顺序表,并且拿到了它的地址psl。
SL sl;
SL* psl = &sl;
由于后面的操作都要对psl解引用,所以我们假设每次解引用前都断言了psl!=NULL。
assert(psl);
二、初始化
顺序表一开始没有存储任何数据,所以可以把先把结构体的内容都初始化成0。
psl->a = psl->size = psl->capacity = 0;
三、插入、删除
数组的下标是从0开始的,所以我们假定顺序表的下标也是从0开始。那么,我们如何在下标为pos的位置插入一个数据呢?其实很简单,只需要把pos及pos之后的位置向后挪动一格,此时pos位置就空出来了,就可以插入数据了。所以核心代码就一行,即如何挪动数据。我们需要知道起始位置是pos,往后挪动一格就到了pos+1。一共要挪动几个数据呢?举个例子,假设一共有10个数据,也就是size=10,假设pos是3,下标是从0开始的,pos前面的数据有0,1,2,总共3个数据,也就是说,pos前面的数据有pos个,那么pos及pos之后的数据就有size-pos个,这也是要挪动的数据的个数。我们使用memmove函数来挪动数据,注意不能使用memcpy,因为挪动前后的内存空间是有重叠的。
memmove(psl->a+pos+1, psl->a+pos, (psl->size-pos)*sizeof(int));
接着插入数据。
psl->a[pos] = x;
psl->size++;
这就完了吗?No!你忽略了一个重要的问题。一开始初始化时,psl为空,能直接解引用吗?还有万一size=capacity,能直接插入吗?所以每次插入前,都要检查一下容量是否充足,如果满了,要扩容!
检查容量是否充足很好办,如果size==capacity就满了,这包括两种情况,一种是capacity=size=0,另一种是随着size逐渐增长,size==capacity时就满了。
那如何扩容呢?这就可以使用realloc。注意当传给realloc的指针为NULL时,realloc的表现和malloc是一样的。所以我们可以把上面两种情况当做一种情况来处理。为了叙述方便,我们假设一开始给4个空间,后面每次都扩2倍,也就是说新的容量是:
int newCapacity = psl->capacity==0 ? 4 : 2*psl->capacity;
接着调用realloc函数:
int* tmp = (int*)realloc(psl->a, newCapacity*sizeof(int));
若tmp经过检查不为NULL,说明扩容成功。
psl->a = tmp;
psl->capacity = newCapacity;
知道了如何插入数据,删除数据也是同样的道理。只需要把pos位置之后的数据向前挪一格,就覆盖掉了pos位置的数据,相当于把pos位置的数据删除了。目标位置是pos,起始位置是pos+1,要挪动的数据个数是多少呢?在上面插入数据的讲解中,pos及pos之后的数据个数是size-pos,那么不包括pos的数据个数就是size-pos-1。
memmove(psl->a+pos, psl->a+pos+1, (size-pos-1)*sizeof(int));
最后不要忘了psl->size--。但是有个问题,如果一开始就没有数据,那就不能继续删了呀!所以删除之前要断言一下psl->size>0,才可以删除(但有了后面的讲解,其实不用断言这一点,具体原因后面说)。
其实,前面的讲述中漏掉了一点,那就是pos并不是任何值都可以的。在插入数据的时候,必须保证pos>=0 && pos<=psl->size,而在删除的时候,必须保证pos>=0 && pos<psl->size。pos>=0很好理解,因为下标从0开始。pos<size也很好理解,因为size表示有效数据个数。假设有6个数据,即size是6,下标就是0~5,pos就只能从这之间取。但是插入时为啥允许pos==size呢?还是假设有6个数据,size是6,已经有的数据下标是0~5,那如果是否允许在下标为6的位置插入呢?当然允许呀!因为在已经有的最后一个数据后面紧接着插入一个数据,并没有违反顺序表数据挨着挨着存储的原则。
由于删除顺序表的元素时有pos>=0,size>pos,故有size>0,所以不用断言这一点。
四、打印
只需要遍历这个数组然后打印即可。
for (int i = 0; i<psl->size; ++i)
{
printf("%d ", psl->a[i]);
}
printf("\n");
五、查找、修改
查找也是遍历数组即可,非常简单。需要注意的是,pos必须要满足pos>=0 && pos<psl->size。具体原因,讲解插入删除时已经讲过。
如查找x:
for (int i = 0; i<psl->size; ++i)
{
if (psl->a[i] == x)
return i;
}
return -1; // 找不到就返回负数
修改也很简单,访问下标为pos的数据并且改掉即可。
psl->a[pos] = x;