顺序表是什么?
顺序表(Sequential List)是一种线性数据结构,它是一种用于存储和管理一组元素的数据结构,其中元素按照一定的顺序排列在内存中的连续存储区域中。顺序表通常用于静态分配或动态分配内存来存储元素。
以下是顺序表的一些关键特点和简单介绍:
-
连续存储:顺序表中的元素在内存中是连续存储的,这意味着每个元素都紧邻着下一个元素存储,因此可以通过下标或偏移来快速访问元素。
-
固定或动态大小:顺序表可以是固定大小的(静态顺序表)或动态大小的(动态顺序表)。静态顺序表在创建时分配固定大小的内存,不能动态增加或减少元素的数量。而动态顺序表可以根据需要自动扩展或收缩内存空间以容纳更多或更少的元素。
-
随机访问:由于元素在连续存储区域中,顺序表支持随机访问,可以通过索引或下标在常量时间内访问任何位置的元素。
-
插入和删除的效率:在静态顺序表中,插入和删除元素可能需要移动其他元素以维护顺序,因此效率较低。而在动态顺序表中,插入和删除的效率通常较高,但仍可能需要进行内存分配或释放操作。
-
适用场景:顺序表适用于需要频繁随机访问元素,但不需要频繁插入和删除元素的情况。它们通常用于实现数组、列表、栈和队列等抽象数据类型。
总之,顺序表是一种常见的数据结构,具有快速的随机访问特性,但在插入和删除方面的效率可能有所不足,因此在选择数据结构时需要根据具体应用场景来考虑是否使用顺序表。
顺序表的实现
下面我们将一步一步的完成顺序表的实现:
头文件的规划
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct Seqlist {
int* a; // a是一个指针,类似数组的首元素的地址
int size; //有效数据
int capacity; //空间容量
}SL;
void SLInit(SL* psl);
void SLDestroy(SL* psl);
void SLPrint(SL* psl);
void SLCheckCapacity(SL* psl);
//头尾插入删除
void SLPushBack(SL* psl, SLDataType x);
void SLPushFront(SL* psl, SLDataType x);
void SLPopBack(SL* psl);
void SLPopfront(SL* psl);
//任意下标位置插入和删除
void SLInsert(SL* psl, int pos, SLDataType x);
void SLErase(SL* psl, int pos);
简单介绍一下这个头文件内包含了什么 :
这个结构体名为Seqlist
,它由三个成员组成,每个成员的作用如下:
-
int* a
:这是一个指向整数类型的指针,用于存储顺序表中的元素。通常,它指向一个动态分配的整数数组,这个数组用来存储顺序表的元素。通过指针,可以访问和操作顺序表中的元素。 -
int size
:这是一个整数,用于记录当前顺序表中有效数据的数量。也就是说,它表示顺序表中当前存储的元素个数。通过size
可以快速获取顺序表的大小。 -
int capacity
:这是一个整数,用于记录顺序表的容量,即可以容纳的最大元素数量。它表示了顺序表内部分配的存储空间的大小。当顺序表的元素数量接近容量时,通常需要进行扩容操作,以容纳更多的元素。
这个结构体的成员组合在一起,用于表示一个顺序表数据结构,其中a
指向一个整数数组,size
记录有效数据的数量,capacity
表示容量。这个结构体的定义提供了一个框架,使得可以在其中存储和管理整数类型的元素,同时跟踪顺序表的大小和容量信息。
初始化顺序表
void SLInit(SL* psl) {
assert(psl->a != NULL);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
上面的函数SLInit
是用于初始化一个顺序表(结构体 SL
)的函数。它接受一个指向顺序表的指针 psl
作为参数,并将该顺序表的成员变量初始化为特定的初始状态。
具体而言,该函数的主要操作如下:
-
assert(psl->a != NULL);
:这是一个断言语句,用于检查传入的顺序表psl
的a
成员是否为NULL
。如果a
不是NULL
,则断言成功,程序继续执行。如果a
是NULL
,则断言失败,程序会引发错误并停止执行。这个断言的目的是确保在初始化之前,顺序表的a
成员不为空,以避免潜在的内存错误。 -
psl->a = NULL;
:接下来,函数将顺序表的a
成员设置为NULL
,这表示将其指向空指针,即没有分配内存的状态。这是为了确保在初始化之后,a
不再指向任何之前分配的内存区域,以避免内存泄漏。 -
psl->size = 0;
:将顺序表的size
成员初始化为0,表示当前顺序表中没有有效的数据元素。 -
psl->capacity = 0;
:将顺序表的capacity
成员初始化为0,表示当前顺序表的容量为0,即没有分配任何内存用于存储数据。
总之,SLInit
函数的作用是将传入的顺序表 psl
初始化为一个空表,即没有分配内存用于存储数据,size
和 capacity
都被设置为0。这个初始化操作可以在开始使用顺序表之前调用,以确保顺序表处于合适的初始状态。在初始化之前,还使用断言检查 a
成员不为空,以提前发现潜在的错误。这是保证程序的健壮性和安全性的一种常见做法。
顺序表的销毁
void SLDestroy(SL* psl) {
assert(psl);
if (psl->a != NULL) {
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
}
SLDestroy
函数用于销毁(释放)一个顺序表,释放与顺序表相关联的动态分配的内存,并将顺序表的成员变量重置为初始状态,以便下次使用。以下是该函数的主要操作:
-
assert(psl);
:这个断言用于确保传入的指针psl
不为空。它确保在进行销毁操作之前,psl
必须是有效的,以防止对空指针的非法操作。 -
if (psl->a != NULL)
:这个条件判断用于检查顺序表的a
成员是否为NULL
,如果不为NULL
,说明顺序表内部有动态分配的内存。 -
free(psl->a);
:如果a
不为NULL
,则调用free
函数释放a
指向的动态分配的内存。这一步非常重要,因为它确保在销毁顺序表时释放了所有分配的内存,避免了内存泄漏。 -
psl->a = NULL;
:将顺序表的a
成员设置为NULL
,以确保不再指向之前释放的内存区域。 -
psl->size = 0;
和psl->capacity = 0;
:将顺序表的size
和capacity
成员都设置为0,表示顺序表已经为空,并且容量为0。
总之,SLDestroy
函数的主要作用是释放顺序表 psl
内部动态分配的内存,确保在程序结束后不会发生内存泄漏。同时,它将顺序表的成员变量重置为初始状态,以便下次使用。这个函数的正确实现非常重要,因为内存泄漏可能会导致程序的性能问题和不稳定性。
顺序表的打印
void SLPrint(SL* psl) {
assert(psl);
for (int i = 0; i < psl->size; i++) {
printf("%d ", psl->a[i]);
}
printf("\n");
}
SLPrint
函数是用于打印顺序表中的元素的功能函数。它接受一个指向顺序表的指针 psl
作为参数,并通过一个循环遍历顺序表的元素,将它们按顺序打印到控制台上,每个元素之间用空格分隔,最后添加一个换行符。这个函数有助于在调试和测试时查看顺序表中的数据,以验证程序的正确性,但请确保传入的顺序表指针不为空以避免异常情况
顺序表的空间扩容
void SLCheckCapacity(SL* psl) {
assert(psl);
if (psl->size == psl->capacity) {
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);//如果空间是空,realloc也可以开一个新的空间
if (tmp == NULL) {
perror("realloc fail");
return;
}
psl->a = tmp;
psl->capacity = newCapacity;
}
}
上面的函数 SLCheckCapacity
用于检查顺序表的容量是否足够,并在需要时进行扩容。以下是对该函数的详细介绍:
-
assert(psl);
:这是一个断言语句,用于确保传入的指针psl
不为空(不为NULL
)。如果psl
为空,断言会失败,导致程序中止。这个断言旨在确保在执行扩容操作之前,传入的顺序表指针有效,以防止出现潜在的问题。 -
if (psl->size == psl->capacity)
:这是一个条件判断,用于检查顺序表的size
(当前有效数据的数量)是否等于capacity
(顺序表的容量,即可以容纳的最大元素数量)。如果相等,说明当前容量不足以容纳更多的元素,需要进行扩容。 -
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
:在需要扩容时,计算新的容量newCapacity
。如果当前容量是0(表示顺序表为空),则将新容量设置为4;否则,将新容量设置为当前容量的两倍,以扩大顺序表的容量。 -
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);
:使用realloc
函数尝试重新分配内存,将顺序表的a
成员扩展到新的容量newCapacity
。realloc
函数首先尝试在原有内存块上扩展空间,如果失败,则会在堆上分配新的内存块,并将数据从旧内存块复制到新内存块。tmp
是一个临时指针,用于指向重新分配后的内存块。同时相较于mealloc
开辟新的空间相比,realloc
可以保证新开辟的内存不存在异地内存的可能性,这也节省了一些运行时间,其中必须sizeof(SLDataType)*newCapacity
如果不乘sizeof(SLDataType)
,不能保证增加的每个空间开辟了需要的4个字节的空间。 -
if (tmp == NULL)
:在尝试扩容后,检查tmp
是否为NULL
。如果tmp
为NULL
,说明内存分配失败,通常是由于内存不足导致的。在这种情况下,程序会输出错误消息并返回,扩容操作失败。 -
psl->a = tmp;
:如果扩容成功,将psl
的a
成员指向新分配的内存块,即tmp
所指向的地址。 -
psl->capacity = newCapacity;
:更新顺序表的capacity
成员,将其设置为新的容量newCapacity
。
总之,SLCheckCapacity
函数的作用是在需要时扩展顺序表的容量,以容纳更多的元素。它通过检查当前有效数据的数量和容量的关系来判断是否需要扩容,然后使用 realloc
函数进行内存的重新分配。这个函数确保顺序表能够动态地扩展其容量,以适应不断增长的数据集合。
顺序表的从头插入和从尾插入
void SLPushBack(SL* psl, SLDataType x) {
assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;//将a数组内下标为size的内容变成x,因为size代表的是最后的数据下标
psl->size++;
}
void SLPushFront(SL* psl, SLDataType x) {
assert(psl);
SLCheckCapacity(psl);
int end = psl->size - 1;
//挪动数据
while (end >= 0) {
psl->a[end + 1] = psl->a[end];//将后面的数据向前移动一个
--end;
}
psl->a[0] = x;
psl->size++;
}
上面的两个函数 SLPushBack
和 SLPushFront
用于在顺序表的尾部和头部插入元素,它们有以下不同的功能:
-
SLPushBack
函数:SLPushBack
的作用是将一个元素x
插入到顺序表的尾部。- 首先,函数使用
assert
断言来确保传入的顺序表指针psl
不为空。 - 然后,调用
SLCheckCapacity(psl)
来检查顺序表的容量是否足够,如果容量不足,会自动扩容。 - 接下来,将元素
x
插入到顺序表的尾部,即在数组a
的索引为psl->size
的位置处赋值为x
,并将size
增加1,表示有效数据数量增加了一个元素。
-
SLPushFront
函数:SLPushFront
的作用是将一个元素x
插入到顺序表的头部。- 同样,函数开始使用
assert
断言来确保传入的顺序表指针psl
不为空。 - 然后,调用
SLCheckCapacity(psl)
来检查顺序表的容量是否足够,如果容量不足,会自动扩容。 - 接下来,函数使用循环将顺序表中的元素向后挪动一个位置,以为新元素
x
腾出空间。 - 最后,将元素
x
插入到顺序表的头部,即在数组a
的索引为0的位置处赋值为x
,并将size
增加1,表示有效数据数量增加了一个元素。
顺序表的从头删除和从尾删除
void SLPopBack(SL* psl) {
assert(psl);
//温柔的检查
//if (psl->size == 0) {
// return;
//}
//暴力的检查
assert(psl->size > 0);
//psl->a[psl->size - 1] = -1;//但是-1已经把这个数给写死了,不能改变数据
psl->size--; //光光改变size时,当减到0的时候,size会减到负数,后续加数据会从-1开始,导致数据错误
//所以要在函数中保证psl中size不能是负数
}
void SLPopfront(SL* psl) {
assert(psl);
assert(psl->size > 0);
int begin = 1;
while (begin < psl->size) {
psl->a[begin - 1] = psl->a[begin];
++begin;
}
psl->size--;
}
上面的两个函数 SLPopBack
和 SLPopfront
分别用于从顺序表的尾部和头部删除元素。它们的作用如下:
-
SLPopBack
函数:SLPopBack
的作用是从顺序表的尾部删除一个元素。- 首先,函数使用
assert
断言来确保传入的顺序表指针psl
不为空。 - 接下来,函数使用
assert(psl->size > 0)
断言来检查顺序表中是否有有效的数据。如果顺序表的size
为0,即没有有效数据,函数将无法继续执行删除操作,这是一种温柔的检查。 - 然后,函数将顺序表的
size
成员减1,表示有效数据数量减少一个元素,相当于从尾部删除了一个元素。这是通过减小size
来实现的,不会直接操作数组元素的值。
-
SLPopfront
函数:SLPopfront
的作用是从顺序表的头部删除一个元素。- 同样,函数开始使用
assert
断言来确保传入的顺序表指针psl
不为空。 - 然后,函数使用
assert(psl->size > 0)
断言来检查顺序表中是否有有效的数据,如果size
为0,函数将无法继续执行删除操作。 - 接下来,函数使用一个循环将顺序表中的元素向前挪动一个位置,以覆盖被删除的元素。这个过程将元素逐个向前移动,直到删除的元素从顺序表中被完全覆盖。
- 最后,将顺序表的
size
成员减1,表示有效数据数量减少一个元素,相当于从头部删除了一个元素。
顺序表的任意位置的删除和插入
void SLInsert(SL* psl, int pos, SLDataType x) {
assert(psl);
assert(pos >= 0 && pos <= psl->size);
SLCheckCapacity(psl);
int end = psl->size - 1;
while (end >= pos) {//保证了在pos之后的内容都向后移动一格
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[pos] = x;//再将元素插入顺序表
psl->size++;
}
void SLErase(SL* psl, int pos) {
assert(psl);
assert(pos >= 0 && pos < psl->size);//size的位置指向空,所以删除就没有意义
//挪动覆盖
int begin = pos + 1;
while (begin < psl->size) {
psl->a[begin - 1] = psl->a[begin];//可以直接覆盖,不需要删除
++begin;
}
psl->size--;
}
上面的两个函数 SLInsert
和 SLErase
分别用于在顺序表中插入元素和删除元素,下面是它们的实现:
-
SLInsert
函数:SLInsert
的作用是在顺序表的指定位置pos
处插入一个元素x
。- 首先,函数使用两个
assert
断言来确保传入的顺序表指针psl
不为空,并且pos
的值在有效范围内,即大于等于0且小于等于当前有效数据数量psl->size
。 - 接下来,函数调用
SLCheckCapacity(psl)
来检查容量是否足够,如果需要,会自动进行扩容。 - 然后,函数使用一个循环将顺序表中从位置
pos
开始的元素都向后挪动一个位置,以为新元素x
腾出位置。 - 最后,将元素
x
插入到位置pos
处,更新size
成员,表示有效数据数量增加了一个元素。
-
SLErase
函数:SLErase
的作用是从顺序表中删除指定位置pos
处的元素。- 同样,函数使用两个
assert
断言来确保传入的顺序表指针psl
不为空,并且pos
的值在有效范围内,即大于等于0且小于当前有效数据数量psl->size
。 - 接下来,函数使用一个循环将顺序表中从位置
pos+1
开始的元素都向前覆盖一个位置,以删除指定位置的元素。 - 最后,将
size
成员减1,表示有效数据数量减少了一个元素。