目录
线性表
顺序表Sequential List
静态顺序表
动态顺序表
主函数Test.c
test1
test2
test3
test4
头文件&函数声明SeqList.h
头文件
函数声明
函数实现SeqList.c
初始化SLInit
释放销毁SLDestroy
扩容SLCheckCapacity
打印SLPrint
尾插SLPushBack
头插SLPushFront
尾删SLPopBack
头删SLPopFront
今天介绍数据结构中的顺序表。
顺序表和链表都是最基础和最常见的实用的数据结构。
线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。(也就是说在逻辑上是依次存储的)但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表Sequential List
顺序表是用一段 物理地址连续的存储单元 依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表简单来说就是一个数组,要求这个数组的空间是连续的,且存储的数据也必须是从头开始连续存储。
静态顺序表
静态顺序表:使用定长数组存储元素。
静态顺序表的长度是固定的。 虽然在不少的书籍和课本上都喜欢实用静态顺序表去讲解和测试。但是静态顺序表的空间是固定的,空间给大了,很浪费;空间给小了,不够实用。所以在我们后面的学习中,我们需要去学习动态的顺序表。它的实用性实践性更高,当然更加难。
动态顺序表
动态顺序表:使用动态开辟的数组存储。
动态顺序表虽然可以规避一些空间浪费,但是还是会有一点点浪费。
实用动态内存开辟函数去开辟空间的时候,每次空间不够的时候都会扩容,每次扩容都会付出一定程度上的代价。所以每次扩容可以稍微多扩容一点点,减少扩容的频率。扩容是根据需求扩容,那我们最常用的扩容倍数就是:2倍 1.5倍。但是不是一定要求是这些倍数。2倍数是一个相对合适的量,扩容的频率会越来越低,具体情况具体分析!
主函数Test.c
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空
间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间
大小,所以下面我们实现动态顺序表。本章实现顺序表的头插 尾插 头删和尾删等功能进行实现。
int main()
{
SL ps;//创建一个结构题变量-传地址调用🆗-形参是实参的一份临时拷贝
test1(&ps);//测试尾插
test2(&ps);//测试头插
test3(&ps);//测试尾删
test4(&ps);//测试头删
return 0;
}
test1
#include"SeqList.h"
void test1(SL*ps)//测试尾插
{
SLInit(ps);
SLPushBack(ps, 10);
SLPushBack(ps, 20);
SLPushBack(ps, 30);
SLPushBack(ps, 40);
SLPrint(ps);
SLDestroy(ps);
}
test2
void test2(SL*ps)//测试头插
{
SLInit(ps);
SLPushFront(ps, 10);
SLPushFront(ps, 20);
SLPushFront(ps, 30);
SLPushFront(ps, 40);
SLPrint(ps);
SLDestroy(ps);
}
test3
void test3(SL*ps)//测试头删
{
SLInit(ps);
SLPushBack(ps, 10);
SLPushBack(ps, 20);
SLPushBack(ps, 30);
SLPushBack(ps, 40);
SLPopBack(ps);
SLPopBack(ps);
SLPrint(ps);
SLDestroy(ps);
}
test4
void test4(SL*ps)//测试尾删
{
SLInit(ps);
SLPushBack(ps, 10);
SLPushBack(ps, 20);
SLPushBack(ps, 30);
SLPushBack(ps, 40);
SLPopFront(ps);
SLPopFront(ps);
SLPrint(ps);
SLDestroy(ps);
}
头文件&函数声明SeqList.h
头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//断言
函数声明
- SeqList声明
//声明一个结构体
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;//如果后期a的类型改变就很方便
int size;//有效数据
int capacity;//空间容量
}SL;//SL是这个结构体的类型,用typedef定义更加方便了
- SeqList初始化
//初始化
void SLInit(SL* ps);
- SeqList释放&销毁
//释放销毁
void SLDestroy(SL* ps);
- SeqList展示
//展示
void SLPrint(SL* ps);
- 尾插 头插 尾删 头删
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopBack(SL* ps);//尾删
void SLPopBack(SL* ps);//头删
函数实现SeqList.c
初始化SLInit
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
//关于初始化 可以首先置为NULL 也可以首先放点值
// memset一般用于数组初始化 直接初始化更加清晰
释放销毁SLDestroy
//销毁
void SLDestroy(SL* ps)
{
if (ps->a != NULL)
{
free(ps->a);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
}
扩容SLCheckCapacity
这个函数是我们自己封装,可以不用去函数声明,秘密。但是记住一定要在使用这个CheckCapacity 之前我们一定要声明或者放在使用之前。
扩容就要用到我们在【动态内存管理】讲解的realloc函数了,扩容分为原地扩容和异地扩容,忘记的小伙伴再复习一下哦,一定要熟悉【戳一戳】:C语言之动态内存管理篇(1)-CSDN博客
- 原地扩容和异地扩容判断和最优写法
- realloc函数存储空间起始位置是可以传NULL空指针的
- realloc函数所需要开辟的空间一定要包含旧空间的大小
- 异地扩容对之前的空间不需要手动free,而是操作系统已经帮助释放了原来旧的空间
//扩容
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)//容量满了需要扩容的条件
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * (ps->capacity);
SLDataType* tmp = (SLDataType*)raelloc(ps->a, sizeof(SLDataType) * newcapacity);
if (tmp == NULL)
{
perror("CheckCapacity");//
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
}
打印SLPrint
//展示
void SLPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
尾插SLPushBack
- 首先在size位置放值x
- size往后移动++
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);//扩容
ps->a[ps->size] = x;
ps->size++;
}
头插SLPushFront
在顺序表的前面插入数据,是没有向前扩容和插入空间这种做法的。这段空间整体是连续的,地址是连续的,又要求数据是连续的。所以我们唯一可用的方法就是把数据往后挪动。当然我们可以用函数memcpy和memove来实现,这里我们就手动来写。如果不想挪动数据,只有一种方法就是使用链表。
- 首先把size-1的位置移动到size的位置,整体挪动完成。
- 再在第一个位置放数值
void SLPushFront(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)//注意可以等于0 size为1 但是不能为负数会越界访问
{
ps->a[end+1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
还想补充到的是:尽量别频繁的使用头插法,大量的使用不太行。
尾删SLPopBack
尾删也是非常简单的,有人可能会说把要删除的值赋值为-1或者NULL。但是我们并不能确定顺序表元素的类型,万一类型改变,这里再去改变就不方便了。而且万一这个值本来就是-1。所以直接size--即可。后期插入等操作会把这个值给覆盖掉。
- size-- 意味着着只有--之后的数值有效。意味着我们头尾插会把无效数据覆盖。
- 无效数据不需要free
- 第一动态内存是不支持分期free。
- 第二后续操作会覆盖无效数据,空间可以重复利用。
- 第三单独free,顺序表的空间就不连续了
//尾删
void SLPopBack(SL* ps)
{
assert(ps->size);//本质问题就是害怕这个顺序表空了还在删除
ps->size--;
}
规避过度头删
头删SLPopFront
- 头插是从后向前挪动数据覆盖
- 头删是从前向后挪动数据覆盖
- size--即可
//头删
void SLPopFront(SL* ps)
{
assert(ps->size);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin] = ps->a[begin + 1];
begin++;
}
ps->size--;
}
✔✔✔✔✔最后,感谢大家的阅读,若有错误和不足,欢迎指正。下章继续顺序表。
- 结构体传址调用,形式参数是实际参数的一份临时拷贝
- size永远指向所存储元素的下一个位置,顺序表是数组,下标从0开始。
- realloc函数的注意点
- assert的点
代码---------→【唐棣棣 (TSQXG) - Gitee.com】
联系---------→【邮箱:2784139418@qq.com】