本人能力有限,难免有叙述错误或者不详细之处!希望读者在阅读时可以反馈一下错误以及不够好的地方!感激不尽!
目录
什么是数据结构?它和锅子有啥关系?
数据结构,其实名字上还是蛮抽象的,但是它非常的重要,不论是在平常的使用或者是各类面试题里它的出现率都是高的吓人。
那么它到底是个什么玩意儿呢?
假如说现在我们的面前有一只铁锅(不要问我为什么拿铁锅举例子)
单独的一只锅子其实干不了什么是吧?它最多的功能也只是装水,装很多的水,或者是苏打水(?),当然当个一级头也是可以的
显然,就单独这只锅子的能力也就那样了,但如果我们加上煤气灶的话它就可以炒饭,煮汤或着烧菜,而不是单独的装东西。再加点东西,比如锅盖或者锅铲,那它也可以焖煮鲈鱼什么的,你也可以化身大厨爆炒生鲜等等等等。
再往上,我们为其添加各类器件和电控系统,看,一只原本只能装水的锅子就变成了电饭煲了。
那么,这一切跟顺序表有啥关系?你隔着忽悠人?
别急,你看,这锅其实本质像不像一个数组?
只能简单的存储数据,在C语言里还不能支持简单的动态指定容量(柔性数组除外),取出数据非常麻烦,有时候还需要遍历整个数组。
我们回到科学的解释上:数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的。
而顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改
顺序表的本质,就是加装在锅子身上的各类电子器件了,说到这你也应该明白了,顺序表的其实是一个更加“趁手”的电饭锅,它的本质依旧是一个铁锅,也就是一个数组。
那么,我们为了更好的把数组升个级,我们使用函数和结构体来帮助我们操作数据,其实也就是实现一个顺序表的基本操作。
顺序表的定义:
为了更好的控制一个数组,我们用结构体去包装它,让结构体里不仅这个数组,也存放关于这个数组的信息,方便我们控制和获得数组信息,有如下两种包装方式。
静态顺序表:
这样的顺序表还是非常蛋疼的,它的空间不是动态的,不够空间了还要自己去改N的值,所以咱们不用这个。
#define N 10
typedef int DataType;
typedef struct SeqList
{
DataType arr[N]; //定长数组
size_t size; //有效数据个数
}SL;
动态顺序表:
换用一个定义指定类型的指针来存放数据,这样就方便我们在堆上动态开辟空间,DataType也方便修改存储类型,size的类型也可以使用无符号整型,
size用来存储当前数组的长度,且size永远指向当前最后一个数据之后的位置
capacity则用来存储当前数组的容量,也控制着在堆上动态开辟的空间大小
比方说一个容量为7的数组,里面存放了4个数据
typedef int Datatype;
typedef struct Seqlist
{
Datatype* a;
int size; //当前数组长度
int capacity; //容量
}SL;
顺序表的接口
接口的意义就很像电饭锅的各种功能,我们只需要提供所需的材料,按下按键,电饭锅就会启动对应功能。
接口也是一样,我们只需要包装好接口,提供所需变量,增删查改就非常方便了,只需要调对应的函数就好了,不需要遍历整个数组什么的。
创建:
为了更好的管理我们的项目,我们分类创建代码文件,分别为函数的声明与实现,以及主要的接口使用函数。
我们使用顺序表也是为了更方便的对数组进行增删查改,那么我们就先将我们需要实现的函数结构先声明一下:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int Datatype;
typedef struct Seqlist
{
Datatype* a;
int size;
int capacity;
}SL;
void SeqInit(SL* slt);
//初始化
void SLPrint(const SL* slt);
//打印
void CheckSeq(SL* slt);
//判满
void SLDestory(SL* slt);
//销毁
void SeqListPushBack(SL* slt, Datatype x);
// 顺序表尾插
void SeqListPopBack(SL* slt);
顺序表尾删
void SeqListPushFront(SL* slt, Datatype x);
顺序表头插
void SeqListPopFront(SL* slt);
顺序表头删
int SeqListFind(SL* slt, Datatype x);
顺序表查找
void SeqListInsert(SL* slt, size_t pos, Datatype x);
顺序表在pos位置插入x
void SeqListErase(SL*slt, size_t pos);
顺序表删除pos位置的值
void SLModify(SL* slt, size_t pos, Datatype x);
//更改pos位置的值
接下来就是这些接口功能的实现了。
顺序表的初始化
初始化无非就是给个初始值,在这里为了更好的装数据,我们就都给上空的,像一个空碗一样。
指针置空,容量以及大小置零。
assert防止传递空指针
//顺序表的初始化
void SeqInit(SL* slt)
{
assert(slt);//防止转递一个空指针
slt->a = NULL;
slt->size = slt->capacity = 0;
}
顺序表的打印
为了更好的观察顺序表的增删查改,写个打印接口还是很有必要的。
前文提过size的位置永远指向最后一个数据之后的空位,我们只需要控制循环小于它就好。
void SLPrint(const SL* slt)
{
assert(slt);
for (int i = 0; i < slt->size; i++)
{
printf("%d ", slt->a[i]);
}
printf("\n");
}
顺序表的判满
我们希望动态的顺序表可以在我们需要扩容的时候自动扩容,我们就需要检查当前数组是否存满了数据,控制以及观察数组容量的变量我们已经放进结构体里了,当size 等于 capacity 的时候就意味着顺序表满了,我们这个时候就需要增大capacity的值,并且在堆上开辟对应大小的空间。
我们初始的空间值给4个对应类型大小的空间
这里我们用一个三目操作符 ? : 来简化代码,创建一个新的newcapacity,如果满了就乘以2,然后用realloc开辟对应大小的空间。
void CheckSeq(SL* slt)
{
//扩容
if (slt->size == slt->capacity)
{
int newcapacity = slt->capacity == 0 ? 4 : slt->capacity * 2;
Datatype* tmp = (Datatype*)realloc(slt->a, newcapacity * sizeof(Datatype));
//返回值检查,是否为空
if (realloc == NULL)
{
perror("realloc fail!");
exit(-1);
}
//更新扩容后的空间以及容量
slt->a = tmp;
slt->capacity = newcapacity;
}
}
顺序表尾插
接下来就是插入数据了,我们先写简单的,尾部插入,非常简单,只需要在size的位置插入数据就可以了
把刚才写的判满函数写进去,让空间动态开辟
//尾插
void SeqListPushBack(SL* slt, Datatype x)
{
assert(slt);
CheckSeq(slt);
slt->a[slt->size] = x;
slt->size++;
}
至此,一个有着增加功能的顺序表已经可以使用了,我们试试效果。
NICE!但是只能增不能删也太逊了,接下来就是尾删。
顺序表尾删
顺序表的尾删只需要--size就好。
咱们乍一想可能会觉得有点掩耳盗铃,你这只是size--了,根本妹删啊?你为什么不置零呢?
原理则很简单size--了之后,下一次尾插的数据会直接覆盖掉当前尾部的数据,如下图:
4就会被覆盖掉,这样子不仅简单,也规避了如果置零但数据类型不一致的问题。
void SeqListPopBack(SL* slt)
{
assert(slt);
assert(slt->size > 0); //顺序表不能为空
slt->size--;
}
顺序表头插
头插其实很好理解,如下(随手画的)
头插稍微麻烦一点的问题就在于头部之后的数据需要被挪动,不过实现也不困难。
//头插
void SeqListPushFront(SL* slt, Datatype x)
{
assert(slt);
assert(slt->size > 0);
CheckSeq(slt);
for (int i = slt->size; i >= 0 ; i--)
{
slt->a[i + 1] = slt->a[i];
}
slt->a[0] = x;
slt->size++;
}
依次挪动头部之前的数据,然后把X给到头部位置即可,别忘了更新size的值。
顺序表头删
和头插差不多,我们不能不管之后的数据,也要进行挪动。
挪动原理也是差不多的,一个循环就能解决,将头部后面的数据向前覆盖一位就行。别忘记更新size的值。
//头删
void SeqListPopFront(SL* slt)
{
assert(slt);
assert(slt->size > 0);
for (int i = 0; i < slt -> size - 1; i++)
{
slt->a[i] = slt->a[i + 1];
}
slt->size--;
}
顺序表查找
这个就很简单了,简单的遍历整个数组,找到与X相等的值,返回其位置,找不到就返回-1.
int SeqListFind(SL* slt, Datatype x)
{
assert(slt);
assert(slt->size > 0);
for (int i = 0; i < slt->size; i++)
{
if (slt->a[i] == x)
{
return i + 1;
}
}
printf("can't found!\n");
return -1;
}
顺序表插入
插入的逻辑也和头插头删大同小异,只不过循环的位置变化了而已,本质上并没有特别大的区别,依旧是向后覆盖挪动插入位置之后的数据,然后覆盖插入位置的数据
//插入
void SeqListInsert(SL* slt, size_t pos, Datatype x)
{
assert(slt);
assert(pos <= slt->size);
CheckSeq(slt);
int end = slt->size;
while (end > pos)
{
slt->a[end] = slt->a[end-1];
end--;
}
slt->a[pos] = x;
slt->size++;
}
其实指定位置插入我们写好了以后可以拿去直接替换头插尾插,毕竟指定位置就好。
void SLPushFront(SL* slt, DataType x)
{
SLInsert(slt, 0, x);
}
void SLPushBack(SL* slt, DataType x)
}
SLInsert(slt, slt->size, x);
}
顺序表指定位置删除:
//定向位置删除
void SeqListErase(SL* slt, size_t pos)
{
assert(slt);
int cur = pos;
while (cur < slt->size)
{
slt->a[cur] = slt->a[cur + 1];
cur++;
}
slt->size--;
}
同理,也可以替换头删尾删。
顺序表的替换
这个就真的没啥好说的了,直接换就完事儿了。
void SLModify(SL*slt, size_t pos, Datatype x)
{
assert(slt);
assert(pos < slt->size);
slt->a[pos] = x;
}
顺序表的销毁
释放开辟的空间,对应指针置空,size和capacity置零就好。
//销毁
void SLDestory(SL* slt)
{
assert(slt);
free(slt->a);
slt->a = NULL;
slt->capacity =slt->size = 0;
}
至此,整个顺序表的基本功能就实现完毕了
感谢阅读!希望对你有点帮助!