一、顺序表基础知识
1.1 线性表
线性表是具有一类相同特性
的数据结构的集合
常见的线性表:顺序表、链表、栈、队列、字符串
线性表的数据呈线性从左往右挨着存放
线性表的两种结构
逻辑结构:人为想象出来的数据组织形式 - - 一定是线性
例如:我们认为排队的时候,人们整体是呈一条直线或曲线在排列
物理结构:数据在内存上的存储形式 - - 不一定是线性
(1)数组 - -> 空间是连续的
(2)链表:数据是一块一块的,用指针链接起来(java中用引用来链接)- -> 空间是不连续的
1.2 顺序表
作用:把数据存起来
顺序表是用一段物理地址
连续
的存储单元依次
存储数据元素的线性结构,一般采用
数组存储
,在数组上完成数据的增删查改数据结构完成的是对顺序表的增删查改等接口函数
顺序表要求
1.连续的物理空间存储 ,一般采用数组存储
2.数据必须是从头开始,依次存储(不能间隔着存)
顺序表特点
逻辑结构:一定是线性
物理结构:一定是线性
底层结构是数组
顺序表的分类
1)静态顺序表:使用定长数组存储,长度是固定的
缺点:空间给小了不够用,给大了浪费
2)动态顺序表:使用动态开辟的数组存储
二、代码实现动态顺序表
2.1 所需文件
SeqList.c: 完成数据的增删查改等函数的具体实现
SeqList.h:完成完成数据的增删查改等接口函数
Test.c :对 SeqList.c 里写的函数进行测试
Seq 是sequnce的缩写,意思为流畅的,流利的
List 意思为列表
2.2 SeqList .h文件
顺序表是对数组进行封装,并且由于涉及多个数据,所以我们使用结构体
单个数据定义变量,多个数据定义结构体
一般我们定义顺序表之前,是不知道顺序表具体大小的,所以我们定义的第一个成员数组用一个指针变量 int* arr 来表示,等到程序运行中需要使用的时候再申请空间(动态内存管理)。又因为动态顺序表可以增容,所以我们再定义一个成员变量int capacity用来记录当前顺序表的空间大小。
因为顺序表中不只能存储整型的数据,所以为了方便我们以后修改顺序表里数据的类型,我们使用 typedef 将不确定
的数据类型 (比如int* arr中的int类型),重命名为SLDatatype
由于在顺序表中,我们可以动态地插入数据,所以我们需要定义一个变量 int size,用来记录当前顺序表的有效数据个数
由于原结构体的名字比较长,不方便书写,所以我们使用 typedef 将其重命名为SL
(1) 顺序表结构的初始化、销毁、打印 - - 函数声明
返回类型:void
形参:指向结构体的指针变量 ps
形参的部分为什么要使用指针变量来接收呢?原因是函数的传参分为传值调用和传址调用,
传值调用:把实参拷贝给形参,实参和形参指向的是两块不同的空间,对形参的改变不会影响到实参
传址调用:把实参的地址拷贝给形参,形参和实参指向的是同一块空间,对形参的改变会影响到实参
由于涉及到在屏幕上打印信息,所以要包含头文件 stdio.h
(2) 插入数据(头插,尾插)- - 函数声明
返回类型:void
形参:指向结构体的指针变量 ps 和要插入的数据 x
需包含的头文件:
插入数据涉及到动态内存管理,要包含头文件 stdlib.h使用 assert 判断插入的数据是否为空,要包含头文件 assert.h
(3) 删除数据(头删,尾删) - - 函数声明
返回类型:void
形参:指向结构体的指针变量 ps
(4) 在指定位置插入,删除数据 – 函数声明
1)在指定位置插入数据
返回类型:void
形参:指向结构体的指针变量 ps,指定插入的数据,指定插入数据位置的下标
2)在指定位置删除数据
返回类型:void
形参:指向结构体的指针变量 ps,指定删除数据位置的下标
(5)查找数据 – 函数声明
返回类型:int
形参:指向结构体的指针变量 SL* ps ,要查找的下标 x
查找数据分两种情况
1)查找成功,我们需要返回该数据在顺序表中对应的下标
2)查找失败,我们只需要返回一个无效的下标,比如-1
所以查找数据的函数需要一个整型的返回值
2.3 SeqList.c文件
要包含头文件 SeqList.h
(1) 顺序表的初始化,销毁,打印
初始化:
初始情况下数组里没有数据,我们把它置为空,等到要使用的时候再动态地申请空间。
数组为空,所以有效数据个数和空间大小都为0
销毁:
销毁顺序表之前,我们需要先判断顺序表底层的数组是否为空,如果不为空,我们就将申请的空间释放
空间释放后,将数组置为空,有效数据个数和空间大小的值为0,让顺序表回到初始状态
打印:
使用循环来打印顺序表里的有效数据(size)
(2) 插入数据(头插,尾插)
插入数据分为头插(从头部插入数据)和尾插(从末尾插入数据)
但无论是哪种方法插入数据,我们都需要先判断空间是否充足
以尾插为例:
空间充足:直接往size指向的位置插入一个数据,再size++
空间不够:需要先增容 (一般两倍) ,然后在size指向的位置插入一个数据,再size++
所以我们先封装一个判断空间是否充足的函数,后续只要是涉及到插入数据,我们就先调用这个函数,用来检查空间是否充足
判断空间是否充足的函数实现思路:
- 首先,判断空间是否充足
当capacity==size时,说明顺序表满了,需要增容(一般两倍)
增容前需要判断capacity的值,如果capacity为0,就给默认值,否则就capacity*2
if (ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
}
- 然后,动态申请空间
动态申请空间有两种情况,申请成功和申请失败。
1)如果申请失败,会返回空,导致顺序表里的所有数据都被清空,所以我们用一个临时变量tmp来接收返回值,然后判断tmp里的值是否为空,如果为空,就不继续插入数据,打印完提示信息后就退出
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity * sizeof(SLDatatype));
if (tmp == NULL)//动态申请空间失败
{
perror("realloc fail!");//打印信息
exit(1);//不继续插入数据,退出
}
realloc的两个参数分别表示:对哪块地址增加空间, 增加多少
字节
的空间(size_t类型)
2)如果申请成功,就将tmp的值赋给arr,将增容后的空间赋给capacity
//动态申请空间成功
ps->arr = tmp;
ps->capacity = newCapacity;//容量增大为2倍
尾插:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 判断空间是否充足(调用SLCcapacity函数)
空间充足:直接往size指向的位置插入一个数据,再size++
空间不够:先增容 (一般两倍) ,然后在size指向的位置插入一个数据,再size++
头插:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 判断空间是否充足(调用SLCcapacity函数)
- 原数组里的数据整体向后挪动一位 ,将要插入的数据放到数组的第一个位置
相应代码:
(3) 删除数据(头删,尾删)
尾删:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 判断顺序表结构是否为空,为空(size = 0),则不可删除
- 有效数据个数-1
相应代码:
头删:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 判断顺序表结构是否为空,为空(size = 0),则不可删除
- 将数组中第一个数据之后的数据整体向前挪动一位
1)把下标为0的数据覆盖
2)把后一个位置的数据赋给前一个位置的数据
相应代码:
(4) 在指定位置插入,删除数据
在指定位置插入数据:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 限制
指定位置的下标
pos的范围,[0,size] 之间
1)pos = 0,相当于头插
2)pos = size,相当于尾插 - 判断空间是否足够(CapaCity(ps) )
- 让pos及pos之后的数据整体后移一位
- 将要插入的数据赋给arr[pos],有效数据个数+1
相应代码:
删除指定位置的数据:
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- 判断顺序表结构是否为空,为空(size = 0),则不可删除
- 限制
指定位置的下标
pos的范围,[0,size) 之间
1)pos = 0,相当于头删
2)pos不能等于size,原因:size指向的是最后一个有效数据的下一个位置,是没有数据的 - 把pos之后的数据整体向前挪动一位
- 有效数据个数-1
(5)查找数据
- 判断传过来的是否是一个有效的顺序表结构 assert(ps)
- for循环查找数据
1)如果查找到,就返回数据对应的下标
2)如果没有查找到,就返回无效数据,比如 -1
2.4 Test.c文件中
要包含头文件 SeqList.h
(1) 测试顺序表的初始化和销毁
(2) 测试插入数据(头插,尾插)
尾插:
运行结果:
头插:
运行结果:
(3) 测试删除数据(头删,尾删)
尾删:
运行结果:
我们再测试一下把顺序表里的数据删完后,还能否再删除数据
出现断言为假的报错,说明代码正确
头删:
同样,测试一下把顺序表里的数据删完后,还能否再删除数据
出现断言报错的弹窗,说明代码正确
(4) 测试在指定位置插入、删除数据
1. 测试指定位置插入数据:
测试在数据头部、中间,尾部插入数据,
测试,当顺序表为空时插入数据
2. 测试删除指定位置数据:
(5)测试查找数据
三、动态顺序表完整代码
SeqList .h文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//增强程序的可维护性
typedef int SLDatatype;//将int类型重命名
typedef struct SeqList {
SLDatatype* arr;
//只能为整型
int capacity;//空间大小
int size; //当前顺序表中有效数据个数
}SL;
//typedef struct SeqList SL;
//初始化
void SLInit(SL* ps);
//销毁
void SLDestroy(SL* ps);
//打印
void SLPrint(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, SLDatatype x, int pos);
//删除指定位置的数据
void SLErase(SL* ps, int pos);
//查找数据
int SLFind(SL* ps, SLDatatype x);
SeqList.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
//顺序表的初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
//销毁
void SLDestroy(SL* ps)
{
if (ps->arr)//相当于ps->arr != NULL
{
free(ps->arr);//申请的空间释放
}
//让其回到最初始的状态
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
//打印
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
//判断空间是否充足
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
//判断capacity, 0*2 =0,如果为0,给默认值,否则*2倍
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
//动态申请空间
SLDatatype* tmp = (SLDatatype*)realloc(ps->arr, newCapacity * sizeof(SLDatatype));
//动态申请空间失败
if (tmp == NULL)
{
perror("realloc fail!");
exit(1); //不继续插入数据,退出
}
//动态申请空间成功
ps->arr = tmp;
ps->capacity = newCapacity;//空间大小增加2倍
}
}
//插入数据(头插,尾插)
void SLPushBack(SL* ps, SLDatatype x)//尾插
{
//先判断,传过来的顺序表结构不能为空
assert(ps);//等价于assert(ps != null)
SLCheckCapacity(ps);//判断空间是否充足
ps->arr[ps->size++] = x;
}
void SLPushFront(SL* ps, SLDatatype x)//头插
{
assert(ps);
SLCheckCapacity(ps);
//数据整体后移一位
int i = 0;
for (i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//下标为0的位置空出来
ps->arr[0] = x;
ps->size++;//插入数据,有效数据+1
}
//删除数据(头删,尾删)
void SLPopBack(SL* ps)//尾删
{
assert(ps);//判断是否为有效的顺序表结构
assert(ps->size);//判断顺序表结构是否为空
ps->size--;//有效数据个数-1
}
void SLPopFront(SL* ps)//头删
{
assert(ps);
assert(ps->size);
//数据整体向前挪动一位
int i = 0;
for (i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
//有效数据个数-1
ps->size--;
}
//在指定位置之前插入数据(空间足够才能插入数据)
//pos指的是下标的位置
void SLInsert(SL* ps, SLDatatype x, int pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
//先判断空间大小是否足够
SLCheckCapacity(ps);
//pos及向后的数据整体向后移动一位
int i = 0;
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
//删除指定位置的数据
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
assert(ps->size);//若顺序表结构为空,则不可删除数据
//pos之后的数据整体向前挪动一位
int i = 0;
for (i = pos; i < ps->size - 1 ; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
//查找数据
int SLFind(SL* ps, SLDatatype x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (x == ps->arr[i])
{
return i;
}
}
return -1;
}