【C语言实现简单数据结构】顺序表
心有所向,日复一日,必有精进
文章目录
线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表和链表的物理存储结构:
顺序表
2.1概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素。
![](https://i-blog.csdnimg.cn/blog_migrate/ebd2fc24d0df53e52e50b974a2baa62c.png)
- 动态顺序表:使用动态开辟的数组存储。
![](https://i-blog.csdnimg.cn/blog_migrate/585b276f1f4b32a2ddba0e170c291c75.png)
2.2 接口实现(增删查改)
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空
间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间
大小,所以下面我们实现动态顺序表。
typedef int SLDataType;
//顺序表的动态开辟
typedef struct SeqList
{
SLDataType* arr;//动态开辟
int size ; //数据个数
int capacity; //数据容量
}SL;
// 顺序表初始化
void SeqListInit(SeqList* psl);
// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SL* ps);
// 检查空间,如果满了,进行增容
void CheckCapacity(SL* ps);
// 顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SL* ps);
// 顺序表头插
void SeqListPushFront(SL* ps, SLDataType x);
// 顺序表头删
void SeqListPopFront(SL* ps);
// 顺序表查找
int SeqListFind(SL* ps, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SL* ps, size_t pos);
// 顺序表销毁
void SeqListDestory(SL* ps);
// 顺序表打印
void SeqListPrint(const SL*ps);
2.3基本函数实现(增删查改)
2.3.1顺序表初始化
//顺序表初始化
void SeqListInit(SL* ps){
assert(ps); //检查指针
ps->arr = NULL; //置空
ps->size = 0; //数据个数
ps->capacity = 0;//容量
}
2.3.2检查空间,如果满了,进行增容
// 检查空间,如果满了,进行增容
void CheckCapacity(SL* ps){
//为什么2倍?
if (ps->size == ps->capacity){
int newCapacity = (ps->capacity == 0) ? 4 : ps->capacity * 2;
int* tmp = (SLDataType*)realloc(ps->arr, newCapacity*sizeof(SLDataType));
//判断开辟是否成功
if (tmp == NULL){
perror("ralloc");
return;
//exit(-1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
这里有个问题,为什么开辟2倍的空间内?
2倍比较合适,但是其他也可以,有个度就好了,
- 如果一次性扩容太多,存在空间浪费,浪费的太多,
- 如果一次扩容太少,就可能频繁扩容,可能导致效率下降。
2.3.3 顺序表尾插
// 顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x){
assert(ps);
CheckCapacity(ps);
ps->arr[ps->size] = x;
ps->size++;
}
2.3.4顺序表尾删
// 顺序表尾删
void SeqListPopBack(SL* ps){
assert(ps);
if (ps->size == 0){
printf("已经没有可以删的了内");
return;
}
ps->size--;
}
删除的时候不要想着把
realloc
的空间也缩小释放,
首先把删除后的空间free
了这种想法都很危险,在常见的内存错误中,就有使用free释放动态开辟内存的一部分,剩下的空间就找不到了,造成空间泄露。
如果我们在realloc一个比原开辟空间还小可以吗?
但是没必要,我们都没必要考虑,首先现在的空间大小已经足够,在开辟的时候开辟一个适当的就可以,如果减少了使用时又要开辟这时候的代价就比较大。
2.3.5 顺序表头插
// 顺序表头插
void SeqListPushFront(SL* ps, SLDataType x){
assert(ps);
CheckCapacity(ps);
for (int i = ps->size - 1; i >= 0; i--){
ps->arr[i + 1] = ps->arr[i] ;
}
ps->arr[0] = x;
ps->size++;
}
2.3.6 顺序表头删
// 顺序表头删
void SeqListPopFront(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;
}
2.3.7 顺序表查找
// 顺序表查找
int SeqListFind(SL* ps, SLDataType x){
assert(ps);
//找到返回下标,否则返回-1
for (int i = 0; i < ps->size; i++){
if (ps->arr[i] == x){
return i;
}
}
return -1;
}
找到返回下标,否则返回-1
- 返回下标是有好处的,你要查找的元素已经知道了,告诉位置可能更好;
- 如果我们在插入、删除、改的时候,知道元素但不知道下标,就可以调用查找函数寻到下标;
2.3.8 顺序表在pos位置插入x
// 顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x){
//扩容
assert(ps);
assert(pos <= ps->size);
CheckCapacity(ps);
size_t end = ps->size ;
while (end > pos){
ps->arr[end] = ps->arr[end - 1];
--end;
}
ps->arr[pos] = x;
++ps->size;
}
pos这里是
size_t
类型,size_t
类型其实就是:无符号整形;
这时候循环一定要注意,我们来分析一下以下程序
// 顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x){
//扩容
assert(ps);
assert(pos <= ps->size);
CheckCapacity(ps);
size_t end = ps->size - 1;
while (end >= pos){
ps->arr[end + 1] = ps->arr[end];
--end;
}
ps->arr[pos] = x;
++ps->size;
}
pos==0
,存在死循环问题, 因为size_t
是无符号整形,永远比0大所以死循环;
这时候如果改为int呢?如下列代码:
// 顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x){
//扩容
assert(ps);
assert(pos <= ps->size);
CheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos){
ps->arr[end + 1] = ps->arr[end];
--end;
}
ps->arr[pos] = x;
++ps->size;
}
如果认为没有问题那就大错特错了,因为
int
和unsigned
在比较的时候发生算数转换,这时候比较的是一个int算数转换后的一个临时变量,所以依旧是死循环。
2.3.9 顺序表删除pos位置的值
// 顺序表删除pos位置的值
void SeqListErase(SL* ps, size_t pos){
assert(ps);
assert(pos < ps->size);
for (int i = pos;i<=ps->size-1; i++){
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
2.3.10 顺序表销毁
// 顺序表销毁
void SeqListErase(SL* ps, size_t pos){
assert(ps);
assert(pos < ps->size);
size_t i = pos;
while(i<ps->size-1){
ps->arr[i] = ps->arr[i + 1];
++i;
}
--ps->size;
}
总结
数据结构一定要注意画图,这样可以直观的理解,细节方面很多,我们一定要注意,其实在函数实现里面有点重复,比如在下标最后一个删除就是尾删,我们可以直接调用删除,pos赋值为最后一个下标,仅供大家参考,最后,我把代码放在了gitee上,有需要可以自取:Gitee仓库