一、线性表概念
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
1.顺序表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组来存储。
1.1静态顺序表:使用定长数组存储元素
// 静态的顺序表
#define N 10000
typedef int SLDatatype;
struct SeqList
{
SLDatatype a[N];
int size;
};
用define来定义一个常数N为10000
在这里,为什么要用typedef来重命名int类型呢?
因为在后续如果我们要改用char类型、double类型等等,只需在typedef此处改就行了,不用在后续代码一个一个的改。
但是静态顺序表有一个缺点就是内存给小了就不够用 给多了就浪费了
为了解决这个问题,就引出一种新的顺序表——动态顺序表
1.2动态顺序表:使用动态开辟的数组存储
// 动态顺序表
//typedef double SLDatatype;
typedef int SLDatatype;
//通过typedef来重新定义类型是为了方便以后如果要修改类型,不用一个一个修改,直接在typedef内修改就行了
typedef struct SeqList
{
SLDatatype* a;
int size; // 存储的有效数据的个数
int capacity; // 容量
}SL;
定义一个SLDatatype(本节内容以int类型为例,接下来都用int来表示)类型的指针用来开辟动态内存,用size来记录已经存储的有效数据的个数,用capacity用来记录改顺序表的容量
三、接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。所以基本都是使用动态开辟空间。
什么是接口函数呢?
接口函数就是在数据进行操作时进行调用的函数,通过调用数据结构的接口帮助你完成一系列操作。
基本的增删查改的接口
//顺序表初始化
void SLInit(SL* psl);
//顺序表销毁
void SLDestroy(SL* psl);
//顺序表打印
void SLPrint(SL* psl);
//STL命名风格
//顺序表尾插
void SLPushBack(SL* psl, SLDatatype x);
//顺序表头插
void SLPushFront(SL* psl, SLDatatype x);
//顺序表尾删
void SLPopBack(SL* psl);
//顺序表头删
void SLPopFront(SL* psl);
//在第pos位之前插入新的数据
void SLInsert(SL* psl, int pos, SLDatatype x);
//删除第pos位的数据
void SLErase(SL* psl, int pos);
//顺序表查找
// 找到返回下标,没有找到返回-1
int SLFind(SL* psl, SLDatatype x);
//顺序表修改
void SLModify(SL* psl, int pos, SLDatatype x);
3.1创建
为了养成良好的习惯,我们尽量把函数的声明和定义以及main函数分开写。
顺序表的头文件一般用“SeqList.h”来命名文件,源文件一般用“SeqList.c”来命名文件。
在SeqList.h中写入顺序表要声明的函数
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 静态的顺序表
// 给小了不够用,给多了浪费
//#define N 10000
//typedef int SLDatatype;
//struct SeqList
//{
// SLDatatype a[N];
// int size;
//};
// 动态顺序表
//typedef double SLDatatype;
typedef int SLDatatype;
//通过typedef来重新定义类型是为了方便以后如果要修改类型,不用一个一个修改,直接在typedef内修改就行了
typedef struct SeqList
{
SLDatatype* a;
int size; // 存储的有效数据的个数
int capacity; // 容量
}SL;
//顺序表初始化
void SLInit(SL* psl);
//顺序表销毁
void SLDestroy(SL* psl);
//顺序表打印
void SLPrint(SL* psl);
//STL命名风格
//顺序表尾插
void SLPushBack(SL* psl, SLDatatype x);
//顺序表头插
void SLPushFront(SL* psl, SLDatatype x);
//顺序表尾删
void SLPopBack(SL* psl);
//顺序表头删
void SLPopFront(SL* psl);
//在第pos位之前插入新的数据
void SLInsert(SL* psl, int pos, SLDatatype x);
//删除第pos位的数据
void SLErase(SL* psl, int pos);
//顺序表查找
// 找到返回下标,没有找到返回-1
int SLFind(SL* psl, SLDatatype x);
//顺序表修改
void SLModify(SL* psl, int pos, SLDatatype x);
3.2 初始化
首先引入我们自己创建的头文件 #include "SeqList.h" ,我们就可以开始动手实现顺序表初始化函数了。
首先通过 psl 指向 a,使用malloc函数动态开辟一段内存,因为开辟了4个int类型的字节空间,使用让capacity容量为4,size有效个数为0。
//顺序表初始化
void SLInit(SL* psl)
{
psl->a = (SLDatatype*)malloc(sizeof(SLDatatype)*4);
if (psl->a == NULL)
{
perror("malloc fail");
return;
}
psl->capacity = 4;
psl->size = 0;
}
3.3顺序表销毁
因为是使用动态开辟的空间,所以在使用结束后要将动态开辟的空间释放销毁掉。
//顺序表销毁
void SLDestroy(SL* psl)
{
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
3.4 顺序表打印
实现函数后,我们如果想要打印到屏幕上,需要实现打印函数,这样我们每次实现一个功能,测试时,只需调用这个函数就可以了
//顺序表打印
void SLPrint(SL* psl)
{
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
3.5判断内存,进行增容
我们刚开始初始化是开辟了4个int类型的空间,当空间不够用了的话,我们就使用realloc函数来进行扩容,每次扩容就只扩大一倍减少空间的浪费。
//判断内存,进行增容
void SLCheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)//当有效个数与容量相同时,就说明空间不够用了,就需要扩容了
{
SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
psl->a = tmp;
psl->capacity *= 2;
}
}
3.6顺序表尾部插入
尾插就是在最后一个元素的后面再插入一个新的元素
尾插有两种情况
第一种是顺序表里没有元素
第二种是顺序表里用元素
//顺序表尾部插入
void SLPushBack(SL* psl, SLDatatype x)
{
SLCheckCapacity(psl);//先判断内存是否够用
psl->a[psl->size++] = x;
//psl->a[psl->size] = x;
//psl->size++;
}
3.7顺序表头部插入
顺序表要求数据是连续存储的,且必须是从头开始存储。所以,对于顺序表而言如果要实现头插,就需要把数据往后挪动。不能从前往后挪,如果从前往后挪就挪就会把后面的数据覆盖掉。
思路:首先创建一个 end 变量用来指向要移动的数据,因为指向的是数据的下标,所以是 size 要减 1 。随后进入 while 循环,如果 end >= 0 说明还没有移动完,就会进入循环。循环体内利用下标,进行向后移动操作,移动结束后再 end-- ,进行下一个数据的向后移动。挪动数据成功后,就可以插入了。此时顺序表第一个位置就被腾出来了,就可以在下标0位置插入欲插入的数据 x 了。最后记得 size++ 。
//顺序表头部插入
void SLPushFront(SL* psl, SLDatatype x)
{
SLCheckCapacity(psl);
//从后往前挪动数据 也可以用memove函数
int end=psl->size-1;
while(end>=0)
{
psl->a[end+1]=psl->a[end];
--end;
}
psl->a[0]=x;
psl->size++;
}
3.8顺序表尾删
即删除最后一个元素,大部分人所想也许是把最后一个元素置为0或者-1,这是可行的,但如果最后一个数就是0呢?
其实我们这里只需要元素数量减去一个就好了,即size--,这样我们就无法访问最后一个元素了,它便是无效的数据。
这里有可能会出现如果内存中一个元素都没有了,size有可能减到-1的位置上,这便是越界了,但size是我们用来统计元素数量的,不可能小于0的,所以这里需要判断是否等于0,也可以用assert断言一下
//顺序表尾删
void SLPopBack(SL* psl)
{
//当size<0,就结束尾删
if(psl->size==0)
return;
//assert(psl->size>0);
psl->size--;
}
3.9顺序表头删
思路和头插类似,依次向前挪动,然后数量-1即size--
如果头删多次调用,如何内存中已经没有元素了,size依然在减。所以这里依然会出现越界,所以需要断言。
//顺序表头删
void SLPopFront(SL* psl)
{
//暴力检查
assert(psl->size>0);
int start=0;
while (start < psl->size-1) {
psl->a[start] = psl->a[start + 1];
start++;
}
psl->size--;
//int start = 1;
//while (start < psl->size)
//{
// psl->a[start-1] = psl->a[start];
// start++;
//}
}
3.10插入任意位置
顺序表要求数据从第一个开始放并且数据是连续存储的,所以我们就要注意下指定的下标位置 pos 的位置了!有些位置并不可以,所以我们需要进行一些检查。
代码思路:
首先添加 pos 位置的限定条件,限定 pos >= 0 并且 pos <= psl->size 从而保证 pos 合法。然后,因为是插入所以免不了要检查增容,直接调用之前写好的检查增容的函数即可。检查完后就可以开始移动了,和头插差不多,我们创建一个变量 end 记录最后一个的下标(psl->size-1),并通过它来指向要移动的数据。最后进入 while 循环,以 end >= pos 作为条件。移动完后,x 的位置就腾出来了,再把 x 插入进去,最后再 size++,就完成了。
//中间插入
void SLInsert(SL* psl, int pos, SLDatatype x)
{
assert(psl);
assert(0 <= pos && pos<= psl->size);
SLCheckCapacity(psl);
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[pos] = x;
psl->size++;
}
中间插入可以代替头插,尾插
3.11删除任意位置
删除指定位置的数据,我们仍然要限制 pos 的位置。限制条件部分和 SeqListInsert 不同的是,因为 psl->size 这个位置没有效数据,所以删除的位置不能是 psl->size!
//删除pos位的值
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(0 <= pos && pos < psl->size);
int start = pos + 1;
while (start < psl->size)
{
psl->a[start - 1] = psl->a[start];
++start;
}
psl->size--;
}
删除任意位置可以用于代替头删,尾删
3.12顺序表查找
/查找
int SLFind(SL* psl, SLDatatype x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
3.13顺序表修改
//修改
void SLModify(SL* psl, int pos, SLDatatype x)
{
assert(psl);
assert(0 <= pos && pos < psl->size);
psl->a[pos] = x;
}
四、顺序表源文件完整代码
#include"SeqList.h"
//void SLInit(SL* psl)
//{
// psl->a = NULL;
// psl->capacity = 0;
// psl->size = 0;
//}
//typedef int SLDatatype;
//通过typedef来重新定义类型是为了方便以后如果要修改类型,不用一个一个修改,直接在typedef内修改就行了
//typedef struct SeqList
//{
// SLDatatype* a;
// int size; // 存储的有效数据的个数
// int capacity; // 容量
//}SL;
//基本的增删查改接口
//顺序表初始化
void SLInit(SL* psl)
{
psl->a = (SLDatatype*)malloc(sizeof(SLDatatype)*4);
if (psl->a == NULL)
{
perror("malloc fail");
return;
}
psl->capacity = 4;
psl->size = 0;
}
//顺序表销毁
void SLDestroy(SL* psl)
{
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
//顺序表打印
void SLPrint(SL* psl)
{
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
//判断内存,进行增容
void SLCheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
psl->a = tmp;
psl->capacity *= 2;
}
}
//顺序表尾部插入
void SLPushBack(SL* psl, SLDatatype x)
{
//psl->a[psl->size] = x;
//psl->size++;
SLCheckCapacity(psl);
psl->a[psl->size++] = x;
}
//顺序表头部插入
void SLPushFront(SL* psl, SLDatatype x)
{
SLCheckCapacity(psl);
//从后往前挪动数据 也可以用memove函数
int end=psl->size-1;
while(end>=0)
{
psl->a[end+1]=psl->a[end];
--end;
}
psl->a[0]=x;
psl->size++;
}
//中间插入
void SLInsert(SL* psl, int pos, SLDatatype x)
{
assert(psl);
//assert(0 <= pos <= psl->size);
assert(0 <= pos && pos<= psl->size);
SLCheckCapacity(psl);
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[pos] = x;
psl->size++;
}
//删除pos位的值
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(0 <= pos && pos < psl->size);
//assert(psl->size > 0);
int start = pos + 1;
while (start < psl->size)
{
psl->a[start - 1] = psl->a[start];
++start;
}
psl->size--;
}
//顺序表尾删
void SLPopBack(SL* psl)
{
//当size<0,就结束尾删
if(psl->size==0)
return;
//assert(psl->size>0);
psl->size--;
}
//顺序表头删
void SLPopFront(SL* psl)
{
//暴力检查
assert(psl->size>0);
int start=0;
while (start < psl->size-1) {
psl->a[start] = psl->a[start + 1];
start++;
}
psl->size--;
//int start = 1;
//while (start < psl->size)
//{
// psl->a[start-1] = psl->a[start];
// start++;
//}
}
//查找
int SLFind(SL* psl, SLDatatype x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
//修改
void SLModify(SL* psl, int pos, SLDatatype x)
{
assert(psl);
assert(0 <= pos && pos < psl->size);
psl->a[pos] = x;
}
五、顺序表的测试
#include "SeqList.h"
void TestSeqList1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPushBack(&s, 7);
SLPushBack(&s, 8);
SLPushBack(&s, 9);
SLPrint(&s);
SLDestroy(&s);
}
void TestSeqList2()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushFront(&s, 6);
SLPushFront(&s, 7);
SLPushFront(&s, 8);
SLPushFront(&s, 9);
SLPrint(&s);
SLDestroy(&s);
}
void TestSeqList3()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPopBack(&s);
SLPushBack(&s, 5);
SLPushFront(&s, 6);
SLPushFront(&s, 7);
SLPushFront(&s, 8);
SLPushFront(&s, 9);
SLPrint(&s);
SLDestroy(&s);
}
void menu()
{
printf("***************************************\n");
printf("1、尾插数据 2、尾删数据\n");
printf("3、头插数据 4、头删数据\n");
printf("5、打印数据 -1、退出\n");
printf("***************************************\n");
}
int main()
{
int option = 0;
SL s;
SLInit(&s);
while (option != -1)
{
menu();
printf("请输入你的操作:>");
scanf("%d", &option);
if (option == 1)
{
/*printf("请输入要尾插的数据,以-1结束:");
int x = 0;
scanf("%d", &x);
while (x != -1)
{
SLPushBack(&s, x);
scanf("%d", &x);
}*/
int n = 0;
printf("请输入要尾插的数据个数,再依次输入要插入的数据:");
scanf("%d", &n);
int x = 0;
while (n > 0)
{
scanf("%d", &x);
SLPushBack(&s, x);
n--;
}
}
else if (option == 5)
{
SLPrint(&s);
}
else if (option == -1)
{
break;
}
else
{
printf("无此选项,请重新输入\n");
}
}
SLDestroy(&s);
return 0;
}
大家可以根据自己的想法来进行测试!
以上就是顺序表的基本知识,谢谢大家的耐心阅读!
感谢!