本篇的内容是使用C语言实现一个动态顺序表
文章目录
顺序表的概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
我们可以把顺序表理解为一个数组
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储。
- 动态顺序表:使用动态开辟的数组存储。
静态顺序表的结构:
#define N 100
struct SeqList
{
int a[N];//定长的数组
int size;//有效数据的个数
};
我们使用结构体来表示一个顺序表,因为是静态顺序表,所以我们就直接创建了一个长度不变的数组,然后再用size这个变量来存储该顺序表中有效数据的个数。
动态顺序表的结构:
struct SeqList
{
int* a;//指向申请的空间
int size;//有效数据的个数
int capacity;//容量
};
动态顺序表与静态顺序表不同的是我们创建的数组大小是可变的,所以我们在结构体中又多增加了一个成员来存储当前我们创建出的数组的容量。
动态顺序表的接口实现
下面我们以动态顺序表为例,在实现顺序表之前,我们首先要创建一个结构体类型来创建我们的顺序表。
typedef int SeqDataType;
typedef struct SeqList
{
SeqDataType* a;//指向申请的空间
int size;//有效数据的个数
int capacity;//容量
}SeqList;
我们用typedef定义一个类型,以方便我们以后来创建别的元素类型的顺序表。为了后面我们写代码方便一些,我又用typedef重命名了这个结构体类型为SeqList。
我们动态顺序表的基本功能要能够做到在顺序表中的增删改查,下面我们来挨个实现它的接口。
初始化接口
在我们创建了一个顺序表并且要对它进行各种操作之前,我们首先要初始化这个顺序表,因为我们用结构体类型创建出来的顺序表没有经过初始化,它里面的内容就是随机的,不便于我们对它进行操作。下面是初始化的代码
void SeqListInit(SeqList* pq)
{
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
assert是断言我们传入的地址不是空指针。
我们把我们创建出来的结构体变量的地址传到初始化函数中,这时候我们还没有在内存中开辟数组来存放数据,所以我们把a指向空指针,有效数据个数和数组的容量都置为0。
销毁接口
因为我们要做到我们数组的容量是可变的,所以就要用到动态内存开辟,而使用动态内存开辟的空间结束后我们就要对开辟的空间进行释放,否则就会造成内存泄漏,所以我们就需要一个销毁接口在我们使用完顺序表后对顺序表进行销毁
void SeqListDestory(SeqList* pq)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->capacity = pq->size = 0;
}
我们先用free释放我们的数组,然后再让数组指向空指针,容量和有效数据长度都置为0。
打印接口
当我们想要看到我们顺序表里存储的元素时,打印接口可以实现对这个顺序表进行打印
void SeqListPrint(SeqList* pq)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
printf("%d ", pq->a[i]);
}
printf("\n");
}
我们使用一个for循环,循环次数为有效数据的个数,然后对我们数组中的元素挨个打印,以实现打印顺序表的功能。
检查是否增容接口
在我们增加这个顺序表的数据时,有可能会增加顺序表的容量,那么我们就可以写一个检查是否需要增容的接口,在我们增加数据时使用它。
改变数组的大小我们要使用的函数肯定是realloc,但是在给数组增容时,我们又面临这样一个问题,就是我们要增加多大的容量,我们知道,数组增容是有代价的,如果我们一个数据一个数据的增,那会带来大量的性能消耗,而如果我们一次性增加大量的空间,又可能造成空间的浪费,这是顺序表的一个缺点,我们无法彻底解决这个问题,所以只能选取一个折中的方法,使增加的容量的大小为原来空间的两倍,这样既不会频繁的给数组增容,而又不会浪费太多的空间。
下面是检查是否增容接口的代码:
void SeqCheckCapacity(SeqList* pq)
{
if (pq->size == pq->capacity)
{
int newcapacity = pq->capacity == 0 ? 4 : pq->capacity * 2;
SeqDataType* newA = realloc(pq->a, sizeof(SeqDataType) * newcapacity);//realloc定义,传NULL相当于malloc
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
首先进行判断是否需要增容,如果我们有效数据等于我们数组的容量,那么说明数组以及存满了,就需要进行增容,而增容时还有一种情况是我们的数组本来就什么也没有,即刚刚初始化好的数组,我们要在这个新数组增加数据时必须要对其进行扩容,我们把这两种情况整合在一起,使用三目操作符,如果这是空的数组,就创建一个含4个元素的数组,如果不是空数组,就创建元素是之前两倍大小的数组,并且改变数组大小也可能失败,用一条判断语句来处理。
尾插接口
尾插接口的功能是在顺序表的尾部插入一个数据
void SeqListPushBack(SeqList* pq, SeqDataType x)
{
assert(pq);
SeqCheckCapacity(pq);
pq->a[pq->size] = x;
pq->size++;
}
在我们对顺序表进行尾插时,首先要对顺序表是否需要增容进行判断,即使用到了上面判断是否增容的接口,然后我们把序表最后一个元素的后一个元素置成我们指定的值,因为插入了一个元素,有效数据个数加一。
头插接口
即在顺序表头部插入一个数据
void SeqListPushFront(SeqList* pq, SeqDataType x)
{
assert(pq);
SeqCheckCapacity(pq);
int end = pq->size - 1;
while (end >= 0)
{
pq->a[end + 1] = pq->a[end];
end--;
}
pq->a[0] = x;
pq->size++;
}
与尾插一样,我们也要对是否需要增容进行判断,在我们头增时,我们要先把顺序表的每一位都向后挪,然后才能在头部插入数据,而移动时如果我们从前往后挪,就会把所有元素都变成第一个元素。所以我们只能从最后的一个元素从后向前挪。
我们先找到最后一个元素,然后依次把前一个元素向后移动一位,知道移动到第一个元素,然后把我们指定的值置为第一个元素,有效元素个数加一。
尾删接口
删除顺序表最后一个元素
void SeqListPopBack(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
--pq->size;
}
尾删之前,我们先要判断顺序表中是否还有元素可以进行删除,也可以和判断顺序表地址是否为空一样用assert断言,因为是尾删,我们只需要使有效数据的个数减一即可实现删除尾部数据的目的。
在我们尾删时,我们并没有真正删除最后一个数据,为什么这样也可以实现尾删呢?因为在我们打印顺序表时,和增加顺序表容量时,我们都认为我们的数组中只有size个有效数,而在物理内存中在size个数后面存储的数据并不重要,所以尾删时我们虽然没有实际删除最后一个数据,但是我们认为它以及不再是我们顺序表中的内容了,和后面存储的随机值一样没有意义,所以不需要对它进行真正的删除
头删接口
从顺序表头部删除一个数据
void SeqListPopFront(SeqList* pq)
{
assert(pq);
assert(pq->size > 0);
int i = 0;
while (i<pq->size-1)
{
pq->a[i] = pq->a[i + 1];
i++;
}
pq->size--;
}
与尾删相同,我们也要对顺序表的数据个数进行判断,头删时,我们只需要让每一个元素往前挪一位,把第一位覆盖掉即可实现头删,别忘记头删结束把有效数据个数减一。
查找接口
即在顺序表中查找某一个元素
int SeqListFind(SeqList* pq, SeqDataType x)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
if (pq->a[i] == x)
{
return i;
}
}
return -1;
}
遍历这个数组,寻找指定的值,如果找到了返回该值的下表,找不到就返回-1。
在指定位置插入元素接口
在指定的位置插入一个数据
void SeqListInsert(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos <= pq->size);
SeqCheckCapacity(pq);
int end = pq->size - 1;
while (end >= pos)
{
pq->a[end + 1] = pq->a[end];
end--;
}
pq->size++;
pq->a[pos] = x;
}
我们首先要断言我们指定的位置是是数组的有效数据,因为要插入数据,还需要判断是否需要增容,然后和头增一样,从指定的位置pos开始,从后向前把元素向后挪一位,然后把pos位置上的元素置为指定的值。
删除指定位置数据接口
把指定位置上的元素删除
void SeqListErase(SeqList* pq, int pos)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
int i = pos;
while (i < pq->size - 1)
{
pq->a[i] = pq->a[i + 1];
i++;
}
pq->size--;
}
与头删类似,首先判断pos的位置,然后找到指定位置pos,然后把pos后面的位置都向前挪动一位,覆盖pos位上的元素,把有效数据个数减一。
修改指定位置元素接口
即修改指定位置的值为指定的值
void SeqListModify(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}
非常简单,先判断指定位置的合理性,然后把pos位置上的值修改就好了。
动态顺序表代码
我们把以上接口都整合起来,就可以实现一个动态的顺序表了,在整合时,我们一般把函数的声明和所以要用到的库函数都都放在头文件,这样我们只需要在别的源文件中包含这个头文件就相当于在源文件中包含了这些库函数可声明了这些函数。
#pragma once的作用是当我们重复调用某个库函数时,程序也只会调用一次,防止我们多次调用库函数。
// SeqList.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int SeqDataType;
typedef struct SeqList
{
SeqDataType* a;//指向申请的空间
int size;//有效数据的个数
int capacity;//容量
}SeqList;
//初始化接口
void SeqListInit(SeqList* pq);
//销毁接口
void SeqListDestory(SeqList* pq);
//打印接口
void SeqListPrint(SeqList* pq);
//尾插接口
void SeqListPushBack(SeqList* pq, SeqDataType x);
//头插接口
void SeqListPushFront(SeqList* pq, SeqDataType x);
//尾删接口
void SeqListPopBack(SeqList* pq);
//头删接口
void SeqListPopFront(SeqList* pq);
//查找接口
int SeqListFind(SeqList* pq, SeqDataType x);
//在指定位置插入元素接口
void SeqListInsert(SeqList* pq, int pos, SeqDataType x);
//指定位置删除接口
void SeqListErase(SeqList* pq, int pos);
//修改接口
void SeqListModify(SeqList* pq, int pos, SeqDataType x);
接口的实现可以放在一个源文件中。
并且我们发现起始尾删和头删其实可以算做删除指定位置的两种特殊情况,尾增和头增也可以算作在指定位置增加的两种特殊情况,那么我们就可以对这四个接口进行进一步的修改。
// SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SeqListInit(SeqList* pq)
{
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
void SeqListDestory(SeqList* pq)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->capacity = pq->size = 0;
}
void SeqListPrint(SeqList* pq)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
printf("%d ", pq->a[i]);
}
printf("\n");
}
void SeqCheckCapacity(SeqList* pq)
{
if (pq->size == pq->capacity)
{
int newcapacity = pq->capacity == 0 ? 4 : pq->capacity * 2;
SeqDataType* newA = realloc(pq->a, sizeof(SeqDataType) * newcapacity);//realloc定义,传NULL相当于malloc
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
void SeqListPushBack(SeqList* pq, SeqDataType x)
{
//assert(pq);
//SeqCheckCapacity(pq);
//pq->a[pq->size] = x;
//pq->size++;
SeqListInsert(pq, pq->size, x);
}
void SeqListPushFront(SeqList* pq, SeqDataType x)
{
//assert(pq);
//SeqCheckCapacity(pq);
//int end = pq->size - 1;
//while (end >= 0)
//{
// pq->a[end + 1] = pq->a[end];
// end--;
//}
//pq->a[0] = x;
//pq->size++;
SeqListInsert(pq, 0, x);
}
void SeqListPopBack(SeqList* pq)
{
//assert(pq);
//assert(pq->size > 0);
//--pq->size;
SeqListErase(pq, pq->size - 1);
}
void SeqListPopFront(SeqList* pq)
{
//assert(pq);
//assert(pq->size > 0);
//int i = 0;
//while (i<pq->size-1)
//{
// pq->a[i] = pq->a[i + 1];
// i++;
//}
//pq->size--;
SeqListErase(pq, 0);
}
int SeqListFind(SeqList* pq, SeqDataType x)
{
assert(pq);
for (int i = 0; i < pq->size; i++)
{
if (pq->a[i] == x)
{
return i;
}
}
return -1;
}
void SeqListInsert(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos <= pq->size);
SeqCheckCapacity(pq);
int end = pq->size - 1;
while (end >= pos)
{
pq->a[end + 1] = pq->a[end];
end--;
}
pq->size++;
pq->a[pos] = x;
}
void SeqListErase(SeqList* pq, int pos)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
int i = pos;
while (i < pq->size - 1)
{
pq->a[i] = pq->a[i + 1];
i++;
}
pq->size--;
}
void SeqListModify(SeqList* pq, int pos, SeqDataType x)
{
assert(pq);
assert(pos >= 0 && pos < pq->size);
pq->a[pos] = x;
}
然后把程序的测试代码放在另一个源文件中
// test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList()
{
SeqList s;
// 测试处
}
int main()
{
TestSeqList();
return 0;
}
我们可以在这个代码的测试处测试我们的各种接口,并且进行调试。
如果觉得这个代码还比较简陋,大家还可以在这里添加一些边角料使这个代码跟精美,比如菜单,使用一个switch···case语句即可实现,非常简单这里就不写了,但是不建议大家在最开始就写上菜单,因为那样的话在我们调试接口时就会非常麻烦,可以先写一个简单的调试函数,然后调试好了以后再加这些东西。
以上就是本篇的全部内容