Hello~,欢迎大家来到我的博客进行学习!
目录
- 1.线性表
- 2.顺序表
- 2.1 概念与结构
- 2.2 分类
- 2.2.1 静态顺序表
- 2.2.2 动态顺序表
- 2.3 动态顺序表的实现
- 初始化
- 尾插
- 头插
- 尾删
- 头删
- 查找
- 指定位置之前插入数据
- 删除指定位置的数据
- 销毁
1.线性表
首先我们需要知道的是,顺序表和链表都属于线性表。线性表是具有相同特性的一类数据结构的集合。
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
这里的相同特性我从逻辑结构和物理结构两个方面进行分析。
- 逻辑结构(一定是线性的):人为想象出来的。
- 物理结构(不一定是线性的):比如数组在存储空间是连续的,和物理结构上的线性是一样的。
2.顺序表
2.1 概念与结构
概念:顺序表是用⼀段物理地址连续的存储单元(物理结构是线性的)依次存储数据元素的线性结构,⼀般情况下采用数组存储。顺序表的底层结构是数组。
顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口。
2.2 分类
2.2.1 静态顺序表
如果顺序表底层是固定的空间大小,我们把它叫做静态顺序表。
实现静态顺序表,需要三个文件:头文件(.h)、源文件(.c)、测试文件(test.c)。
- 头文件(.h):用来声明一些结构和我们的方法
- 源文件(.c):方法的具体实现
- 测试文件(test.c):用于测试
取名为SeqList(Sequence List),意为连续的。
我们可以定义一下静态顺序表的大小N,还需要一个size来定义一下有效数据的个数。同时这里的数据类型可能是各种各样的,使用typedef来解决。当要使用这个结构体的时候,我每次都要加上关键词,感觉麻烦,使用typedef来解决。
SeqList.h
#define N 1000
typedef char SLDataType;
//静态顺序表
typedef struct SeqList {
SLDataType arr[N];
int size;//有效数据的个数
}SL;
2.2.2 动态顺序表
如果空间是不固定的,我想要多少,可以去增加,就是动态顺序表。
#define N 1000
typedef char SLDataType;
//动态顺序表
typedef struct SeqList {
SLDataType* arr;
int size;//有效数据的个数
int capacity;//容量大小
}SL;
2.3 动态顺序表的实现
初始化
现在我先进行初始化,参数传一个s,初始化的具体方法在SeqList.c文件中实现。
SeqList.c
#include"SeqList.h"
//初始化
void SLInit(SL s)
{
s.arr = NULL;
s.size = s.capacity = 0;
}
在test.c中进行检验。
#include"SeqList.h"
void SLTest()
{
SL sl;
SLInit(sl);
}
int main()
{
SLTest();
return 0;
}
此时会报错:
在test.c的sl是实参,在SeqList里面的s为形参。
我们需要将sl的地址传过去,而不是传值。传值时,改变形参并不能改变实参。在SeqList.c这里我就应该用指针来接收。
改完以后:
SeqList.c
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
test.c
#include"SeqList.h"
void SLTest()
{
SL sl;
SLInit(&sl);
}
int main()
{
SLTest();
return 0;
}
SeqList.h
#include<stdio.h>
#include<stdlib.h>
#define N 1000
typedef int SLDataType;
静态顺序表
//typedef struct SeqList {
// SLDataType arr[N];
// int size;//有效数据的个数
//}SL;
//动态顺序表
typedef struct SeqList {
SLDataType* arr;
int size;//有效数据的个数
int capacity;//容量大小
}SL;
//初始化
void SLInit(SL* ps);
调试看看:
跳出以后,sl里面的部分也初始化了:
尾插
现在来添加插入数据的功能,顺序表的底层是数组,我可能需要需要在原来数据的末尾、中间、开头插入数据。
首先实现尾插(在顺序表最后一个可插的位置,插入数据)的功能。
如上图,我有5块空间,里面有3个有效数据。尾插就是在3的后面插入数据。
用SLPushBack这个函数来实现,里面的参数为ps和我们要插入的数据。现在我们需要在顺序表里插入数据,这里有三个成员:arr、size、capacity。size指向的位置刚好就是最后一个有效数据的下一个位置。
假设空间足够,现在往里面插入一个数据X = 99,直接往3后面放,这里并不需要遍历数据组,往size这个位置放就行,插入完成之后,size++。
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
ps->arr[ps->size] = x;
++ps->size;
}
假设空间不够(此时size和capacity在一个位置),按以上方式插入数据,会越界。就需要对原数组申请空间,,然后再在size位置插入数据,插入后size++。
因而我们需要分情况来写。
在增容时,我们使用realloc(可以在原数组的基础上进行增加容量)来进行这一操作。如果插一个数据,增加一个容量,没有空间的浪费,但是增容频繁,程序效率低下。如果一次多申请一些,可能会造成空间的浪费。通常,增容是按倍数增加,如2倍、3倍…这里我选择两倍。
按照以上思路,其实是有漏洞的。
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//空间不够,申请空间
if (ps->size == ps->capacity)
{
//空间不够,2倍增容
SLDataType* tmp = (SLDataType*)realloc(ps->arr, ps->capacity * 2);
}
ps->arr[ps->size++] = x;
}
在realloc这里,第二个参数是size_t size单位是字节,ps->capacity * 2 这里改为 ps->capacity * 2 *sizeof(SLDataType)。并看看增容成功没有。但是之前我们初始化那里把capacity初始化为0,现在需要扩容的话,需要先给一个初始值。我这里先给一个SLDestroy函数,在程序结束时释放动态分配的内存,不然程序结束时会出现内存泄漏。
SeqList.c
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
if (ps == NULL)
{
return;
}
//空间不够,申请空间
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//空间不够,2倍增容
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
ps->arr[ps->size++] = x;
}
// 释放顺序表
void SLDestroy(SL* ps)
{
if (ps->arr != NULL)
{
free(ps->arr); // Free the allocated memory
ps->arr = NULL;
}
}
头插
现在来实现头插。这里也要看空间大小够不够,则可以分装一个方法来判断空间大小够不够。前面的步骤和尾插差不多,只是实现头插时有部分区别。需要把数据整体往后移动,然后再进行插入。在移动的时候,先把后面的数据往后移动,最后把插入的数据放在下标为0的位置。同样在增加完数据之后,size++。
SeqList.c
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLcheckCapacity(SL* ps)
{
//空间不够,申请空间
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//空间不够,2倍增容
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
if (ps == NULL)
{
return;
}
SLcheckCapacity(ps);
ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
if (ps == NULL)
{
return;
}
SLcheckCapacity(ps);
//直接头插
//数据整体向后移动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
++ps->size;
}
尾删
ps不能传空,删除数据之后,size要 - -。顺序表为空,也不行。这里把最后一个数据弄掉,不是把最后一个数据弄为0,而是可以修改。
//尾删
void SLPopBack(SL* ps)
{
assert(ps&&ps->size>0);
--ps->size;
}
头删
这里的前提和尾删一样。ps不能传空,删除数据之后,size要 - -。顺序表为空,也不行。我们需要移动数据,下标为零以后的数据,整体向前移动一位。
//头删
void SLPopFront(SL* ps)
{
assert(ps && ps->size > 0);
--ps->size;
for (int i = 0; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
}
查找
这里比较简单,遍历顺序表,如果查找到返回下标;如果找不到返回无效下标。
//查找
int SLFind(SL* ps, SLDataType x)
{
for(int i = 0; i < ps->size; i++)
{
if (x == ps->arr[i])
{
//找到了,返回下标
return i;
}
}
//找不到
return -1;
}
指定位置之前插入数据
这里新增一个参数pos(指定位置)。pos需要有效,pos >= 0 && pos <= ps->size。与之前一样需要判断空间是否足够,插入好之后size++。
假设需要将99这个数据插入3这个位置之前,需要将pos以及之后的数据向后移动一位。
//指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
//前=:头插
//后=:尾插
assert(pos >= 0 && pos <= ps->size);
SLcheckCapacity(ps);
for (int i = ps->size - 1; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
++ps->size;
}
删除指定位置的数据
同样的size需要减减,指定位置pos必须pos >= 0 && pos <= ps->size。要删除pos位置的数据就需要把pos之后的数据整体向前移。
//删除指定位置的数据
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;
}
销毁
动态申请的空间在不用的时候需要销毁。
//销毁
void SLDestroy(SL* ps)
{
if (ps->arr)
free(ps->arr);
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
此时,我们对顺序表已经比较理解了。
好了,我们的顺序表的知识就讲到这里。如果文章内容有误,请大佬在评论区斧正!谢谢大家!