目录
前言
顺序表是咱学习数据结构时接触到的第一种数据结构。
在学习顺序表之前,我们需要先弄明白一个问题:何为数据结构?
数据结构,单从字面上来讲就是数据的结构。
设想一下,内存中有一大堆数据(假设这些数据是数字1、2、3...等等)
然后我们想在这堆数据中找到并拿出其中一个数据 “3” ,要怎么找呢?
这时候就需要用到数据结构的知识——将数据有序的存储起来(排序),
接着就只需要访问对应的序列就能找到该数据了。
总结一下上面这个例子,我们可以得知数据结构有两个特性:
1.能够储存数据
2.储存的数据方便查找(删除、或者修改)
说的通俗易懂一点,数据结构就是一种方便存储数据和访问数据的代码结构。
1.顺序表的概念及结构
数据结构有很多种类,线性表就是一种数据结构,而今天我要讲的顺序表就是一种顺序表。
1.1线性表的概念
什么是线性表?
简单的说,线性表就是逻辑上(物理上不一定)呈线性关系(连续)的数据结构。
举个例子:
我们平时用的数组,逻辑上就是成线性关系的(数组的物理上也呈线性关系)。
而接下来要讲解的顺序表的底层逻辑就是数组。
你或许会有点怀疑:真的有物理上不呈线性关系的线性表吗?
有!这种线性表被称为链表,但是我今天不讲这个。
链表的抽象概念大抵如下图:
1.2顺序表与数组的区别
顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接⼝。
说白了就是更灵活,更多功能,更牛b的数组。
但是这里又提到了一个接口的概念,我就顺便讲一下吧。
1.3接口函数
所谓接口函数,就是将函数封装成一个个接口,实现函数功能模块化,每种函数实现一种功能,需要实现这种功能的时候就调用这个函数。
举个例子:
我们平时用的计算器有加减乘除等等功能。
如果用代码写一个计算器出来,那么就需要封装各种功能函数。
比如:实现加法的函数Add,实现减法功能的函数Sub...等等。
而我们将这种被封装为实现特定功能的函数称为接口函数。
2.顺序表的分类
顺序表可以分为两大类:静态顺序表和动态顺序表。
2.1静态顺序表
概念:使用定长数组储存元素(不可扩容)。
缺陷:空间给少了不够用,空间给多了会浪费内存。
例如:
2.2动态顺序表
静态顺序表使用的是定长数组,动态顺序表使用的则是柔性数组,(可扩容)
动态顺序表的特性弥补了静态顺序表的缺陷。
需要注意的是,图中申请空间时使用的函数是realloc,考虑到可能有些人不了解,接下来我就
简单讲解一下这个函数。
void* realloc (void* ptr, size_t size);
realloc - C++ Reference (cplusplus.com)
该函数共有两个参数:①指针ptr ②无符号整数 size
重要内容大概就是:
重新申请一块size字节大小的空间,并将指针ptr指向空间中的数据拷贝到新申请的空间。
最后返回这个新申请空间的地址。(原来的那个旧的内存块会被free掉)
因此,就需要一个temp指针来保存返回来的新空间的地址,再将temp的值赋给指针a。
3.实现动态顺序表
一个完整的动态顺序表,需要具备有增删查改等功能,因此咱需要写各种接口函数来完善其功能。
需要的接口函数如下:
// 动态顺序表 -- 按需申请
#define INIT_CAPACITY 4
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
//初始化和销毁
void SLInit(SL* ps);//初始化顺序表
void SLDestroy(SL* ps);//销毁顺序表
void SLPrint(SL* ps);//打印顺序表
//扩容
void SLCheckCapacity(SL* ps);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPopBack(SL* ps);//尾删
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopFront(SL* ps);//头删
//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
int SLFind(SL* ps, SLDataType x);//查找某个数据的下标
共11个接口函数。
对于以上11个接口函数,我着重讲解后面8个接口函数。
前三个接口函数的代码我直接给出来,大伙自己看看就好。
//初始化和销毁
void SLInit(SL* ps)
{
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
void SLDestroy(SL* ps)
{
free(ps->a);
ps->size = 0;
ps->capacity;
}
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
printf("%d ", ps->a[i]);
printf("\n");
}
问:为什么函数形参要用指针?
答:假设创建一个顺序表:SL seqlist;
若要实际改变顺序表的结构,就必须往函数中传入顺序表的指针。
例如:SLPushBack(&seqlist,1);
问:既然要改变顺序表的时候必须要传指针,那么为什么打印顺序表也传指针?
答:当传入的是结构体而不是指针时,函数内会临时生成一个结构体变量并将实参的数值
赋值给该临时变量,此时就会消耗多余的内存。而如果使用的是指针,指针的大小 永远为4/8,不用担心内存问题。
3.1 SLCheckCapacity
该函数为扩容函数,其功能顾名思义:当数组空间存满时将空间扩大到原来的两倍。
代码实现:
//扩容
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)//当空间存满时
{
//若capacity等于0,就将其值初始化为4(宏定义),若不等于0,则扩容为之前的两倍
int newcapacity = ps->capacity == 0 ? INIT_CAPACITY : 2 * ps->capacity;
SLDataType* temp = realloc(ps->a, newcapacity * sizeof(SLDataType));
ps->a = temp;
ps->capacity = newcapacity;
}
}
3.2 SLPushBack
接下来的这些插入/删除接口函数才是这篇博客的重头戏!
尾插的概念:往数组末尾插入数据。
画图分析:
代码实现:
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);//当传入的指针ps为空指针时断言报错
SLCheckCapacity(ps);//检查数组空间有没有满
ps->a[ps->size] = x;
ps->size++;
}
3.3 SLPopBack
该函数的功能就是删除数组末尾的第一个数据。
其实这个函数的实现是非常简单的,只需要将size--即可。
将size--后,数组就无法访问到这个数据,当尾插新数据时,这个数据就会被新数据覆盖掉。
假设对上图顺序表进行尾删,尾删一次size就会减少1。
如此一来,该顺序表只能访问到1、2、3三个数据,而访问不到数据4。
当尾插新数据时,数据4就会被新数据覆盖。
最后再来看看代码实现,可谓是极其简单!
//尾删
void SLPopBack(SL* ps)
{
assert(ps);//当指针ps为空是断言报错
assert(ps->size);//当顺序表中没有数据时断言报错
ps->size--;
}
3.4 SLPushFront
函数功能:在数组的最开头处插入数据。
例如:
可以看到,头插函数的执行可以分为两步:
①将数组内所有有效数据全部往后挪动一位。
②在数组最开头处插入数据。
代码实现:
//头插
void SLPushFront(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);
//将所有数据往后移一位
for (int i = ps->size; i > 0; i--)
{
ps->a[i] = ps->a[i - 1];
}
//将数组的第一个数据覆盖为新数据x
ps->a[0] = x;
ps->size++;//别忘了size++
}
3.5 SLPopFront
我之前说过尾删的接口函数十分简单,那么头删呢?
头删会比尾删更复杂一点点,因为头删比尾删多了一步:将数组内所有数据往前挪一位。
然后再size--
代码逻辑如下图:
代码实现:
void SLPopFront(SL* ps)
{
assert(ps);//当传入的指针为空时断言报错
assert(ps->size);//当顺序表内没有数据时断言报错
//将数据全体往后挪动一位
for (int cur = 0; cur < ps->size - 1; cur++)
{
ps->a[cur] = ps->a[cur + 1];
}
ps->size--;
}
3.6 SLInsert
该函数的功能为:在指定的下标处插入一个数据
相较于头插尾插函数,该函数更具灵活性,代价是需要传入要插入位置的下标pos。
示例:
当需要在下标为1的位置处插入数据时,就需要传入pos=1
即调用函数SLInsert(&seqlist,1,100);
需要注意的是,在pos位置插入数据时,需要将pos处及后面的数据往后挪一格。
因此需要编写一个循环程序来执行后移动作。
实际代码如下:
//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);//断言
assert(pos >= 0 && pos<=ps->size);//插入的下标必须大于等于0,且小于等于size
SLCheckCapacity(ps);//检查是否需要扩容
//将pos及后面的数据往后挪一格
for (int i = ps->size; i > pos; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
3.7 SLErase
该函数的功能与SLInsert相反,是在指定位置删除一个数据。
示例:
同样的,编写SLErase函数时需要注意,要把pos后面的数据全体往前移动一格。
重点注意移动数据循环结构!!
//指定位置删除数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos<ps->size && pos >= 0);//pos的位置要规范
assert(ps->size);//当顺序表数据为空时删除会报错
//pos往后的数据全体往前挪一格
for (int i = pos; i < ps->size; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
3.8 SLFind
该函数的功能为查找指定数据在数组中的位置,并返回其下标。
代码思路:
for循环遍历数组,当数组元素等于目标数据x时,返回该下标。
若没有找到,则返回-1。
//查找
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
return EOF;
}
使用SLFind查找下标,再配合SLInsert或者SLErase接口函数,就可以灵活增删数据。
SLInsert和SLErase甚至可以直接替代掉头插头删、尾插尾删的接口函数。
例如:
//用SLInsert/SLErase实现版本
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x)
{
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)
{
SLErase(ps, ps->size-1);
}
void SLPushFront(SL* ps, SLDataType x)
{
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)
{
SLErase(ps, 0);
}
完
看完了觉得有用的姥爷们求求您们点个免费的赞和关注,这对我真的很重要!非常感谢大家!