👀先看这里👈
😀作者:江不平
📖博客:江不平
📕学如逆水行舟,不进则退
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
❀本人水平有限,如果发现有错误的地方希望可以告诉我,共同进步👍
🏐1.线性表是什么?
我们为什么要学习线性表呢?
数据结构和算法的学习可以对你思考问题的方式还是对你编程的思维都会有很大的好处。
而这第一关就是线性表了。
- 线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
- 线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点)。
- 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
线性表的优点
线性表的逻辑结构简单,便于实现和操作。因此,线性表这种数据结构在实际应用中是广泛采用的一种数据结构。
- 常见的线性表:顺序表、链表、栈、队列、字符串…
接下来,我们就先看一下线性表中的顺序表。
🏐2.顺序表又是什么??
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表分为静态顺序表和动态顺序表,在我们实际应用中,我们常用动态顺序表,因为我们不好确定我们具体需要多大的空间进行数据存储,接下来我们就来实现一下动态顺序表。
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* arr; // 指向动态开辟的数组
int size ; // 有效数据个数
int capicity ; // 容量空间的大小
}SeqList;//简化类型名
上面就是一个动态顺序表,接下来我们实现一下它的接口,做到对数据的增删查改。
🏐3.顺序表的接口实现
🏀3.1增
增就是增加,也就是插入数据,不难想到,从位置上分,我们可以从表头,表尾还有表的中间位置插入。我们一个一个来看。
在写插入之前,我们要考虑扩容问题,有插入必要考虑容量,先写一下扩容
void SLCheckCapacity(SL* ps)
{
//检查容量空间,满了扩容
if (ps->capacity == ps->size)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//
if (tmp == NULL)
{
return;
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
记住要放在需要调用它的前面,不然后面函数无法调用
- 表头插入数据,简称头插
void SLPushFront(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);//在插入的时候首先要想到是否需要扩容
int end = ps->size - 1;
//往后挪动数据
while(end>=0)
{
ps->arr[end + 1] = ps->arr[end];
--end;
}
ps->arr[0] = x;
ps->size++;
}
- 表尾插入数据,简称尾插
void SLPushBack(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);//插入就要考虑扩容
ps->arr[ps->size] = x;
ps->size++;
}
- 任意位置插入数据(~ ̄▽ ̄)~
void SeqListInsert(SL* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//插入的位置要在[0,size]之间
SLCheckCapacity(ps);//插入考虑扩容
int end = ps->size;
//往后挪动数据,让一个位置供我插入
while (end>=pos)
{
ps->arr[end] = ps->arr[end - 1];
end--;
}
ps->arr[pos] = x;
ps->size++;
}
当然我们可以把将任意位置插入的这段代码复用在头插和尾插中😎,如下
void SLPushFront(SL* ps, SLDataType x)
{
SeqListInsert(ps, 0, x);
}
void SLPushBack(SL* ps, SLDataType x)
{
SeqListInsert(ps, ps->size, x);
}
可以看到对于插入数据来说,我们一般都是定义一个尾,然后往后挪动数据再进行插入。
🏀3.2删
同样的,删除数据也有三种,我们来实现一下
删除数据就不用考虑扩容啦~
- 头部删除数据,简称头删
void SLPopFront(SL* ps)
{
assert(ps != NULL);
assert(ps->size > 0);
int begin = 1;
//往前覆盖数据
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
- 尾部删除数据,简称尾删
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0);//注意顺序表为空就不能进行删除了
ps->size--;//尾删比较简单,直接size减一即可
}
- 任意位置删除数据(~ ̄▽ ̄)~
void SeqListErase(SL* ps, size_t pos)
{
assert(ps && pos < ps->size);
size_t start = pos + 1;
//往前覆盖数据
while (start < ps->size)
{
ps->a[start - 1] = ps->a[start];
++start;
}
ps->size--;
}
当然我们可以把将任意位置插入的这段代码复用在头插和尾插中😎,如下
void SLPopFront(SL* ps)
{
SeqListErase(ps, 0);
}
void SLPopBack(SL* ps)
{
SeqListErase(ps, ps->size-1);
}
可以看到对于删除数据来说,我们一般都是定义一个头(pos的下一个位置),然后往前挪动数据做到覆盖,完成数据的删除。
🏀3.3查
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
//遍历数组
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
return i;//查找成功,返回下标
}
return -1;//查找失败
}
🏀3.4改
void SLModify(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);//注意边界,不包含size
ps->a[pos] = x;
}
给出下标直接进行修改即可
前面我们讲了查找(Find)
如果想要把某些值修改,就先进行查找(Find)再修改(Modify)
int y = 0, z = 0;
printf("请输入你想修改的值:");
scanf("%d%d", &y,&z);
int pos = SLFind(&s1, y);
if (pos != -1)
{
SLModify(&s1,pos, z);
}
else
{
printf("没找到:%d\n", y);
}
如果想要把某些值删除,就先进行查找(Find)再删除(Erase)
int x = 0;
printf("请输入你想删除的值:");
scanf("%d", &x);
int pos = SLFind(&s1, x);
if (pos != -1)
{
SeqListErase(&s1,pos);
}
else
{
printf("没找到:%d\n",x);
}
可以看到各种接口进行组合可以做到很多功能,我们想要看到效果还需要Print,下面来看一下Print的实现
void SLPrint(SL* ps)
{
assert(ps != NULL);
//assert(ps);
//遍历数组,打印
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
🏀3.5空间释放
顺序表不用了我们将其空间进行释放
void SLDestory(SL* ps)
{
assert(ps != NULL);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
}
🏐4.一些小点
- 我们说“线性”和“非线性”,只在逻辑层次上讨论,而不考虑存储层次,所以双向链表和循环链表依旧是线性表。
- 为什么我们总要挪动数据,因为顺序表在存储时物理地址上是连续的,需要挪动数据才有空间进行插入数据,同样的,删除数据后我们也要保持物理地址连续。
- 我们可以看到尾删和尾插比较简单,时间复杂度度为O(1),而中间/头部的插入删除,每次都要挪动数据,时间复杂度为O(N),比较麻烦
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。因为一次我们不可能就扩容一点,不然就可能要连续扩容(看第四点),会有消耗。
6.注意assert的使用,不进行检查的话会出现数组越界的情况,编译器可能不会报错,但这并不代表正确
看到这里觉得还不错的老铁点赞关注一下吧😀