线性表——顺序表
1. 线性表
线性表:0 个或多个数据元素的有限序列。(这些数据元素具有相同特性) 如果还不太理解,那就这样来说把,线性表,从字面上来理解就是 具有线一般特性的表 ,再把一条线分为好多段,每一段就是一个数据元素,注意这里的线是一个抽象概念。不难发现,线性表中的数据元素都是一对一的关系。我们常说的顺序表和链表也都属于线性表。
若将线性表中的数据元素记为 L1,L2,L3,L4,······,Ln-2,Ln-1,Ln ; 我们称 L2 是 L3 的直接前驱元素,而 L4 是 L3 的直接后继元素,也对数据元素 Li 的下标定义,称 i 是 Li 在线性表中的位序,以此类推······ 在一个表中,第一个数据元素只有直接后继,最后一个数据元素只有直接前驱,当然表中也可以没有数据元素,这种表称之为空表。
前面说到数据元素具有相同的特性,数据元素是一个庞大的概念,是不是一个数据元素只能存在一个数据项呢?这样的话其实很不方便,所以在有些复杂的顺序表中,一个数据元素可以是多个数据项组合而成的。就比如学校的系统中存着学生信息,学生信息并不只有一个姓名,而是由学号、姓名、年龄、联系电话等数据组合,最后再按特定的方法有序存放。
线性表是一种逻辑结构,它有两种物理结构:顺序存储结构、链式存储结构。
2. 顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元 依次存储线性表的数据元素。由于数据元素的属性(类型)都相同,顺序表的实现离不开数组。使用顺序存储结构的线性表简称为 顺序表 。
- 线性表地址的计算:
顺序表要使用一段地址连续的存储单元进行存放数据,要想计算数据元素具体的地址(前提是首个数据元素位置确定),下面就有一个公式计算第 i 个数据元素 Li 的存储位置:
LOC( Li ) = LOC( L1 ) + ( i - 1 ) * c
说明:c 表示元素占用存储单元的长度
- 顺序表的两种分配方式:
- 静态顺序表:使用静态分配,在定义数组时,直接使用定长数组(给定数组一个确定的大小)。
- 动态顺序表:使用动态分配,数组大小可根据需要改变,一般使用malloc、realloc 等函数进行操作。(这种分配方式用得最多)
若不熟悉 malloc、realloc 可参考:动态内存管理
3. 顺序表的实现
3.1 顺序表的定义
我们先进行一些基础操作(这里我们对动态顺序表进行实现)
typedef int SLDataType;
typedef struct Sqlist
{
SLDataType x; //插入元素,在顺序表的插入中会用到
int* a; //动态数组指针,暂未给定大小
int size; //有效数据
int length; //空间大小
}SL;
图解:
3.2 顺序表的初始化
由于需要对部分数据进行修改,传参时只能传地址,所以用的是传址调用。
void SLInit(SL* psl)
{
assert(psl); //判断psl是否为空
psl->a = NULL;
psl->size = 0;
psl->length = 0;
}
assert 断言可参考:assert
3.3 顺序表的销毁
void SLDestory(SL* psl)
{
assert(psl); //判断psl是否为空
if (psl->a != NULL)
{
free(psl->a); //释放动态开辟的内存空间
psl->a = NULL; //释放后指针及时置空
psl->size = 0;
psl->length = 0;
}
}
3.4 顺序表的插入
插入数据的操作就是对数组的修改,存入数据,既然是存放数据,就要考虑内存空间够不够用,就需要先对数组的空间大小进行判断,这里我们可以将判断部分进行封装。
既然顺序表要插入数据,那就要给插入的数据留一个空位进行存放,所以插入的操作就是要对顺序表中的数据进行部分移动。
3.4.1 检查存储空间
void SLChecklength(SL* psl)
{
assert(psl); //判断psl是否为空
if (psl->size >= psl->length) //是否空间不够
{
int newlength = psl->length == 0 ? 4 : psl->length * 2; //判断length是否为零
//上一行使用三目操作符,若length为零,则给newlength 4或8的空间大小;不为零newlength则扩大为原来的2倍(2倍不是固定的值,而是最能接受的一个值,可自行更改,)
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newlength);
//对tmp进行动态内存开辟,扩容操作
if (tmp == NULL)
{
perror("realloc fail"); //扩容失败
return;
}
psl->a = tmp;
psl->length = newlength;
}
}
3.4.2 头部插入(头插)
头插需要将整个数组的数据元素向后移动,再插入数据,才能保证数据正确。
void SLPushFront(SL* psl, SLDataType x) //头插
{
assert(psl);
SLChecklength(psl); //保证有空间可插入
int end = psl->size - 1;
while (end >= 0) //数据整体向后移动(覆盖)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = x; //插入数据x
psl->size++; //有效数据个数+1
}
图解:
3.4.3 尾部插入(尾插)
尾插就不用移动数据,直接在末端插入即可。
void SLPushBack(SL* psl, SLDataType x) //尾插
{
assert(psl);
SLChecklength(psl); //保证空间足够
psl->a[psl->size] = x;
psl->size++;
}
3.4.4 指定位置插入
void SLInsert(SL* psl, int pos, SLDataType x) //pos表示下标,x表示要插入的数据
{
assert(psl);
assert(pos >= 0 && pos < psl->size); //判断pos的取值在范围内
SLChecklength(psl); //保证空间足够
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end]; //向后移动数据操作(覆盖)
--end;
}
psl->a[pos] = x; //在指定位置插入
psl->size++; //有效数据长度 + 1
}
3.5 顺序表的删除
3.5.1 头部删除(头删)
void SLPopFront(SL* psl) //头删
{
assert(psl);
//判断psl->size(有效数据)是否为空
//方法1:
//if (psl->size == 0)
//{
// return;
//}
//方法2:
assert(psl->size > 0); //为假报错(断言)
int begin = 0;
while (begin < psl->size)
{
psl->a[begin] = psl->a[begin + 1]; //数据整体向前覆盖
++begin;
}
psl->size--;
}
图解:
3.5.2 尾部删除(尾删)
void SLPopBack(SL* psl) //尾删
{
assert(psl);
//判断psl->size(有效数据)是否为空
//方法1:
//if (psl->size == 0)
//{
// return;
//}
//方法2:
assert(psl->size > 0); //为假报错(断言)
psl->size--; //原数组最后一个有效数据变为无效数据,就不用覆盖再删除
}
3.5.3 指定位置删除
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size); //判断pos的取值在范围内
int begin = pos;
while (begin < psl->size)
{
psl->a[begin] = psl->a[begin + 1]; //数据整体向前覆盖
++begin;
}
psl->size--; //删除后有效数据长度 - 1
}
3.6 顺序表的查找
void SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
printf("%d的下标是%d\n", x, i);
}
}
return -1;
}
3.7 顺序表的打印
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
以上操作可再通过接口实现
4. 功能综合
顺序表功能综合实现如下,另外加入了菜单功能,帮助更快对部分功能进行熟悉。(可自行加入代码增加鲁棒性)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
typedef struct Sqlist
{
SLDataType x; //插入元素,在顺序表的插入中会用到
int* a; //动态数组指针,暂未给定大小
int size; //有效数据
int length; //空间大小
}SL;
void SLInit(SL* psl)
{
assert(psl); //判断psl是否为空
psl->a = NULL;
psl->size = 0;
psl->length = 0;
}
void SLDestory(SL* psl)
{
assert(psl); //判断psl是否为空
if (psl->a != NULL)
{
free(psl->a); //释放动态开辟的内存空间
psl->a = NULL; //释放后指针及时置空
psl->size = 0;
psl->length = 0;
}
}
void SLChecklength(SL* psl)
{
assert(psl); //判断psl是否为空
if (psl->size >= psl->length) //是否空间不够
{
int newlength = psl->length == 0 ? 4 : psl->length * 2; //判断length是否为零
//上一行使用三目操作符,若length为零,则给newlength 4或8的空间大小;不为零newlength则扩大为原来的2倍(2倍不是固定的值,而是最能接受的一个值,可自行更改,)
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newlength);
//对tmp进行动态内存开辟,扩容操作
if (tmp == NULL)
{
perror("realloc fail"); //扩容失败
return;
}
psl->a = tmp;
psl->length = newlength;
}
}
void SLPushFront(SL* psl, SLDataType x) //头插
{
assert(psl);
SLChecklength(psl); //保证有空间可插入
int end = psl->size - 1;
while (end >= 0) //数据整体向后移动(覆盖)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = x; //插入数据x
psl->size++; //有效数据个数+1
}
void SLPushBack(SL* psl, SLDataType x) //尾插
{
assert(psl);
SLChecklength(psl); //保证空间足够
psl->a[psl->size] = x;
psl->size++;
}
void SLInsert(SL* psl, int pos, SLDataType x) //pos表示下标,x表示要插入的数据
{
assert(psl);
assert(pos >= 0 && pos < psl->size); //判断pos的取值在范围内
SLChecklength(psl); //保证空间足够
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end]; //向后移动数据操作(覆盖)
--end;
}
psl->a[pos] = x; //在指定位置插入
psl->size++; //有效数据长度 + 1
}
void SLPopFront(SL* psl) //头删
{
assert(psl);
//判断psl->size(有效数据)是否为空
//方法1:
//if (psl->size == 0)
//{
// return;
//}
//方法2:
assert(psl->size > 0); //为假报错(断言)
int begin = 0;
while (begin < psl->size)
{
psl->a[begin] = psl->a[begin + 1]; //数据整体向前覆盖
++begin;
}
psl->size--;
}
void SLPopBack(SL* psl) //尾删
{
assert(psl);
//判断psl->size(有效数据)是否为空
//方法1:
//if (psl->size == 0)
//{
// return;
//}
//方法2:
assert(psl->size > 0); //为假报错(断言)
psl->size--; //原数组最后一个有效数据变为无效数据,就不用覆盖再删除
}
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size); //判断pos的取值在范围内
int begin = pos;
while (begin < psl->size)
{
psl->a[begin] = psl->a[begin + 1]; //数据整体向前覆盖
++begin;
}
psl->size--; //删除后有效数据长度 - 1
}
void SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
printf("%d的下标是%d\n", x, i);
}
}
return -1;
}
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
void menu()
{
printf("================================\n");
printf("1.尾插 2.尾删\n");
printf("3.头插 4.头删\n");
printf("5.打印 0.退出\n");
printf("================================\n");
}
int main()
{
SL s;
SLInit(&s);
int choose = 0;
do //这里可以用if 替换do...while、switch...case
{
menu();
printf("请输入选项:");
scanf("%d", &choose);
switch (choose)
{
case 1:
printf("请输入要插入的数据个数和数据:");
int a = 0;
scanf("%d", &a);
for (int i = 0; i < a; i++)
{
int x = 0;
scanf("%d", &x);
SLPushBack(&s, x);
}
break;
case 2:
SLPopBack(&s);
printf("操作已完成\n");
break;
case 3:
printf("请输入要插入的数据个数和数据:");
int b = 0;
scanf("%d", &b);
for (int i = 0; i < b; i++)
{
int x = 0;
scanf("%d", &x);
SLPushBack(&s, x);
}
break;
case 4:
SLPopFront(&s);
printf("操作已完成\n");
break;
case 5:
SLPrint(&s);
break;
case 0:
printf("退出.......");
break;
default:
printf("请输入正确的选项......");
break;
}
} while (choose);
SLDestory(&s);
return 0;
}
5. 顺序表的优缺点
优点:
- 顺序表的物理空间是连续的,不用为数据间的逻辑关系增加额外存储空间。
- 可以通过下标进行访问(访问方便)。
缺点:
- 尾部插入数据效率高,头部和中间插入删除,需要移动数据,效率较低。
- 当空间不够时会进行扩容,而扩容会有一定消耗,也存在一定的空间浪费(扩多了浪费,少了不够,就需要频繁扩容)。
6. 链表
传送门:链表(点击传送)