认识顺序表
顺序表是一种线性表的存储结构,它的元素是按照一定顺序依次存放在一段连续的存储空间中的数据结构。顺序表的特点是元素之间的逻辑次序和物理次序一致,可以通过元素在内存中的相对位置来直接访问元素,因此支持随机访问。常见的实现方式是使用数组来存储顺序表的元素。顺序表的优点是存取速度快,缺点是插入和删除操作需要移动大量元素。
顺序表的分类
顺序表可以按照不同的特征进行分类,主要包括以下几种:
-
动态顺序表:动态顺序表是指在程序运行时,根据需要动态调整内存空间大小的顺序表。当顺序表存储数据的数量超过当前内存空间大小时,动态顺序表会自动扩容,以保证存储数据的有效性和完整性。
-
静态顺序表:静态顺序表是指在程序运行前就确定了内存空间大小的顺序表。静态顺序表的内存空间大小是固定的,在程序运行时不能动态调整,因此在存储数据时需要确保数据不超过预先设定的容量。
-
有序顺序表:有序顺序表是指其中存储的数据是按照一定的规则或顺序排列的顺序表。有序顺序表可以根据特定的排序算法来实现数据的有序存储和检索,提高数据操作的效率。
-
稀疏顺序表:稀疏顺序表是指其中只有部分位置存储了数据,其他位置为空的顺序表。稀疏顺序表通常用于存储稀疏矩阵等数据结构,能够有效节省内存空间。
这些是顺序表的一些常见分类方式,不同类型的顺序表适用于不同的应用场景,可以根据具体需求选择合适的顺序表类型。
此处我们着重了解动态顺序表。
动态顺序表的实现
要实现顺序表首先要有函数声明,函数实现,测试。我们可以将此分为三个文件来撰写
当写test.c测试文件时,我们可以用来验证我们编写的函数是否正常工作。将函数主体写在SeqList.c文件中,可以更好地分离功能。这样有助于避免代码挤在一起,提高代码的可读性和可维护性。
//SeqList.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
//定义顺序表结构
typedef struct SeqList
{
SLDataType* arr;
int size;//有效数据个数
int capacity;//空间大小
}SL;
void SLTnit(SL* ps); //初始化
void SLDestroy(SL* ps);//销毁
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopBack(SL* ps);//尾删
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 SLCheackCapacity(SL* ps);//判断空间
void SLPrint(SL s);//打印顺序表
以上就是我们要实现的函数,将我们未来可能要用带的各种函数头文件写入,并定义了顺序表的结构。
为什么要重新命名int
我们将int重命名为SLDataType,是为了考虑到日后可能存储其他类型的数据,比如char、double、float等等。如果在代码中将int逐一替换为所需存储的数据类型,可能会出现误改或漏改的情况。而重命名后,只需要在一个地方进行修改即可,避免了频繁修改代码带来的风险。
定义顺序表结构
结构体内部成员
SLDataType* arr;
arr是一个指向SLDataType类型的指针,用于指向顺序表在内存中存储数据的起始位置。
int size;表示顺序表当前有效数据的个数,记录当前顺序表实际存储数据数量情况。
int capacity;表示当前顺序表所分配的内存空间大小,也就是顺序表最多能容纳的元素个数。
最后用typedef为结构体struct SeqList定义一个新的名字SL,在后续代码可以使用SL来表示structSeqList这种结构体类型,方便代码书写和使用。
初始化顺序表
void SLTnit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity;
}
销毁顺序表
void SLDestroy(SL* ps)
{
if (ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
判断空间大小
每次插入数据时我们都要判断空间是不是足够,空间不够肯定是不能插入数据的
void SLCheackCapacity(SL* ps)
{
assert(ps);//ps不能为空
if (ps->capacity == ps->size)
{
int newCapacity = (ps->capacity == 0) ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->arr,newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
assert("realloc faild");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
首先使用断言检查ps是否为NULL。
接着使用if语句判断如果容量等于大小(size)则说明顺序表已满,需要进行扩容操作。
计算新容量时,可以使用三目操作符,即(ps->capacity == 0) ? 4 : (ps->capacity * 2),该表达式的意思是:如果当前容量为0,则将新容量设为4,否则将新容量设为当前容量的两倍。
然后使用realloc函数重新分配内存,将容量扩大为原来的两倍,并将新的地址赋值给tmp。这是为了防止扩容失败导致原地址被覆盖而无法找到原数据。接着应该判断tmp不为NULL,然后将地址赋值给arr,同时更新容量。
尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheackCapacity(ps);
ps->arr[ps->size++] = x;
}
断言ps并检查空间大小是否足够,在末尾插入数据,最后不要忘记更新数据数——size++
头插
相当于我们要在下标为0的地方插入一个数据,那么就要将数据整体向后移动一个位置,怎么移动?是从前往后还是从后往前。不言而喻肯定是从后开始,否则会将原数据覆盖
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheackCapacity(ps);
//数据整体往后移动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
++ps->size;
}
依旧是断言ps和检查空间大小(以下不做解释),size直接赋值给i当移动到最后时候i肯定是要大于0的,移动完成后插入数据,同时更新空间——size++
尾删
尾删就较为简单了
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
ps->size--;
}
所以我们只需要size--就行
头删
头插实现较头插其实差不多,只需将数据整体往前移动一位
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
///数据整体向前移动一位
for (int i = 0; i< ps-> size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
在指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
SLCheackCapacity(ps);
assert(pos >= 0 && pos <= ps->size);
//将pos后面数据整体向后移动一位
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i- 1];
}
ps->arr[pos] = x;
ps->size++;
}
这段代码之中断言了pos的取值,验证插入位置 pos
的合法性。位置 pos
必须是非负的,并且不能超过当前顺序表的大小,pos可不可以=size,可以其实就相当于在最后插入一个数据
删除指定位置数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(ps->size);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
这里和上述代码就有些不同了,为什么这里pos不能等于size。
size指向的位置是最后一个数据的下一个空间,这个空间是没有数据的,所以pos必须小于size
查找指定数据
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
这里就只要一个for循环遍历数据,同时返回找到数据的下标
打印顺序表
void SLPrint(SL s)
{
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.arr[i]);
}
printf("\n");
}
这里就不需要指针了,直接传值调用,不需要返回什么值
总结
至此,顺序表的功能已经完成,顺序表本质上就是基于数组实现的数据结构。虽然结构体和指针在顺序表中并不是很复杂的部分,但掌握相关知识仍然很重要。此外,动态内存管理也是一个关键点,包括使用realloc函数调整内存大小以及使用free函数释放内存,以避免内存泄漏。希望这些内容能够帮助您更好地理解和应用顺序表。