顺序表
数据结构概念
定义:数据结构是计算机存储、组织数据的⽅式
根据学过C语言的基础上,数组是最简单的数据结构
顺序表的底层就是数组
为什么呢?
例子如下:
int arr[100]={1,2,3,4,5};
//修改某一个数据:arr[pos]=x;
//插入一个元素:找到数组已有元素,再进行插入数据
//删除一个元素,找到数组已有元素,再进行删除数据
这个情况还是我们可见的情况下,如果说是好多数组要进行这样的操作,或者很大的未知大小的数组要进行如下的操作,是不是特别麻烦
顺序表说:我虽然底层是数组,但是我提供了很多现成的方法,开箱即用,我就变成了一个很厉害的数据结构。
数组和顺序表的对比
数组(苍蝇馆子) | 顺序表(米其林) |
---|---|
炒土豆丝 | 豪华金丝(炒土豆丝+摆盘) |
蔬菜汤 | 西湖牛肉羹(蔬菜+水+摆盘) |
由此可以形象地表现出顺序表:在数组的基础上增加了增删改查的方法
顺序表也是一种线性表
线性表是具有相同性质的数据结构的集合
初步理解:
蔬菜分为绿叶类、⽠类、菌菇类。
水果有:香蕉,苹果,菠萝。。。。
那线性表是具有相同性质的数据结构的集合具体表现在哪两个方面呢?
(1):物理结构:不一定连续
(2):逻辑结构:连续的
如图所示:
这个是物理结构上的理解
逻辑结构就是我们通常所认为的理解。
打个比方,我们去商店购物:如果是比较火的店就需要排队
右边那串像葡萄一样的,可以抽象理解为一个连续的结构,再来看数组,数组是连续存放的,我们可以通过指针的方式,去访问数组里面的元素,所以数组是逻辑结构上连续的
再来看顺序表:
顺序表的底层是数组,所以它的物理结构和逻辑结构都是连续的。
接下来做一个知识的铺垫引入:
int arr[10]={0};//定长数组
int* arr;//动态内存开辟,确定大小之后再去动态申请
顺序表的分类
静态顺序表
struct SeqList
{
int arr[100];//定长数组
int size;//顺序表当前有效的数据个数;
}
动态顺序表
struct SeqList
{
int* arr;
int size;//有效的数据个数
int capcity;//空间大小
}
那么这两种哪一种更好呢?
首先我们来谈静态顺序表:
(1)数组大小给小了,空间不够用 :如果说开发一个APP,你只提供了能储存一万个用户信息的空间,但是这款APP,却意外的火了,现在有很多很多用户想要去注册,但是没有空间可以注册,这样子给公司造成了不小的损失。
(2)数组大小给大了,空间造成了浪费:这样子也会给公司造成了一定的损失
接着我们再来谈动态顺序表:
动态增容:譬如说只开辟了100个空间,但是我们可以动态地扩容,相较于静态顺序表来说浪费的更少了
这里我们不难得出答案:动态顺序表更好
顺序表的实现
这就好比一本书的目录一样,头文件就相当于是目录的作用
Seq就是 sequence 的意思,也就是顺序的意思,List顾名思义就是列表
注意这里还需要创建一个源文件作为测试用叫test.c
注意来看:
#define _CRT_SECURE_NO_WARNINGS 1
//一般不推荐写静态顺序表
//以下是动态顺序表
typedef int SLDataType;
struct SeqList
{
SLDataType* arr;
int size;//有效的数据大小
int capacity;//空间大小
};
为什么我这里还要重新定义int ,而不是直接写int 呢?
打个比方,我们以后进了公司,有100000000行的代码涉及到了int ,这时候领导来了一句,把其中的6000行换成char,虽然我们知道编译器有一键置换的功能,但是这个功能可不能轻易使用,我们只要换其中的6000行不是所有的,所以我们要用重命名的方法来做,提高了我们的效率。
初始化和销毁
注意点:
来看一些代码的实现:
头文件内的:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
//一般不推荐写静态顺序表
//以下是动态顺序表
typedef int SLDataType;
struct SeqList
{
SLDataType* arr;
int size;//有效的数据大小
int capacity;//空间大小
}SL;
//顺序表的初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestory(SL* ps);
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
SeqList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLInit(SL *ps)
{
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
test.c:
#include "SeqList.h"
int main()
void SLTest01()
{
SL sl;
SLInit(&sl);
}
int main()
{
SLTest01();
return 0;
}
尾插
对尾插的概念图解:
ps指向的是一个数组,X=5是我要插入的数,size是我有效的数字个数,在这幅图中有4个,capacity是指空间大小
接下来是代码的实现过程了:
这是在SeqLsit.c的内容:
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//ps->arr[ps->size]=x;
//++ps->size;
ps->arr[ps->size++];//这行代码是将上面的两行代码变成了一行代码,
//对其size进行了后置++,提高程序运行效率
}
这是在test.c的内容:
void SLTest01()
{
SL sl;
SLInit(&sl);
//增删查改操作
//测试尾插
SLPushBack(&sl, 1);
SLDestory(&sl);
}
但是这样做在运行的时候会出现一些问题:
当你去监视程序的时候会发现:
size和capacity为0,也就意味着没有创建顺序表去可以去接受内容
这样就给我们提了一个醒:
插入数据前先看空间够不够
代码如下:
if(ps->capacity==ps->size)
如果不够,接下来我们就要去申请空间了
申请空间就不难想到之前学过的三个有关动态开辟空间的函数:malloc,calloc,realloc
经典的三选一:我们选择realloc,因为在这三个函数中只有realloc有增容的概念,其他两个都只是开辟空间,在这里又要注意malloc和calloc的区别,calloc开辟空间的时候会对其进行初始化
接下来出现了我们的第一个问题:
要申请多大的空间/一次增容多少?
如上图,我到底是开辟一个还是开辟n个空间呢。
直接给出结论:
增容通常是成倍数的增加,一般是2倍或3倍,但通常来说是2倍的去增容
实际上是数学推理出来的,用2或者3倍更合理
若要频繁地增容,程序运行效率大大降低
空间大小和数据个数的关系
从图上可以清楚地看出:空间大小和数据个数成正比的关系
以下是一些补充(感兴趣自己看,不感兴趣也无妨,不会影响学习的效率)
在数据结构中,特别是当涉及到数组这样的连续存储结构时,增容(或称为扩容)通常以倍数的形式增加,这样做的主要原因有以下几点:
- 减少数据移动次数:当数组需要扩容时,通常的做法是开辟一块新的、更大的内存空间,然后将原数组中的元素复制到新的内存空间中。如果每次扩容只增加少量空间,那么随着数组元素的增加,这种复制操作会频繁发生,导致时间复杂度较高。而如果以倍数形式增加容量,比如每次扩容为原来的两倍,那么虽然单次扩容时复制的元素数量较多,但复制操作的频率会大大降低,从而减少总的复制次数和时间消耗。
- 均摊时间复杂度为O(1):当以倍数形式扩容时,可以计算出均摊到每个元素上的复制次数是常数级别的。具体来说,假设每次扩容为原来的两倍,那么最终数组中的n个元素中,n个元素至少被复制了1次,n/2个元素至少被复制了2次,n/4个元素至少被复制了3次,以此类推。将这些复制次数相加,可以得到一个等比数列的和,其和是O(n)的。但由于这个复制次数是分摊到n个元素上的,所以均摊到每个元素上的复制次数是O(1)的。这意味着,从长期来看,每次插入或删除元素的时间复杂度是常数级别的。
- 空间利用率:虽然以倍数形式扩容可能会导致一定的空间浪费(因为当数组未满时就已经进行了扩容),但这种浪费是可控的,并且可以通过选择合适的扩容倍数来平衡空间利用率和性能。通常,选择2倍作为扩容倍数是一个较好的折衷方案,因为它既能保证较好的性能,又能保持相对较高的空间利用率。
综上所述,数据结构中增容以倍数的形式增加主要是为了减少数据移动次数、降低均摊时间复杂度和保持相对较高的空间利用率。
代码如下:
ps->arr = realloc(ps->arr, ps->capacity * 2*sizeof(SLDataType));
接下来我们来看我们已经写的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLInit(SL *ps)
{
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//插入前先看空间够不够
if (ps->capacity == ps->size)
{
//申请空间
ps->arr = realloc(ps->arr, ps->capacity * 2*sizeof(SLDataType));
}
//ps->arr[ps->size]=x;
//++ps->size;
ps->arr[ps->size++];//这行代码是将上面的两行代码变成了一行代码,
//对其size进行了后置++,提高程序运行效率
}
有没有发现什么问题呢?
ps->size = ps->capcacity = 0;
我把空间初始化为0了,我怎么插入数据呢?
看代码如下:
//判断空间大小是否为0
// 三目表达式
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
但是不要忘了动态申请内存可能会失败,realloc申请空间失败会怎样呢?
里面会变成NULL.
例子如下:
还是来看这幅图:
如果这幅图我要插入的数据大于它所能存储的空间范围呢,那样子不就全都没了,竹篮打水一场空
来看代码的实现:
SLDataType*tmp = realloc(ps->arr, ps->newCapacity * 2*sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
//空间申请成功
ps->arr = tmp;
ps->capacity = newCapacity;
我先去判断是否为空,这里还要注意我用了*tmp去接收内容。tmp是临时变量,以防数据丢失
再来看一个坑:
在test.c文件中:
#include "SeqList.h"
int main()
void SLTest01()
{
SL sl;
SLInit(&sl);
//增删查改操作
//测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(NULL, 5);//注意看这个代码
SLDestory(&sl);
}
int main()
{
SLTest01();
return 0;
}
如果我贸然地将前面的参数变为NULL,此时我们的代码还不具有健壮性,程序会出现访问冲突的问题,此时该怎么做呢?
if (ps == NULL)
{
return;
}
在插入空间前看是否为空指针。
Tip:在程序运行的时候退出码为0,证明是正常退出的,如果不为0,那就是没有正常退出
这是一种比较温柔的解决方法
下面是比较粗暴的解决方式:
#include <assert.h>
assert(ps);//等价于assert(ps!=NULL);
头插
概念图:
把元素插到arr[0]的位置
大体思路如下:
目标是打算把最后一位有效数字往后挪一位
代码如下:
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//先让顺序表已有的数据往后挪动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] =ps-> arr[i - 1];//arr[1]=arr[0],最后一次情况是这样,由此来推断i的限制条件
}
ps->arr = x;
}
特别需要注意这里i的限制条件,我们不确定的时候可以先往下写,由写出的代码推出for循环的限制条件
接下来的一步我们要去打印顺序表:
void SLPrint(SL s)
{
for (int i = 0; i < s.size; i++)
{
printf("%d ",s.arr[i]);
}
printf("\n");
}
为什么这里不要取地址呢?
因为我们要取的是它的值。
但是我们去打印的时候会出现一些不太符合我们预期的结果,在头插上,这是因为有效个数没有增加:
ps->size++;
综合来看代码如下:
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//先让顺序表已有的数据往后挪动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] =ps-> arr[i - 1];//arr[1]=arr[0],最后一次情况是这样,由此来推断i的限制条件
ps->size++;//特别要注意了
}
ps->arr = x;
}
尾删
概念图
情况一:
情况二:
我们需要知道一个道理:只要写的表达式能够很好地满足增删改查数据就是好的表达式
头删
代码如下:
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
for (int i = 0; i < size - 2; i++)
{
ps->arr[i] = ps->arr[i + 1];//arr[size-2]=arr[size-1];
}
ps->size--;
}
完整的实现过程:
SeqList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//一般不推荐写静态顺序表
//以下是动态顺序表
typedef int SLDataType;
struct SeqList
{
SLDataType* arr;
int size;//有效的数据大小
int capacity;//空间大小
}SL;
//顺序表的初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestory(SL* ps);
//顺序表的打印
void SLPrint(SL s);
//头部插入删除 / 尾部插入删除
//插入
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
//删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
//专门封装一个函数用来检查空间是否够不够
void SLCheckCapacity(SL*ps)
{
//插入前先看空间够不够
if (ps->capacity == ps->size)
{
//温柔的解决方法
if (ps == NULL)
{
return;
}
//判断空间大小是否为0
// 三目表达式
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//申请空间
SLDataType* tmp = realloc(ps->arr, ps->newCapacity * 2 * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
//空间申请成功
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
void SLInit(SL *ps)
{
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
void SLDestory(SL* ps)
{
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capcacity = 0;
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//ps->arr[ps->size]=x;
//++ps->size;
ps->arr[ps->size++]=x;//这行代码是将上面的两行代码变成了一行代码,
//对其size进行了后置++,提高程序运行效率
}
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//先让顺序表已有的数据往后挪动一位
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] =ps-> arr[i - 1];//arr[1]=arr[0],最后一次情况是这样,由此来推断i的限制条件
ps->size++;//特别要注意了
}
ps->arr = x;
}
void SLPrint(SL s)
{
for (int i = 0; i < s.size; i++)
{
printf("%d ",s.arr[i]);
}
printf("\n");
}
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
//顺序表不为空
--ps->size;
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
for (int i = 0; i < size - 2; i++)
{
ps->arr[i] = ps->arr[i + 1];//arr[size-2]=arr[size-1];
}
ps->size--;
}
test.c
#include "SeqList.h"
int main()
void SLTest01()
{
SL sl;
SLInit(&sl);
//增删查改操作
//测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
//SLPushBack(NULL, 5);
SLPrint(sl);
SLPushFront(&sl, 6);
SLPrint(sl);
SLDestory(&sl);
}
int main()
{
SLTest01();
return 0;
}
思维导图
送大家一句话:如果你遇到困难的时候,证明你在走上坡路。
希望大家喜欢的博客,如有不足请大家指正!