1.是什么
顺序表是一种基本的数据结构,它使用一组连续的内存空间来存储数据元素,这些元素在逻辑上也是连续的。
顺序表中,每个元素都占据一个特定的位置,可根据位置找到元素。
在生活中常见于各种联系人列表,如QQ群的群成员列表。
2.静态顺序表
2.1实现
根据顺序表的要求,可使用数组存储成员,并添加一个变量表个数:
struct SeqList
{
int a[20];
int size;
};
在这个简单的实现中,定义了一个结构体SeqList
,其中,包含了一个固定大小的数组a
和一个整型变量size
。
数组a
用于存储实际的数据,而size
则记录了当前数组中的数据数量。
上面的顺序表比较简陋,可以加点东西修饰:
- 表示成员个数
#define N 20
- 表示成员类型
typedef int SLDataType
改版:
struct SeqList
{
SLDataType a[N];
int size;
};
这样的改进可使成员类型和最初数量的修改更加高效。
关于命名:
SeqList
为sequence
与list
的缩写,表示顺序表SLDataType
为SeqList
与DataType
的缩写,表示顺序表的成员类型
2.2缺点
静态顺序表的缺点十分明显,即它的成员数量是固定的。
使用静态顺序表永远无法准确满足需求,如果预先分配的空间过大,会造成内存浪费;如果分配的空间过小,可能会导致空间不足。
综上,我们需要一种更灵活的方法,即动态顺序表。
3.动态顺序表
3.1总览
- 动态顺序表是一种更加灵活的顺序表实现方式,可根据需要动态地调整存储空间大小。
- 数据结构中最主要的就是对数据进行管理,而管理的需求一般分为四类:增、删、查、改。这同样是实现动态顺序表时必须完成的部分。
- 为了更好地组织代码,动态顺序表在实现时建议放入三个文件中:
SeqList.h | SeqList.c | test.c |
---|---|---|
头文件 | 源文件 | 测试文件 |
引用头文件,函数声明,宏定义,结构体定义 | 具体的函数实现 | 验证功能是否正确 |
.c
文件都只需引用SeqList.h
,可避免头文件的重复引用。
注:引用自建的头文件使用双引号:#include "SeqList.h"
3.2动态顺序表的创建
在动态顺序表中,不能再用数组,而改为指针。
且静态顺序表只准备了size
来存储当前数量,此时,还需一个变量存储当前最大数量,以免溢出。
SeqList.h
:
struct SeqList
{
SLDataType* a;
int size;
int capacity;
};
其中,当空间容量capacity
与有效数据个数size
相等,需扩容。
可重命名简化类型:
typedef struct SeqList SeqList
3.3初始化
初始化函数应该确保结构体的成员被适当地设置,并且为数据分配足够的初始空间。
SeqList.h
:
void SeqListInit(SeqList* ps);
#define INIT_CAPACITY 3
SeqList.c
:
有两种方法,法一:
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
这种方法不推荐,也意义不大,因为结构体变量创建后自动初始化为0
。
法二:
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_CAPACITY);
if (!ps->a)
{
perror("SeqListInit::malloc");
return;
}
ps->size = 0;
ps->capacity = ;INIT_CAPACITY
}
使用malloc
函数为数组a
分配初始容量大小的空间。
如果内存分配失败,将输出错误信息并返回。
最后,它将size
设置为0
,并将capacity
设置为初始容量。
关于命名:
SeqListInit
为SeqList
与initialize
的缩写,表示初始化顺序表
INIT_CAPACITY
为initialize
与capacity
的缩写,表示初始空间容量
3.4销毁
销毁函数负责释放分配给顺序表的内存。
SeqList.h
:
void SeqListDestroy(SeqList* ps);
SeqList.c
:
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
使用free
函数释放之前通过malloc
分配的内存,并将指针设置为NULL
,以防止野指针的产生。
最后,将size
和capacity
设置为0
,表示顺序表已经被清空。
注:多个等号,从右向左赋值。
3.5打印
打印函数用于输出顺序表中的所有元素。
SeqList.h
:
void SeqListPrint(const SeqList* ps);
SeqList.c
:
void SeqListPrint(const SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
}
遍历所有元素,并打印。
这里建议用const SeqList* ps
作为参数类型,表示这个函数不会修改顺序表的内容。
3.6插入
尾插
SeqList.h
:
void SeqListPushBack(SeqList* ps, SLDataType x);
SeqList.c
:
要在顺序表尾部插入一个数据特别简单,因为,当前数量size
就是最后一个数据的下一个位置,即,插入元素的下标。
可以很快写出:
void SeqListPushBack(SeqList* ps, SLDataType x)
{
assert(ps);
ps->a[ps->size] = x;
ps->size++;
}//有bug
还能将函数最后的两句合并:
ps->a[ps->size++] = x;
现在可以写个程序测试一下:
test.c
:
#include "SeqList.h"
int main()
{
SeqList s;
SeqListInit(&s);
for (int i = 0; i < 5; i++)
{
SeqListPushBack(&s, i);
SeqListPrint(&s);
printf("\n");
}
SeqListDestroy(&s);
return 0;
}
运行结果:
内容打印出来了,但是报错了,为什么?可以调试一下,最终发现是free
出了问题。
而free
出问题主要就两种情况:
1.
野指针,即从中间位置开始释放
1.野指针,即从中间位置开始释放
1.野指针,即从中间位置开始释放
2.
f
r
e
e
的空间有越界
2.free的空间有越界
2.free的空间有越界
在这里不可能是情况一,毕竟ps->a
从未改过。
因此,空间越界,检查一下SeqListPushBack
函数,可以发现,插入前,需检查是否需要扩容。
修改后:
void SeqListPushBack(SeqList* ps, SLDataType x)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * ps->capacity);
if (!tmp)
{
perror("SeqListPushBack::realloc");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->size++] = x;
}
在插入新元素之前,首先检查当前的size
是否等于capacity
。如果是,则说明需要扩容。
这里使用realloc
函数重新分配内存,并将新分配的内存地址赋值给a
。如果内存分配失败,将输出错误信息并返回。
最后,将新元素插入到数组的末尾,并将size
加一。
头插
与尾插的实现基本类似。
SeqList.h
:
void SeqListPushFront(SeqList* ps, SLDataType x);
SeqList.c
:
void SeqListPushFront(SeqList* ps, SLDataType x)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * 2 * ps->capacity);
if (!tmp)
{
perror("SeqListPushBack::realloc");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
for (int i = ps->size; i > 0; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[0] = x;
ps->size++;
}
在头插操作中,首先检查是否需要扩容。如果需要扩容,则按照尾插的方式进行。
接着,使用一个循环将已有的元素向后移动一位,以便为新元素腾出头部位置。
最后,将新元素插入到数组的起始位置,并将size
加一。
测试一下:
test.c
:
#include "SeqList.h"
int main()
{
SeqList s;
SeqListInit(&s);
for (int i = 0; i < 5; i++)
{
SeqListPushFront(&s, i);
SeqListPrint(&s);
printf("\n");
}
SeqListDestroy(&s);
return 0;
}
运行结果:
关于命名:
PushBack
,表尾插
PushFront
,表头插
好像STL库中是这样命名的,我还没学,不清楚。
3.7删除
尾删
SeqList.h
:
void SeqListPopBack(SeqList* ps);
SeqList.c
:
void SeqListPopBack(SeqList* ps, SLDataType x)
{
assert(ps);
//assert(ps->size > 0);
if (ps->size == 0)
return;
ps->a[ps->size-1] = 0;//不需要
ps->size--;
}
上面写了句废话:
ps->a[ps->size-1] = 0;
回顾一下顺序表的要求:从开始位置连续储存,这里是size
个数据。
我们是用size
来遍历,因此size--
就足够了,这使最后一个数据的空间不会被访问。
且这里将数据置为0
也极不合理,因为并不能确定原数据会存什么。
这里,还需防止数据被删完了还在删,有两种检查方法:
- 暴力检查
assert(ps->size > 0);
使用assert
断言数据个数大于0
,否则终止程序,并报错。
- 温柔的检查
if (ps->size == 0)
return;
当数据个数等于0
直接返回,无其他处理。
头删
SeqList.h
:
void SeqListPopFront(SeqList* ps);
SeqList.c
:
void SeqListPopFront(SeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
在头删操作中,同样先检查size
是否为0
。如果不是,则使用一个循环将后面的元素向前移动一位,以覆盖掉原本位于头部的元素。
最后,将size
减一,表示删除了一个元素。
关于命名:
PopBack
,表尾删
PopFront
,表头删
在头插,头删时,可以发现操作较为繁琐,时间复杂度为O(N)
,此时,顺序表不合适,可用链表。
希望本篇文章对你有所帮助,并激发您进一步探索数据结构和算法的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!