顺序表我们采用将函数声明放到SeqList.h里面,函数的实现放到SeqList.c里面,test.c调用函数实现。
线性表
- 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结
构,常见的线性表:顺序表、链表、栈、队列、字符串…
- 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表
1)什么是顺序表
- 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
- 顺序表:可动态增长的数组,要求数据是连续存储的。
2)顺序表的定义
1、静态顺序表:使用定长数组存储元素
#define N 10
typedef int SLDataType;
typedef struct SeqList
{
SLDataType a[N]; //定长数组
size_t size; //有效数据个数
}SL;
2、动态顺序表:使用动态开辟的数组存储元素
- 动态顺序表可根据我们的需要分配空间大小
- size 表示当前顺序表中已存放的数据个数
- capacity 表示顺序表总共能够存放的数据个数
typedef int SLDataType; //类型重命名,后续要存储其它类型时方便更改
typedef struct SeqList
{
SLDataType* a; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量大小
}SL;
2)顺序表的接口实现
首先新建一个工程( 博主使用的是 VS2019 )此次用的是动态顺序表
- SeqList.h(顺序表的类型定义、接口函数声明、引用的头文件)
- SeqList.c(顺序表接口函数的实现)
- Test.c(主函数、测试顺序表各个接口功能)
如图:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//动态顺序表
#define INIT_CAPACITY 4
typedef int SLDataType;
typedef struct SeqList
{
int size; //有效数据个数
int capacity; //空间容量
SLDataType* a;
}SL;
void SLInit(SL *ps); //初始化
void SLDstroy(SL *ps); //销毁
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x); //尾插 x为插入的数据
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x); //头插 x为插入的数据
void SLPopFront(SL* ps);
// 顺序表查找
int SListFind(SL* ps, SLDataType x);
// 顺序表在pos位置插入x
void SListInsert(SL* ps, int pos, SLDataType x);
// 顺序表删除pos位置的值
void SListErase(SL* ps, int pos);
这里重点讲解 SeqList.c 中各个接口函数的实现。
1、初始化顺序表
记得一定要加上断言,防止传进来的指针为空
void SLInit(SL *ps)
{
assert(ps);
//初始化
ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = INIT_CAPACITY;
ps->size = 0;
2、销毁(释放)顺序表
记得一定要加上断言,防止传进来的指针为空
void SLDstroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL; //置空
ps->capacity = ps->size = 0; //从右往左赋值
}
3、检查顺序表容量是否满了,好进行增容
为什么不采取插一个数据,增容一个空间的方式呢,因为这样也太麻烦了,代价也太大了,一般情况下,为了避免频繁的增容,当空间满了后,我们不会一个一个的去增,而是一次增容 2 倍,当然也不会一次增容太大,比如 3 倍 4 倍,空间可能会浪费,2 倍是一个折中的选择。
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2); //用realloc 给capacity增容
if (tmp == NULL) //扩容失败怎么办
{
perror("realloc fail");
return;
}
//增容为tmp
ps->capacity *= 2;
ps->a = tmp;
3、顺序表尾插
void SLPushBack(SL* ps, SLDataType x) //尾插 x为插入的数据 加的时候需要判断一下 size和capacity的大小
{
//SLCheckCapacity(ps);
//ps->a[ps->size++] = x;
SListInsert(ps, ps->size, x);
}
4、顺序表尾删
不知道 SLDataType 是什么类型的数据,不能冒然的将顺序表最后一个数据赋值为 0,我们只需将有效数据个数 size 减 1 即可达到尾删的目的。
void SLPopBack(SL* ps) // 尾删 原理和尾插差不多 越界问题 注意size的检查
{
////暴力检查
////assert(ps->size > 0);
////温柔的检查
//assert(ps);
//if (ps->size == 0)
//{
//
// return;
//}
//ps->a[ps->size-1] = 0;
//ps->size--;
SListErase(ps, ps->size);
}
5、顺序表头插
因为顺序表是连续存储的,所以头插时要依次挪动数据
void SLPushFront(SL* ps, SLDataType x) //头插 x为插入的数据
{
//assert(ps);
//SLCheckCapacity(ps);
//
//
//ps->a[ps->size++]=0;
//if (ps->size > ps->capacity)
//{
// perror("SLPushFront fail");
// return;
//}
//for (int i = ps->size - 1; i > 0; i--) //交换位置
//{
//
// int temp = 0;
// temp = ps->a[i];
// ps->a[i] = ps->a[i - 1];
// ps->a[i - 1] = temp;
//}
//ps->a[0] = x;
SListInsert(ps, 0, x);
}
6、顺序表头删
因为顺序表是连续存储的,所以头删时要依次挪动数据
void SLPopFront(SL* ps) //头删
{
//assert(ps);
//if (ps->size == 0) //防止越界
//{
// return;
//}
////把第一个数据放到整个最后
//for (int i = 0; i < ps->size; i++)
//{
// int temp = 0;
// temp = ps->a[i];
// ps->a[i] = ps->a[i + 1];
// ps->a[i + 1] = temp;
//}
//ps->a[ps->size - 1] = 0;
//ps->size--;
SListErase(ps, 0);
}
7、打印顺序表
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
8、在顺序表中查找指定值
// 顺序表查找
int SListFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
9、在顺序表指定下标位置插入数据(要注意下int与size_t间的转换问题)
原先这种写法,当顺序表为空 size = 0 时,会导致 i = -1,
执行 i >= pos 时,i 被算术转换成无符号数,而无符号数的 -1 是一个值很大的正数,
远大于 pos,满足条件进入循环,会造成越界访问
注:转换并不会改变 i 本身的值,而是执行 i >= pos 时,生成一个临时的值与 pos 比较
如果在顺序表头部插入数据 pos = 0,i 最终也会减减变成 -1,被算术转换后变成一个很大的数
总结:避免负数给到无符号数,或者避免有符号数变成负数后,被算术转换或整型提升后,变成一个很大的数
下面这样写就避免 i 变成 -1 负数了。
// 顺序表在pos位置插入x
void SListInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos > 0 && pos <= ps->size);
SLCheckCapacity(ps);
int i = ps->size-1;
while (i >= pos)
{
ps->a[i+1] = ps->a[i];
i--;
}
ps->a[pos] = x;
ps->size++;
}
10、在顺序表中删除指定下标位置的数据
// 顺序表删除pos位置的值
void SListErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
if (ps->size == 0)
{
return;
}
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
越界不一定报错,系统对越界的检查是一种抽查
- 越界读一般是检查不出来的
- 越界写如果是修改到标志位才会检查出来
(系统在数组末尾后设的有标志位,越界写时,恰好修改到标志位了,就会被检查出来)
12、修改指定下标位置的数据
void SListAt(SL* ps, int pos, SLDataType x)
{
assert(ps); //断言
assert(ps->size > 0); //顺序表不能为空
assert(pos >= 0 && pos < psl->size); //检查pos下标的合法性
ps->a[pos] = x; //修改pos下标处对应的数据
}