顺序表分类
顺序表分为动态顺序表和静态顺序表,他们的基本结构是怎么展现的呢?
#define N 100
//静态顺序表
struct Seqlist
{
int arr[N];//存放数组大小
int size;//有效存储数据个数
};
静态顺序表只有两个成员,首先是存放数据数组,其次我们还要知道有效的数据个数,其存放数据大小是一开始便已经确定的,并不灵活!
再看动态顺序表
struct Seqlist
{
int* arr;//存放数组大小
int size;//有效存储数据个数
int capacity;//开辟空间大小
};
为啥这里是int* ?当然是因为我们需要动态开辟内存空间,既然是动态,当然要用到动态内存管理的知识 ,让我们的arr依据存储数据size需求量,对数组空间进行扩容或缩容。这里多了一个capacity表示开辟空间的大小。为啥要这样设计,因为有效存储数据size的大小并不一定等于空间capacity的大小,这点我们接下来就会明白。
其实动态和静态都能解决问题,可是对于静态顺序表来说,一开始我们也不知道到底需要存储多少数据,就像抖音的老板一样,他也不知道自己的产品需要存储多少用户信息。万一初始N给小了,就放不了太多,容易导致数据丢失;万一初始N给大了,会占用很大的内存空间!
因此,对比动态和静态顺序表,我们可以清楚的知道动态顺序表更有优势,接下来就是实现一个简单的动态顺序表。
动态顺序表的具体实现
初始化
初始化其实很简单,用一个封装函数,让arr指针为空,size和capacity都为0。也可以用malloc初始扩充一定的空间,不多赘述。
void Seqlist_Init(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
这里改良一下,将int重定义位SLDataType是为了以后方便修改数据
万一我们以后要存储字符串咋办,很多涉及到int这个关键字的地方肯定不能全用VS的替换吧,当然不可能啊,以后代码万一增多,手动修改出现错误或误操作,代码可能直接崩溃,这是很可怕的事情。
同时为了避免每次传结构体都要加上struct,把它重新命名为SL,这样会很方便!
那我初始化函数这样写对不对?
相信你们判断得没错,当然是不对的,我们渴望修改结构体里的数据,从而初始化。根据函数调用中实参和形参的关系可知,形参只是形参的临时拷贝,改变形参不会改变实参,所以传值调用是不合法的(建议可以了解一下函数栈帧的知识),对于用函数交换两个变量,我们是不是只能传两个变量的地址啊,这里也一样。
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType* arr;//存放数组大小
int size;//有效存储数据个数
int capacity;//开辟空间的大小
}SL;
这样写是不是没有问题了,我们经过调试,发现初始化成功了。
也可用malloc初始化先开辟小块空间,这里就不再赘述。
销毁
有初始化必然就有销毁!销毁就必然要将开辟的内存空间重新还给操作系统,并将size和capacity置0。
void Seqlist_Destroy(SL* ps)
{
if (ps->arr) //等价于 if(ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
这里不要忘了将arr手动置NULL。为啥啊?我们用free函数将内存释放只是失去了访问之前开辟空间的能力,首地址还是那个地址没有变,可是已经没有明确的指向,构成野指针,而且free不会自动置空。
补充一点,例如return 返回值,我们调用一个函数来返回一个结果,我们是不是一般都是返回一个值啊,万一返回一个地址咋办,而且这个地址是正常的吗?
以一道题目为例:
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
可以运行吗?
抱歉,编译器直接报错。原因嘛和上面一样啊,这个p地址还是那个地址,但是GetMemory函数执行完后p指向的空间还给操作系统了,没有访问权限,就会指向不明确,所以p为野指针,不能正确执行程序。就算有些编译器可以成功运行此程序,那也是非法访问。
插入数据
有很多种插入方法,我们以头插和尾插为例:
尾插
//尾插
void SLPushBack(SL* ps, SLDataType x)//x为插入数据
{
assert(ps);
SLCheckOut(ps);
ps->arr[ps->size++] = x;
}
直接这样写行不行?试想,万一capacity和size相等,即空间大小等于有效个数大小,是不是没有空间了,需要扩容。还有如果capacity为0,arr的地址是无效的哦,也不能插入数据吧!
因此需要写个判断函数:
void SLCheckOut(SL* ps)
{
if (ps->capacity == ps->size)
{
int NEWCapacity = (!ps->capacity) ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, NEWCapacity * sizeof(SLDataType));
//空间开辟失败
if (!tmp)
{
perror("realloc");
exit(1);
}
//空间开辟成功
ps->arr = tmp;
ps->capacity = NEWCapacity;
}
}
注意:
1、capacity == size怎么办?说明此时需要先扩容再插入数据。
2、capacity为0怎么办?所以要单独先判断capacity,为0就给个初始空间为4(随便给,别太离谱),不为0,扩容两倍。
3、realloc万一开辟失败怎么办?需要报错,来终止程序,开辟内存成功后,才能进行插入数据操作。因为一旦realloc开辟失败,不光不能解决问题,同时还有极大可能把我原来的数据删掉,这不是帮倒忙吗,因此我们使用realloc必须考虑这一点。
4、万一ps接收到空指针怎么办?那就无法实现插入数据,因此要用assert断言一下(可以去网上找一下assert的用法)其他插入删除数据同理
5、"exit(1)":只是一个简单的退出程序语句。
6、size表示有效数据个数,其初始值为0,所以插入完要++,因为是后置++,在插入数据前不会影响,也可以分开写,无所谓。
我们测试一下:
插入成功。
头插
老规矩还是先整个图,方便理解:
看到这里是不是就明白了:
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckOut(ps);
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//最终arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
测试一下:
测试成功!
打印数据
void PrintSeqlist(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
是不是很简单。
还记得刚才在头插中ps->size为啥要加啊,其实与尾插一样,既然你插入了一个数据,那么对于有效数据size来说,肯定要增加1才符合逻辑。
无“ps->size++”:
有“ps->size++”:
删除数据
显然对应前面插入数据有尾删和头删两种情况。
尾删
据图知:
void SLPopBack(SL* ps)
{
assert(ps);
ps->size--;
}
下面是测试结果:
注意:
1、不需要考虑arr里面的数据是啥,所以我们只需要打印有效数据就可以。并不需要对要删除的数据做处理。
2、还是要断言一下,如果ps为空指针,删除就无意义了。
头删
据图知:
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
注意:
1、assert(ps->size)是判断顺序表是否为空,要是size为0,虽然打印有一个数据,但是此时无效数据为0。
下面是测试:
任意位置的插删数据
插入
因此,就是要将pos位之后的数据像右移动一位,再把要放入的数据赋值给数组第pos位,最后有效数据个数加一
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
for (int i = ps->size; i >= pos + 1; i--)
{
ps->arr[i] = ps->arr[i - 1];//ps->arr[pos + 1] = ps->arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
那我们还是测试一下:
由此可知,头插尾插也是这种特殊情况。
删除
据图知,将pos位之后的数据向前移动,移动位置不在pos位前,最后有效数据个数减一。
//任意位置删除
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
SLCheckOut(ps);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//ps->arr[ps->size - 2] = ps->arr[ps->size - 1]
}
ps->size--;
}
测试一下:
注意:
1、用assert断言pos,让pos在安全且合法的范围内。
2、插入数据时,可以把数据插入到下标size上,而删除数据时,不能删除下标size-1后面的数据。
还是用图来说明一下:
相信看完这个图,你就会明白了。
查找
//查找
int FindSL(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//没有找到
return -1;
}
这里我用下标i来遍历数组中的元素,如果找到数据返回下标,如果没找到返回-1,其中-1是随便给的数,目的是为了与数组下标区分开。
补充
关于扩容的规则,可以参考一下这篇博客,扩容规则的推导涉及到概率论的知识,作者还不会。
总结
顺序表的本质就是对一个数组增删查改,顺序表是属于线性表的一种。顺序表物理和逻辑结构都是线性的,而线性表不同的地方是其只在物理结构是非线性,逻辑结构依然是线性的!
源码
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "sqlist.h"
//测试顺序表
void test()
{
//声明一下结构体
SL sl;
/*初始化*/
Seqlist_Init(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
PrintSeqlist(&sl);
SLInsert(&sl, 0, 9);
SLInsert(&sl, 2, 8);
//SLPushFront(&sl, 5);
//SLPushFront(&sl, 6);
//SLPushFront(&sl, 7);
//SLPopBack(&sl);//尾删
//SLPopFront(&sl);//头删
SLInsert(&sl, sl.size, 7);
PrintSeqlist(&sl);
SLErase(&sl, 2, 8);
SLErase(&sl, 0, 9);
SLErase(&sl, sl.size - 1, 7);
PrintSeqlist(&sl);
/*销毁*/
Seqlist_Destroy(&sl);
printf("\n");
int ret = FindSL(&sl, 4);
if (ret < 0)
{
printf("没找到");
}
else
{
printf("找到啦");
}
}
int main()
{
test();
return 0;
}
sqlist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "sqlist.h"
void Seqlist_Init(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
void Seqlist_Destroy(SL* ps)
{
if (ps->arr) //等价于 if(ps->arr != NULL)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLCheckOut(SL* ps)
{
if (ps->capacity == ps->size)
{
int NEWCapacity = (!ps->capacity) ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, NEWCapacity * sizeof(SLDataType));
//空间开辟失败
if (!tmp)
{
perror("realloc");
exit(1);
}
//空间开辟成功
ps->arr = tmp;
ps->capacity = NEWCapacity;
}
}
//尾插
void SLPushBack(SL* ps, SLDataType x)//x为插入数据
{
assert(ps);
SLCheckOut(ps);
ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckOut(ps);
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//最终arr[1] = arr[0]
}
ps->arr[0] = x;
ps->size++;
}
//打印
void PrintSeqlist(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
ps->size--;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
//判断顺序表是否为空
assert(ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
//任意位置插入
void SLInsert(SL* ps, int pos)
{
assert(ps);
//不能让顺序表为空
assert(pos >= 0 && pos <= ps->size);
SLCheckOut(ps);
for (int i = ps->size; i >= pos + 1; i--)
{
ps->arr[i] = ps->arr[i - 1];//ps->arr[pos + 1] = ps->arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
//任意位置删除
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
SLCheckOut(ps);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
//查找
int FindSL(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
//找到了
return i;
}
}
//没有找到
return -1;
}
sqlist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//#define N 100
静态顺序表
//struct Seqlist
//{
// int arr[N];//存放数组大小
// int size;//有效存储数据个数
//};
//动态顺序表
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType* arr;//存放数组大小
int size;//有效存储数据个数
int capacity;//开辟空间的大小
}SL;
//初始化
void Seqlist_Init(SL* ps);
//销毁
void Seqlist_Destroy(SL* ps);
//打印
void PrintSeqlist(SL* ps);
//头部插入 / 尾部插入
void SLPushBack(SL* ps, SLDataType x);
void SLPushFront(SL* ps, SLDataType x);
//头部删除 / 尾部删除
void SLPopBack(SL* ps);
void SLPopFront(SL* ps);
//任意位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
//任意位置删除
void SLErase(SL* ps, int pos, SLDataType x);
//查找
int FindSL(SL* ps, SLDataType x);