目录
一、顺序表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线序结构(本质上是数组)。
大白话就是数据是挨着存放的。
二、静态顺序表和动态顺序表比较
静态顺序表使用定长数组存储元素:
现实中基本不使用静态顺序表,因为要考虑到要确认存储多少数据的场景,如果定长数组太大,导致空间浪费,定长数组太小则不够用。
动态顺序表使用动态开辟的数组存储元素:
动态顺序表可以按需动态申请空间大小,也是现实中经常使用的,规避了静态顺序表的缺点,因此该篇文章主要实现动态顺序表。
三、动态顺序表的实现
首先创建三个文件SeqList.h SeqList.c test.c,分别完成函数声明,代码实现,代码测试功能
1、定义动态顺序表和完成函数声明
//在SeqList.h中
#pragma once//防止头文件重复包含
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
typedef struct SList
{
SLDataType* a; //指针指向动态开辟的数组
int size; //有效数据个数
int capacity; //容量空间大小
}SL;
void SLInit(SL* ps); //初始化
void SLDestory(SL* ps); //销毁
void SLPrint(SL* ps); //打印
void SLCheckCapacity(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); //任意位置删除
int SLFind(SL* ps, SLDataType x); //查找
void SLModify(SL* ps, int pos, SLDataType x);//修改
2、顺序表的初始化
//在SeqList.c文件中
void SLInit(SL* ps)
{
//将指向动态数组的指针置为空
//有效数据个数size和容量空间大小capacity置为0
assert(ps);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
3、顺序表的销毁
//在SeqList.c文件中
void SLDestory(SL* ps)
{
//如果指向动态数组的指针不为空就将其free释放
//指向动态数组指针置为空
//有效数据个数和容量空间大小置为0
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
}
4、顺序表的打印
//SeqLsit.c文件中
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ",ps->a[i]);
}
printf("\n");
}
5、顺序表的扩容
//在SeqList.c文件中
void SLCheckCapacity(SL* ps)
{
//当有效数据个数等于该顺序表容量的时候要进行扩容
//分为两种情况:
//第一种:一开始容量就为0,需要开辟空间,这里初始化选择容量空间大小为4
//第二种:一开始容量不为0,存储数据后,当size等于capacity时,
//可以在该基础上扩容2倍
assert(ps);
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity*sizeof(SLDataType));
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1); //realloc失败的情况下退出
}
ps->a = tmp; //realloc后的数组地址赋值给指针a
ps->capacity = newcapacity; //新开辟的容量赋值给capacity
}
}
6、顺序表尾插
//在SeqList.c文件中
void SLPushBack(SL* ps, SLDataType x)
{
//尾部插入数据的时候要考虑空间是否足够
//将x放入size的位置即可完成尾插
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
//代码复用
//SLInsert(ps, ps->size, x);
}
7、顺序表头插
//在SeqList文件中
void SLPushFront(SL* ps, SLDataType x)
{
//检查容量大小
//将数据都往后移动
//插入后size要增加
assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
//代码复用
//SLInsert(ps, 0, x);
}
8、顺序表尾删
//在SeqList.c文件中
void SLPopBack(SL* ps)
{
//断言处检查是否有数据可以删除
assert(ps);
assert(ps->size > 0);//防止越界
ps->size--;
//代码复用
//SLErase(ps,ps->size-1);
}
9、顺序表头删
//在SeqList.c文件中
void SLPopFront(SL* ps)
{
//数据往前移动覆盖第一个数据
//while循环中要begin < ps->size-1
//写成begin < ps->size 会越界(容易范的错误)
assert(ps);
int begin = 0;
while (begin < ps->size - 1)
{
ps->a[begin] = ps->a[begin + 1];
}
ps->size--;
//代码复用
//SLErase(ps,0);
}
10、顺序表查找
int SLFind(SL* ps, SLDataType x)
{
//找到返回该数字下标位置
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
11、在顺序表pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
12、在顺序表pos位置删除x
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
13、修改顺序表pos位置的值
void SLModify(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
ps->a[pos] = x;
}
四、代码测试
//在test.c文件中
#define _CRT_SECURE_NO_WARNINGS 1
#include"SLlist.h"
void SLtest1()
{
//测试尾插和头插
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPrint(&sl);
SLPushFront(&sl, 5);
SLPushFront(&sl, 4);
SLPushFront(&sl, 3);
SLPushFront(&sl, 2);
SLPushFront(&sl, 1);
SLPrint(&sl);
SLDestory(&sl);
//运行结果
//1 2 3 4 5
//1 2 3 4 5 1 2 3 4 5
}
void SLtest2()
{
//测试头删和尾删
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 1);
SLPushBack(&sl, 3);
SLPushBack(&sl, 2);
SLPushBack(&sl, 2);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPopFront(&sl);
SLPopFront(&sl);
SLPrint(&sl);
SLDestory(&sl);
//运行结果
//1 1 3 2 2
//1 1 3
//3
}
void SLtest3()
{
//测试任意位置插入删除
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 1);
SLPushBack(&sl, 3);
SLPushBack(&sl, 2);
SLPushBack(&sl, 2);
SLPrint(&sl);
SLInsert(&sl, 2, 300);//在下标为2的位置插入300
SLPrint(&sl);
SLErase(&sl, 2);//删除在下标为2的位置的数
SLPrint(&sl);
SLDestory(&sl);
//运行结果
//1 1 3 2 2
//1 1 300 3 2 2
//1 1 3 2 2
}
void SLtest4()
{
//测试查找和修改
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 1);
SLPushBack(&sl, 3);
SLPushBack(&sl, 2);
SLPushBack(&sl, 2);
SLPrint(&sl);
int x = 0;
int y = 0;
printf("请输入你要修改的值和修改后的值:");
scanf("%d %d", &x,&y);
int pos = SLFind(&sl, x);
if (pos != -1)
{
SLModify(&sl, pos,y);
printf("修改后");
SLPrint(&sl);
}
else
{
printf("没找到该值");
}
SLDestory(&sl);
//1 1 3 2 2
//请输入你要修改的值和修改后的值:3 300
//修改后1 1 300 2 2
}
int main()
{
SLtest1();//测试尾插和头插
SLtest2();//测试头删和尾删
SLtest3();//测试任意位置插入删除
SLtest4();//测试查找和修改
return 0;
}
五、动态顺序表代码中的相关问题
1、为什么要assert断言判断(防御式编程)
判断位置的合法性:在插入删除的时候会有越界问题,在任意位置插入和删除的时候要考虑pos位置的合法性。
防止结构体指针为空指针:顺序表结构体指针应该指向一个合适的结构体,对结构体里的数据进行增删查改。
2、realloc的功能
对已有的空间扩容,有两种扩容方式,原地扩容和异地扩容
函数原型 void* realloc(void* ptr ,size_t size)
如果ptr指向的空间是空指针的话,它的行为和malloc一样。
size单位是字节数
3、越界问题
有些越界检查是在free之后才发生的,但是越界不一定会报错,因为系统对越界的检查是设岗抽查
所以在删除有效数据的时候要判断是否有能删除的数据
4、效率问题
头删和头插的时间复杂度是o(n)
尾插和尾删的时间复杂度是o(1)