顺序表的实现
1.概念与结构
概念:顺序表是一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下使用数组存储。
顺序表的底层结构是数组,对数组进行了封装,实现了常用的增删改查等接口。
2.顺序表的分类
2.1静态顺序表
#define N 1000
typedef int SLDataType;
struct SeqList
{
SLDataType arr[N];
int size;//数据元素的个数
int capacity;//顺序表的容量
};
这样我们就定义好了一个静态的顺序表,我们使用typedef
给顺序表的元素类型重命名了,这样当我们需要修改数据元素类型的时候,可以减少修改的代码量,当然我们可以使用typedef
关键字来给顺序表结构体命名来简化代码。
typedef struct SeqList
{
SLDataType arr[N];
int size;//数据元素的个数
int capacity;//顺序表的容量
}SL;
SL就是重命名的类型名,后续的代码可以直接使用SL来定义一个顺序表的结构体类型,当然我们也可以单独的给定义好的结构体进行类型重命名。
struct SeqList
{
SLDataType arr[N];
int size;
int capacity;
}SL;
typedef struct Seqlist SL;
动态顺序表有一个缺点就是,顺序表的容量是事先定义好的,空间给少了不够用,给多了造成浪费,这在实际的开发的过程中是一个很大的问题,于是我们引出了动态顺序表。
2.2 动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
这就是动态顺序表的定义。动态顺序表通过动态内存管理可以实现空间的自由的申请和释放,大大避免了静态顺序表的弊端。接下来我们将实现动态顺序表的各种接口。
3.动态顺序表的实现
3.1顺序表的初始化
算法:
- 把arr指向NULL
- 把size置为0
- 把capacity置为0
void SLInit(SL s)
{
s.arr = NULL;
s.size = 0;
s.capacity = 0;
}
这就是初始化的代码,让我们调试一下。
void test()
{
SL sl;
SLInit(sl);
}
int main()
{
test();
}
还没有开始调试直接弹出bug。
我们的目的就是初始化顺序表的结构体变量,但是在使用初始化函数结构体之前,因为变量使用前必须初始化而报错,这是很搞得的一件事情。但是我们为了测试这个函数,还是强行的随意赋值一下。
void test()
{
SL sl;
sl.size = 2;
SLInit(sl);
}
int main()
{
test();
}
F10开始调试,调试结果如下。
这里首先要注意的是,sl是实参的名字,s是形参的命名,调试结果显示:实参sl并没有被初始化函数初始成功,被修改的还是我们为了测试函数作用而给实参成员随意赋的值,这里我们才恍然大悟,形参是实参的一份临时拷贝,这个函数确实初始化了形参变量,但是函数返回后,这个函数栈帧被销毁,等于这个函数什么都没有做,所以我们修改变量的值的时候,要使用传址的方式。
void SLInit(SL* s)
{
s->arr = NULL;
s->size = 0;
s->capacity = 0;
}
void test()
{
SL sl;
SLInit(&sl);
}
int main()
{
test();
}
这是修改后的代码,下面我们调试一下。
这里实参sl被正确的初始化,说明修改后的代码是正确的。
3.2判定顺序表的空间是否足够
当顺序表的容量capacity和顺序表中有效的数据元素个数size相等的时候说明空间已经满了,这个时候我们就需要对顺序表进行扩容。先给出代码,然后进行解释。
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
int NewCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->arr,sizeof(SLDataType) * NewCapacity);
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps ->arr = tmp;
ps->capacity = NewCapacity;
}
}
- 当顺序表的size和capacity相等的时候,顺序表需要扩容,进入if判断语句
- 定义新容量的大小的时候,使用了三目操作符,意思是第一次扩容的初始值是4,后续都是2倍扩容,2倍扩容是一种高效的扩容方式。
- 然后使用realloc函数重新申请了空间并进行了非空判断,当tmp为空的时候,说明申请失败,直接退出程序。当tmp不为空,说明申请成功,把中间变量tmp的值赋给arr实现了扩容。
- 最后把NewCapacity赋给ps成员capacity实现了容量记录的更新。
这里说明一下,结构体传参的时候尽量按址传递,因为按值传递的时候,形参会拷贝同一份结构体在内存中,而结构体占用内存是相对大的,多次调用按值传递的函数时,会占用大量内存。
3.4遍历打印顺序表
给出一个顺序表
由上表可知,我们只需要从下标为0打印到下标为size-1即可完成顺序表的遍历打印
void SLPrint(SL* ps)
{
int i = 0;
for(i;i<=ps->size-1;i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
这就是遍历打印的代码,无需过多的解释。
3.4尾插
尾插就是在顺序表的尾部插入一个数据元素。
由图可知,尾插元素的下标就是size,我们插入后记得把size++,下面给出代码。
void SLPushBack (SL* ps, SLDataType x)
{
assert(ps != NULL);
SLCheckCapacity(ps);
ps->arr[ps->size] = x;
ps->size++;
}
- 想要插入元素首先进行顺序表的容量检查,调用一下SLCheckCapaity函数。
- assert的参数为假的时候就是中断程序并报错。
- 尾插的时间复杂度为O(1)
这里使用打印函数测试一下尾插
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPrint(&sl);
}
3.5头插
头插顾名思义就是在顺序表的头部插入一个数据元素,这里头部就是下标为0的位置。
下面给出头插的图示和代码
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
int i = 0;
for (i = ps->size - 1; i >= 0, i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = x;
ps->size++;
}
- 头插前同样要判定顺序表的容量
- 我把第一个需要移动的数据元素的下标赋值给i,i>=0这个条件由图可以看出。
- 把元素插入到下标为0的位置后别忘了size++。
- 头插的时间复杂度为O(n)
下面测试一下头插。
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushFront(&sl, 0);
SLPrint(&sl);
}
头插成功!
3.6尾删
尾删就是删除线性表的最后一个数据。
void SLPopBack(SL* ps)
{
if (ps == NULL)
{
return;
}
assert(ps->size > 0);
ps->size--;
}
- 删除的条件首先是顺序表中有数据让你删,所以size要大于0。
- assert(ps)也可以使用if语句,效果相同。
- 计算机严格来说不存在“删除”这个词,因为存储器中存储的都是1和0,并且存储器的容量是固定的,不会越删越小,我们经常说的删除实际就是放弃对某块内存数据的管理,或者直接覆盖。
- 这里我们直接size–就可以了,下次进行插入的时候,数据就会被覆盖了。
- 尾删的时间复杂度为O(1)
测试一下。
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPopBack(&sl);
SLPrint(&sl);
}
测试成功。
3.7头删
头删就是删除下标为0的数据元素。
void SLPopFront(SL* ps)
{
if (ps == NULL)
{
return;
}
assert(ps->size > 0);
int i = 0;
for (i = 1; i <= ps->size - 1; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
- 头删同样要判断size是否要大于0。
- 头的数据是被直接”覆盖“了。
- 我把要移动的第一个元素的下标赋给i,由图可以看出i<=size-1;
- 头删的时间复杂度为O(n)
测试一下
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPopFront(&sl);
SLPrint(&sl);
}
测试成功!
3.8在顺序表中查找指定元素
遍历查找就可以啦,这个比较简单。
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
int i = 0;
for (i = 0; i <= ps->size - 1; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
- 遍历的下标为0 到size -1;
- 跳出循环,说明没有找到对应的元素,返回-1是因为和数组的下标区分。
测试一下
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
int find1 = SLFind(&sl, 2);
int find2 = SLFind(&sl, 99);
printf("%d %d", find1, find2);
//SLPrint(&sl);
}
测试成功!
3.9在指定的位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size );
SLCheckCapacity(ps);
int i = 0;
for (i = ps->size - 1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size++;
}
- pos 的位置必须在下标的范围内,当pos等于0的时候相当于头插,当pos等于size的时候相当于尾插。
- 插入前必要检查顺序表的容量,插入后size++。
- 我把第一个要移动的元素的下标赋值给i,由图看出,i>=pos。
- 时间复杂度最坏的情况下为O(n)
测试一下
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLInsert(&sl, 4, 99);
SLPrint(&sl);
}
测试成功!
3.10 删除指定位置的元素
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
int i = 0;
for (int i = pos + 1; i <= ps->size - 1; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
- 这个函数的pos的取值和上一个函数的pos不同,这个函数的pos只能取下标范围内的,而上一个可以取到size实现尾插。
- 我把第一个要移动元素的下标赋值给i,由图看出i<=size-1。
测试一下
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLErase(&sl, 2);
SLPrint(&sl);
}
测试成功!
3.11销毁顺序表
顺序表实际是数组的封装,销毁顺序表的第一步首先是销毁被封装的数组。
void SLDesTroy(SL* ps)
{
if (ps->arr)
free(ps->arr);
ps -> arr = NULL;
ps->size = ps->capacity = 0;
}
测试一下
void test()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLDesTroy(&sl);
SLPrint(&sl);
}
当尾插后的顺序表被摧毁后,什么都没有打印。
这篇文章是我的第一篇关于数据结构的文章,码出来的目的是为了梳理思路加强学习效果并分享给有需要的朋友,本人的水平有限,文章有纰漏的地方,欢迎大家指正和交流。