本章概述
- 线性表
- 顺序表
- 顺序表问题与思考
- 彩蛋时刻!!!
线性表
- 概念:一些在逻辑上成线性关系的数据结构的集合。线性表在逻辑上一定成线性结构,在物理层面上不一定成线性结构。常见的线性表:顺序表,链表,栈,队列,字符串……。
- 逻辑线性结构:就是人为想像的具有某种联系的结构,这种关系是线性的。如图所示:
- 线性物理结构:在空间上是连续的,是线性的,就像火车一样,各个车厢是一节一节的连接的。反之,就是非线性物理结构。在内存中的体现就是地址空间是连续的,如图所示:
- 为什么有的线性表物理上不是线性结构呢?:这就好比我们在一家餐馆排队取餐一样,我们都各自排好队,人与人之间可能并不是紧挨着排队,我们之间会前后错落,这就导致我们在物理空间上不是连续的,不是线性的。但是,我们在逻辑上是线性的,因为我们都是前后交错的,不能乱插队。如图所示:
线性表是这些在逻辑上成线性结构的数据结构的统称,就像苹果,西瓜,香蕉……这些都统称为水果,线性表也是这个意思。其它线性数据结构,我会不断的给大家分享的,继续往后学习,你就会体会到这种线性存储数据的巧妙之处。
顺序表
- 概念:在逻辑和物理上都成线性关系的数据结构。它是以数组为底层来进行数据存储的,所以在物理上体现线性。 如图所示:
读到这里,可能就有人有疑问了——既然顺序表是用数组来实现的,那么两者的区别是什么? 在讲这个问题之前,我先给大家看个图:
我来解释一下这个图的意思:炒西兰花和绿野仙踪的本质都是西兰花,但是,对西兰花进行装饰和好看的摆盘,就成了绿野仙踪(价格也漂亮!),就是对西兰花进行了漂亮的封装。一个普通的数组可以进行数据的存储和数据的访问,功能很少。但是,经过对数组进行封装,我们在它原来的功能基础上增加了删除数据,增加数据和自动增加数组容量,就摇身一变成了顺序表。 - 结构:顺序表的基本功能要对数据进行存储,所以我们自然少不了数组形式的存储。我们还要知道这个顺序表的有效数据个数(存储了多少个数据)。对于动态顺序表,我们还要知道顺序表的容量。所以,针对于以上的特征,我们要用到结构体。结构体知识忘的同学可以回顾:点击:《结构体》。
- 分类:分为静态顺序表和动态顺序表。
- 静态顺序表:以定长数组为原理进行存储。 结构如图所示:
静态顺序表有个致命的缺点——容量大小是固定的。我们静态顺序表里面的数组大小是固定的,当我们要存储的数据量较多时,就会出现空间不够用,当我们存储的数据量很少时,就会出现空间的大量浪费。为了避免空间的不足或浪费,我们每次都要调整数组的容量,这就很不方便,代码执行效率太低了。所以,我们一般不用静态顺序表。我们最常用的是动态顺序表——因为它能自动改变数组的容量。 - 动态顺序表:用动态内存函数
realloc
进行空间的申请。结构如图所示:
动态顺序表相对于静态顺序表,里面多了一些东西。因为我们是在原来空间基础上扩容的,所以要用realloc
,又因为我们要用动态内存函数realloc
进行空间的开辟,所以我们要用SLDatatype * arr
;来接受malloc
值,因为存储什么类型的数据,我们不知道,所以要用typedef
宏定义,我们根据数据类型直接改宏定义,就能改变全局数据类型。capacity
就是用来确定我们要开辟的空间大小。我们后面展示的代码都是以动态数组为根据的。
- 静态顺序表:以定长数组为原理进行存储。 结构如图所示:
- 动态顺序表的实现:我们要建立三个文件来实现动态顺序表。
1. 建个Sqlist.h文件:用于函数和结构体的声明。
2. 建个Sqlist.c文件:用于实现各个功能函数。
3. 建个 test.c 文件:用来测试顺序表的功能。
进行图片展示:
接下来,我直接给大家展示顺序表的各个功能代码的实现。
- Sqlist.h
#pragma once //头文件的定义格式
#include <stdio.h> //我们直接把需要的头文件都定义在<Sqlist.h>,
#include <stdlib.h> //我们就不用在<test.c>写这些头文件了。
#include <assert.h>
typedef int SLDatatype; //宏定义,便于我们后续修改数据类型。
typedef struct Sqlist
{
SLDatatype* arr; //接收malloc返回的空间地址
int size; //有效数据个数
int capacity; //顺序表的容量
}SL;
void SqlistNit(SL* pp); //顺序表初始化
void my_printf(SL* p); //打印函数
void Sqlistcheck(SL* p); //检查空间够不够
void Sqlistpopback(SL* p); //尾部删数据
void Sqlistpushback(SL* ps, SLDatatype x); //数据尾插
void SqlistpopFront(SL* ps); //数据头删
void SqlistpushFront(SL* ps, SLDatatype x); //数据头插
void SqlistpushSy(SL* p, int da, SLDatatype x);//任意位置插入数据
- Sqlist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Sqlist.h" //引用<Sqlist.h>头文件
void Sqlistcheck(SL*p) //检查空间够不够
{
assert(p); //传递的指针不能为空
if (p->size == p->capacity)
{
int newcapacity = p->capacity == 0 ? 4 : p->capacity * 2; //当容量不够时,就自动扩容原来的2倍。
p->capacity = newcapacity;
SLDatatype* temp = (SLDatatype*)realloc(p->arr, newcapacity * 2 * sizeof(SLDatatype)); //malloc开辟空间,用temp接收,要注意类型的转换。
if (temp != NULL)
p->arr = temp; //判断空间开辟是否成功,开辟成功就赋值
else
return 1; //开辟失败就直接返回
}
}
void Sqlistpushback(SL*ps,SLDatatype x) //数据尾插
{
assert(ps); //判断传递的是否为空指针
Sqlistcheck(ps); //检查空间够不够
ps->arr[ps->size++] = x;
}
void SqlistpushFront(SL*ps,SLDatatype x) //数据头插
{
assert(ps); //判断传递的是否为空指针
Sqlistcheck(ps); //检查空间够不够
for (int i = ps->size;i>0; i--)
{
ps->arr[i] = ps->arr[i-1];
}
ps->arr[0] = x;
ps->size++; //因为我们插入了数据,所以有效数据个数就要增加。
}
void SqlistpopFront(SL*ps) //数据头删
{
assert(ps&&ps->size); //有效数据个数不能为空,要不然就不用删了
for (int i = 0; i + 1 < ps->size; i++)
{
ps->arr[i] = ps->arr[i+1];
}
ps->size--; //因为删除了数据,所以有效数据个数就减少
}
void SqlistNit(SL*pp) //初始化
{
pp->arr = NULL; //防止野指针的发生
pp->size = pp->capacity = 0;
}
void my_printf(SL* p) //打印数据
{
int i = 0;
for (i = 0; i < p->size; i++)
{
printf("%d ",p->arr[i]);
}
printf("\n");
}
void Sqlistpopback(SL* p) //尾部删数据
{
assert(p); //判断传递的是否为空指针
--p->size; //因为删除了数据,所以有效数据个数就减少
}
void SqlistpushSy(SL* p,int da,SLDatatype x) //任意位置插入数据
{
assert(p); //判断传递的是否为空指针
Sqlistcheck(p); //检查空间是否足够
for (int i = p->size;i-1>=da; i--)
{
p->arr[i] = p->arr[i-1];
}
p->arr[da] = x;
p ->size++; //插入数据,有效数据个数就增加
}
我们对于容量的扩容,一般采用2
倍扩容。前提是整数倍,因为内存空间没有小数这一说,当小于2
倍时,扩容量较小,数据可能存放不下,当大于2
倍时,扩容量较大,空间浪费。所以,我们一般采用2
倍扩容,刚好在起始位置上增加1倍空间。
- test.c :用来测试顺序表的各个功能函数。
#define _CRT_SECURE_NO_WARNINGS 1
#include "Sqlist.h"
// 可以根据自己的需求来自行测试顺序表的各个函数的功能
void test()
{
SL s1;
SqlistNit(&s1); //给大家举个顺序表插入数据的例子
Sqlistpushback(&s1,2);
Sqlistpushback(&s1,2);
Sqlistpushback(&s1,2);
Sqlistpushback(&s1,2);
my_printf(&s1);
}
int main()
{
test();
return 0;
}
结果运行图:
- 总结:因为动态顺序表的空间和数组一样,所以我们就可以可以通过
++
或- -
来对空间进行访问。我们要注意传递的指针是否为空指针,当删除或插入数据时,要注意有效数据个数的变化。 - 对于动态内存开辟不懂得同学可以查看:点击:《动态内存管理》。
- 对于指针(一级和二级指针)不懂得同学可以查看:点击:《深入理解指针1》.
顺序表问题与思考
- 中间/头部的插入删除,时间复杂度为O(n).
- 增容需要申请新空间,拷贝数据,释放就空间,会有不小的消耗,执行效率不是很好。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再插入5个数据后,那么就会有95个空间浪费了。思考一下:我们该怎样解决以上的问题呢?预告:下期《链表》就会解决的。
彩蛋时刻!!!
给大家听个欢快的曲子,给大家放松放松一下:机器人之梦:《September》
每章一句:人生漫长,祝我们都可爱的不像话!
感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!