目录
一. 为什么要实现顺序表
我们在使用C语言编写代码的时候,如果需要处理大量数据,就需要一个数据结构来储存和管理这些数据。
我们可能会考虑建立数组来储存信息,但是数组在建立之后它的容量大小就已经确定,很多时候,我们难以确定要分配给这个数组多少元素个数,如果分配多了,就会造成空间的浪费,如果分配少了,又无法满足实际需求,这在实际应用中是非常不合适的。
下面是静态顺序表的实现,静态顺序表本身确定了大小,使用定长数组存储元素,使用范围十分有限。
#define N 100
typedef int SLDataType;
//静态顺序表
typedef struct SeqList
{
SLDataType arr[N];//静态数组
int size;//有效的元素个数
}SL;
有什么办法可以动态调整数组的大小呢?没错,就是动态内存管理。
C语言动态内存管理https://blog.youkuaiyun.com/qq_51904510/article/details/136941147
我们通过realloc实时调整分配的动态内存空间的大小,实现空间的动态分配与扩容,这样就可以减少空间的浪费,同时存储任意量的数据,不用担心空间不够的问题。
二. 顺序表的特点
1.线性表
顺序表是线性表的一种,那么什么是线性表,线性表有什么特点呢?
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 ...
线性表在逻辑上是线性结构,也就说是连续的一条直线。
但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
而顺序表的特点:在物理上连续,在逻辑上也连续
2. 顺序表的优缺点
优点:
1.随机访问,即可在O(1)时间内找到第i个元素。
2.存储密度高,每个节点只存储数据元素本身。
缺点:
1.拓展容量不方便。
2.插入、删除操作不方便,需要移动大量元素。
三. 顺序表的逐步实现
1.准备工作
我们创建三个文件,分别是:
文件 | 作用 |
---|---|
SeqList.h | 顺序表的结构体部分和函数的声明 |
SeqList.c | 顺序表进行相关操作的函数的实现 |
test.c | 主函数和测试代码 |
现在,我们在SeqList.h中写入包含的头文件:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
然后让两个.c文件分别包含.h文件,这样就将三个文件链接了起来。
2. 设计动态顺序表的结构体
我们需要一个指针来记录我们分配给顺序表的元素个数,以及实际存储有效元素的个数,最后还需要一个指针指向我们动态开辟的空间,最终,我们设计出了以下结构体并将其重命名以简化写法:
//动态顺序表
typedef struct SeqList
{
SLDataType* arr;//指向动态开辟的内存空间
int size;//有效的元素个数
int capacity;//动态开辟的元素个数
}SL;
由于我们目前需要存储的元素类型不确定,所以我们用SLDataType来重命名我们的元素类型:
typedef int SLDataType;//假设我们现在需要存放int类型的数据
3. 实现具体功能
既然我们要对大量数据进行管理,那么数据的增加、删除、查找、修改,就是必不可少的,不过在此之前,我们需要对我们的顺序表进行初始化,在使用结束后,也应该对其进行销毁,释放动态内存空间以防止内存泄漏。
以下代码放在SeqList.c中。
3.1 初始化SLInit
在顺序表使用前,我们需要对其进行初始化,需要将其元素个数设置为0,将其指针置空。
我们需要在函数中操作实参,因此函数参数传递指针是一个不错的选择。
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
3.2 销毁SLDestroy
在顺序表使用结束后,我们也需要对其进行销毁,需要释放掉其动态申请的空间,将其元素个数设置为0,并将其指针置空。
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->arr)//ps->arr不为空,证明空间未释放
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
3.3 申请动态内存空间SLCheckCapacity
下面,如果我们需要插入数据,就必须为顺序表申请动态内存空间,即扩容。
经过数学计算,我们认为每次扩容后的空间为扩容前的两倍最佳。
那么,什么情况下需要扩容呢?
我们认为在有效数据个数等于空间元素个数时需要立刻扩容。
void SLCheckCapacity(SL* ps)
{
//先判断空间够不够
if (ps->size == ps->capacity)
{
int newcapacity = 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc failed");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
于是我们写出了上面的代码,当有效数据个数等于空间元素个数时,定义新的空间元素个数为原来的两倍,并使用realloc调整空间,然后判断是否申请成功,如果申请成功,就将新的空间元素个数和新空间的地址赋给原来的;如果失败,就报错退出。
那么,这段代码万无一失了吗?其实,上面的代码仍有缺陷。
如果我们是第一次插入数据呢?原来的空间元素个数为0,乘两倍依旧是0啊!
因此,我们需要在扩容前进行判断,判断原来的空间元素个数是否为0,如果为0,就先开辟4个元素大小的空间,如果不是,就变成两倍。
void SLCheckCapacity(SL* ps)
{
assert(ps);
//在插入数据前先判断空间够不够
if (ps->size == ps->capacity)
{
//申请空间 -- 增容 --> realloc
//增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
//注意:判断capacity是否为0
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc failed");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
有的人可能会问:你直接使用realloc,假如第一次传递了空指针怎么办呢?
我们可以看关于realloc的介绍,如下:
如果传递给realloc一个空指针,这个函数的行为类似于malloc 。所以如果传递了空指针,依然可以正常进行扩容。
3.4 尾插SLPushBack
现在,我们解决了增容的问题,现在我们来进行尾插,即在数组的末尾(在有效元素之后)插入数据。
这里的参数为顺序表指针ps和要插入的数据类型SLDataType x。
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//尾插
ps->arr[ps->size++] = x;
//ps->arr[ps->size] = x;
//++(ps->size);
}
这段代码还是比较好理解的,先判断是否需要增容,然后将数据元素插入到有效元素后,然后有效元素自增。
注意:数组下标从0开始
3.5 尾删SLPopBack
尾删的代码也还是比较简单的,就是实现在尾部删除最后一个元素即可,我们只需要确定传入的顺序表指针非空,以便可以对顺序表进行操作,确定有效元素个数非零,确保顺序表中还有元素,最后直接将有效元素个数减一,即完成了尾删操作。
我们并不需要确定删除后该位置的元素是什么,因为它已经不再是一个有效的元素,在我们增加数据时会将其覆盖。
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//ps->arr[ps->size - 1] = -1;
--(ps->size);
}
3.6 头插SLPushFront
现在,如果我们需要在顺序表的头部插入数据,就会麻烦一点,因为我们需要将数组中的每个元素向后移动一位,然后将插入的数据放在第一位。
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//所有数据整体往后面移动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
3.7 头删SLPopFront
头删也是同理,需要将每个元素向前移动一位
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--(ps->size);
}
3.8 打印顺序表SLPrint
对顺序表的打印可以方便我们观察顺序表中的元素,判断我们的操作函数是否起到了相应的作用。
打印输出顺序表中的每个元素并不需要对顺序表做出任意修改,因此参数并不需要携带指针,进行实参的拷贝并传递给实参即可,然后遍历数组中的每一个元素。
注意:此处的SLDataType为int类型,如果需要更改为其他类型,需要更改相应的代码。
void SLPrint(SL s)
{
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.arr[i]);
}
printf("\n");
}
3.9 查找SLFind
对顺序表的操作同样相应对顺序表进行遍历,通过对数据的比对查找是否包含该数据,如果包含,返回元素的下标,如果不包含,返回EOF(-1)。
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return EOF;
}
3.10 任意位置前插入数据SLInsert
现在,我们需要在任意位置插入数据,需要判断传入的数组插入位置是否合法,然后判断增容,最后依次挪动该位置后面的数据并插入该数据。
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
如果pos == 0,相当于头插,如果pos == ps->size 相当于尾插 。
3.11 任意位置删除数据SLErase
任意位置删除数据仅需要将后面的数据前移一位即可。
void SLErase(SL* ps, int pos)
{
assert(ps);
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 == 0,相当于头删,如果pos == ps->size - 1 相当于尾删。
四.动态顺序表的功能测试
上面我们实现了动态顺序表进行操作的函数,好像只实现了对顺序表的增、删、查,似乎没有实现修改啊?其实,我们既然实现了任意位置的增加删除,在任意位置的修改就是删除在插入即可。
与此同时,我们在上面函数的实现的同时,也应当同时在test.c中对我们写的函数进行测试,不然当你写完了再测试如果大量报错,会打击我们编程的积极性,同时影响我们实时对代码逻辑的判断和修改,这里由于连贯性的原因,统一放在了这里。
1. 测试1
void SLtest(void)
{
SL sl;
SLInit(&sl);//初始化
for (int i = 0; i < 10; i++)
{
SLPushBack(&sl, i);//尾插1~9
SLPrint(sl);
}
for (int i = 0; i < 10; i++)
{
SLPushFront(&sl, i);//头插1~9
SLPrint(sl);
}
SLPopFront(&sl);//头删
SLPrint(sl);
SLPopBack(&sl);//尾删
SLPrint(sl);
SLDestroy(&sl);//销毁
}
2. 测试2
void SLtest2()
{
SL sl;
SLInit(&sl);//初始化
for (int i = 0; i < 10; i++)
{
SLPushBack(&sl, i);//尾插1~9
}
SLPrint(sl);
SLErase(&sl, 0);//删掉第一个元素
SLPrint(sl);
SLInsert(&sl, 1, 10);//在第二个元素前插入10
SLPrint(sl);
int ret = 0;
ret = SLFind(&sl, 5);//查找5这个元素
printf("ret=%d\n", ret);
ret = SLFind(&sl, 11);//查找11这个元素
printf("ret=%d\n", ret);
SLDestroy(&sl);//销毁
}
五.参考代码
1. SeqList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
//动态顺序表
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
//顺序表的初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
//头部插入数据
void SLPushFront(SL* ps, SLDataType x);
//尾部插入数据
void SLPushBack(SL* ps, SLDataType x);
//打印顺序表
void SLPrint(SL s);
//头部删除数据
void SLPopFront(SL* ps);
//尾部删除数据
void SLPopBack(SL* ps);
//任意位置前插入数据
void SLInsert(SL* ps, int pos, SLDataType x);
//任意位置删除数据
void SLErase(SL* ps, int pos);
//顺序表的查找
int SLFind(SL* ps, SLDataType x);
2. SeqList.c
#include "SeqList.h"
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
//在插入数据前先判断空间够不够
if (ps->size == ps->capacity)
{
//申请空间 -- 增容 --> realloc
//增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
//注意:判断capacity是否为0
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc failed");
exit(1);
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
在插入数据前先判断空间够不够
//if (ps->size == ps->capacity)
//{
// //申请空间 -- 增容 --> realloc
// //增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
// //注意:判断capacity是否为0
// int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
// SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
// if (tmp == NULL)
// {
// perror("realloc failed");
// exit(1);
// }
// ps->arr = tmp;
// ps->capacity = newcapacity;
//}
SLCheckCapacity(ps);
//尾插
ps->arr[ps->size++] = x;
//ps->arr[ps->size] = x;
//++(ps->size);
}
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//所有数据整体往后面移动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
void SLPrint(SL s)
{
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.arr[i]);
}
printf("\n");
}
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//ps->arr[ps->size - 1] = -1;
--(ps->size);
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
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);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return EOF;
}
3. test.c
#include "SeqList.h"
void SLtest(void)
{
SL sl;
SLInit(&sl);//初始化
for (int i = 0; i < 10; i++)
{
SLPushBack(&sl, i);//尾插1~9
SLPrint(sl);
}
for (int i = 0; i < 10; i++)
{
SLPushFront(&sl, i);//头插1~9
SLPrint(sl);
}
SLPopFront(&sl);//头删
SLPrint(sl);
SLPopBack(&sl);//尾删
SLPrint(sl);
SLDestroy(&sl);//销毁
}
void SLtest2()
{
SL sl;
SLInit(&sl);//初始化
for (int i = 0; i < 10; i++)
{
SLPushBack(&sl, i);//尾插1~9
}
SLPrint(sl);
SLErase(&sl, 0);//删掉第一个元素
SLPrint(sl);
SLInsert(&sl, 1, 10);//在第二个元素前插入10
SLPrint(sl);
int ret = 0;
ret = SLFind(&sl, 5);//查找5这个元素
printf("ret=%d\n", ret);
ret = SLFind(&sl, 11);//查找11这个元素
printf("ret=%d\n", ret);
SLDestroy(&sl);//销毁
}
int main()
{
SLtest();
SLtest2();
return 0;
}