前言
制作不易!三连支持一下呗!!!
从今天起我们将会进入数据结构的学习!
我们先来了解
什么是数据结构
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。
一般数据结构有很多种,一般来说,按照数据的逻辑结构对其进行简单的分类,包括线性结构和非线性结构两类。
线性表在逻辑结构上是连续的,但在物理结构上不一定是连续的!
今天我们就来学习一下线性表之一的顺序表!
一、什么是顺序表
顺序表是最简单的一种数据结构,它底层采用的是我们之前学过的数组,它在逻辑结构(数据之间的关系)上是连续的,在物理结构上也是连续的(因为数组中的数据在内存中是连续存放的)。
但是顺序表又不能等同于数组,它其实是在数组的基础上进行封装,通过增,删,查,改等接口来对数据进行操作。
顺序表根据大小是否是固定的又分为
1.静态顺序表
2.动态顺序表
二、顺序表的实现
1.静态顺序表
底层使用的是固定大小的数组
比如:
#include<stdio.h>
typedef int Seqtype;//如果我们后续要更改存储的数据的类型,这样重定义可以方便我们一键替换
#define Seqtype_MAX 100//方便我们后续修改顺序表的大小
typedef struct SeqList
{
Seqtype arr[100];//存放100个整型类型的数据
int size;//记录有效数据(也就是已经保存的数据)的个数
}SL;//使结构体类型更加简洁
这里着重解释一下为什么只创建一个数组来存放数据还不够,我们还需要一个size来记录有效数据的个数:
这是因为当我们在后续实现一些增,删,查,改等接口时难免会遍历已保存的数据,这样size就可以方便我们知道每次需要遍历多少个数据。
由于静态顺序表有大小是固定的这样的弊端,因此我们更加喜欢使用动态顺序表!
二.动态顺序表
想要实现动态顺序表就要用到我们之前介绍过的动态内存管理的方式(malloc,calloc,realloc),通过realloc函数的特点,在我们空间大小不够的情况下扩容。
实现方式如下:
#include<stdio.h>
typedef int Seqtype;//如果我们后续要更改存储的数据的类型,这样重定义可以方便我们一键替换
typedef struct SeqList
{
Seqtype* arr;
int capacity;//记录当前顺序表的容量
int size;//记录有效数据(也就是已经保存的数据)的个数
}SL;//使结构体类型更加简洁
我们通过指针的方式来遍历整个数组,也通过动态开辟的方式来申请空间。
1. 顺序表的初始化与销毁
创建好一个顺序表,,类比创建变量,我们首先要做的第一步就是初始化这个顺序表。我们可以通过一个接口函数来完成这个功能:
<1>.顺序表的初始化
#include<stdio.h>
#include<assert.h>
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
这里我们只强调为什么我们要传址调用,而不能传值调用:
我们在这里是想要初始化这个顺序表,也就是要对顺序表中的内容进行修改,如果我们采用传值调用的方式,形参只是实参的临时拷贝,对形参的修改是不会影响实参的,也就是说实参是并没有被初始化的!因此,我们这里传一个地址过去,通过指针的方式来对顺序表进行初始化(修改)。
<2>. 顺序表的销毁
当我们会初始化顺序表后,销毁顺序表其实也大差不差:
#include<stdio.h>
#include<assert.h>
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
同样的道理我们这里还是要传址调用!
2.扩容
动态顺序表最大的特点就是当空间不够时,可以自动开辟更多的空间以存放数据。
扩容的原理也有很多,比如:
1.每次扩容一个元素大小的空间
原则就是每当我们空间不足时,使用realloc函数来增加一个元素的空间。
这种方式有它的优点:不会一次扩容过度,造成空间的浪费。
弊端:每次只扩大一个元素的空间,这样会造成需要频繁的扩容,那样就会频繁的调用这个接口函数,造成程序的运行效率低下!
2.每次扩容固定大小的空间
优点:可以做到不会频繁扩容
弊端:可能会导致一次性扩容过度,造成空间的浪费
可以看出前两种方式是两个极端!
3.成倍数的扩容(强烈推荐)
每次扩容为原来的容量的固定倍数的大小的空间,我们通常会使用1.5倍或2倍(倍数不宜过大,会导致空间的浪费)
这种方式平衡了前两种方式的弊端,既不会过度扩容,也不会特别造成空间的浪费,是一种中性的方法。
说完扩容的原则,我们接下来实现扩容的功能:
在后面插入,删除数据的接口中可能会存在空间不足需要扩容的情况,所以我们单独将扩容拿出来,实现一个扩容的接口。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size)//判断是否需要扩容
{
int newcapacity = ps->capacity == 0 ? 2 : 2*ps->capacity;
Seqtype* tmp = (Seqtype*)realloc(ps->arr, newcapacity * sizeof(Seqtype));
//刚开始ps->arr是NULL,realloc会直接开辟新空间,作用相当于malloc
//注意单位是字节,要*类型的大小
if (tmp == NULL)
{
perror("realloc");
exit(1);//直接终止程序,比return 1更加暴力
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
这里我们再实现一个打印顺序表的接口,方便我们进行调试观察代码是否有误
void SLPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
}
非常简单!
3.头插和尾插
<1>.尾插
尾插顾名思义就是在之前的数据的末尾插入新的数据。
当我们插入数据时就会遇到两种情况:
1.空间足够,直接插入
根据上图可以看出ps->size就是我们要插入的位置的下标
2.空间不足时
这种情况我们需要先扩容,再插入
代码实现如下:
void SLPushBack(SL* ps, Seqtype x)
{
assert(ps);
SLCheckCapacity(ps);//判断是否要扩容
ps->arr[ps->size++] = x;//注意:不用忘了将ps->size++。
}
<2>.头插
将每次要插入的数据放到数组下标为0的起始位置,同样分为两种情况:
1.空间足够
假如我们要插入100
最后应该是将之前的元素往后挪动,再加100插入到头部,最后ps->size++
2.空间不足
对于空间不足的情况,同尾插一样,我们需要先扩容再插入。
下面我们实现代码:
void SLPushFront(SL* ps, Seqtype x)
{
assert(ps);
SLCheckCapacity(ps);//判断是否要扩容
int i = 0;
for (i = ps->size; i >0; i--)//边界条件:ps->arr[1]=ps->arr[0]
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
4.头删和尾删
<1>.尾删
尾删的思想与尾插相似,也是从之前数据的末尾删掉一个数据(这里无需判断是否空间足够,因为肯定足够!),代码实现十分简单:
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);//确保顺序表中有元素,否则不执行
ps->size--;
}
可能有些老铁会想先将要删除的元素置为0,再将ps->size--。其实这样是不太合理的,因为我们存入的数据也可能为0,这样无法判断0是删除的数据还是插入进来的数据。而且我们也没必要那样做,因为我们直接将 ps->size--,这样我们遍历顺序表时本就无法再查询到被删除的数据了!
<2>.头删
当我们进行一次头删操作,顺序表将变成这样:
可以看出后面的元素整体向前挪动一位,并且ps->size--。
代码实现如下:
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);//确保顺序表中有元素,否则不执行
int i = 0;
for (i = 0; i < ps->size - 1; i++)//边界位置判断:ps->arr[0]=ps->arr[1]
//还有ps->arr[ps->size-2]=ps->arr[ps->size-1]
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;//注意不要丢掉
}
5.指定位置插入和删除
<1>.指定位置插入
同样的,指定位置插入数据也要先判断是否需要扩容,原理同上,这里不再过多赘述。
因为需要用户自己指定插入的位置,所以我们还需要一个参数pos来确定插入位置的下标。
数据插入后的顺序表应该是这样的:
可以看出,需要将pos位置及以后的数据整体向后挪动1个单位!!!
void SLInsert(SL* ps, Seqtype x,int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//判断pos的合法性
SLCheckCapacity(ps);//判断是否要扩容
int i = 0;
for (i = ps->size - 1; i >= pos;i--)//边界条件判断:ps->arr[pos+1] = ps->arr[pos]
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size ++ ;
}
特别的,pos=ps->size时就相当于尾插,当pos=0时相当于头插!!!
<2>.指定位置删除
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);//检验pos的合法性
assert(ps->size);//确保顺序表中有元素
int i = 0;
for (i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;//不要忘记
}
特别的,pos=ps->size-1时就相当于尾删,当pos=0时相当于头删!!!
6.查找
思路很简单,就是遍历整个顺序表
int SLFind(SL* ps, Seqtype x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;//找到了,返回下标
}
}
return -1;//找不到,返回-1
}
总结
以上就是本节的所有内容
下面将整个顺序表的所有代码放到这里
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int Seqtype;//如果我们后续要更改存储的数据的类型,这样重定义可以方便我们一键替换
typedef struct SeqList
{
Seqtype* arr;
int capacity;//记录当前顺序表的容量
int size;//记录有效数据(也就是已经保存的数据)的个数
}SL;//使结构体类型更加简洁
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size)//判断是否需要扩容
{
int newcapacity = ps->capacity == 0 ? 2 : 2*ps->capacity;
Seqtype* tmp = (Seqtype*)realloc(ps->arr, newcapacity * sizeof(Seqtype));
//刚开始ps->arr是NULL,realloc会直接开辟新空间,作用相当于malloc
//注意单位是字节,要*类型的大小
if (tmp == NULL)
{
perror("realloc");
exit(1);//直接终止程序,比return 1更加暴力
}
ps->arr = tmp;
ps->capacity = newcapacity;
}
}
void SLPushBack(SL* ps, Seqtype x)
{
assert(ps);
SLCheckCapacity(ps);//判断是否要扩容
ps->arr[ps->size++] = x;//注意:不用忘了将ps->size++。
}
void SLPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
}
void SLPushFront(SL* ps, Seqtype x)
{
assert(ps);
SLCheckCapacity(ps);//判断是否要扩容
int i = 0;
for (i = ps->size; i >0; i--)//边界条件:ps->arr[1]=ps->arr[0]
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);//确保顺序表中有元素,否则不执行
ps->size--;
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);//确保顺序表中有元素,否则不执行
int i = 0;
for (i = 0; i < ps->size - 1; i++)//边界位置判断:ps->arr[0]=ps->arr[1]
//还有ps->arr[ps->size-2]=ps->arr[ps->size-1]
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;//注意不要丢掉
}
void SLInsert(SL* ps, Seqtype x,int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);//判断pos的合法性
SLCheckCapacity(ps);//判断是否要扩容
int i = 0;
for (i = ps->size - 1; i >= pos;i--)//边界条件判断:ps->arr[pos+1] = ps->arr[pos]
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size ++ ;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);//检验pos的合法性
assert(ps->size);//确保顺序表中有元素
int i = 0;
for (i = pos; i < ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;//不要忘记
}
int SLFind(SL* ps, Seqtype x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;//找到了,返回下标
}
}
return -1;//找不到,返回-1
}