现在小编就要介绍数据结构有关内容了,本节将介绍顺序表的初始化,插入,删除,打印操作思路和代码实现。
文章目录
一、线性表
概念
在介绍顺序表之前,我们先了解一下线性表(linear list):
线性表是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
特性
线性表在逻辑上是线性结构(人为想像出来的),也就说是连续的一条直线。但在物理结构(数据在内存中存储它的地址空间)上不一定连续,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
概念
顺序表是一段物理地址连续的存储单元依次存储数据元素的数据结构,一般情况下采用数组存储。
那么顺序表和数组有上面区别?顺序表底层提供了增删改查数据的方法,而数组想实现这些功能需要自己手动实现,下面这张图可以直观的看出它们的区别:
静态顺序表
typedef int SLDataType;
#define N 7
typedef struct SeqList
{
SLDataType arr[N];
int sz;
}SL;
静态顺序表顾名思义它只能申请固定大小的空间,如上图所示当N定义为7时这个顺序表大小就被定死为7了,所以静态顺序表的缺陷就是空间给少了不够用,给多了浪费。
这里我们还要关注一个点为什么要把顺序表元素的类型int自定义为SLDataType,这里我们试想一个场景,我们把顺序表类型定为int,在这个顺序表里写了上百行代码,我们想改变顺序表类型时,难道要在代码里一个一个修改吗,有的读者会说可以ctrl+f一键替换,但这样我们就可能把一些我们不想改的数据类型也一起改了,所以我们把int提取出来的目的就是为了方面我们后面一键修改类型。
动态顺序表
//动态版本
#define capacity 10
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL;
这里我们可以看到动态顺序表定义数组用了它的指针,这就表示并没有给定大小,需要我们根据需要使用动态内存函数(malloc realloc)动态的申请空间,所以我们定义了两个变量:size(用来记录有效数据个数) capacity(用来记录空间容量)
在大多数场景下我们都会用到动态顺序表来存储数据,只有当我们明确知道需要存储多少数据时才会用到静态顺序表。
三、顺序表的代码实现
分模块实现
这里和扫雷游戏一样需要分三个模块(有读者感兴趣的话请移步小编这篇博文<点此跳转)一个头文件(SeqList.h)用于定义顺序表结构,声明函数,类似于目录的作用,一个实现文件(SeqList.c)用于实现函数功能,最后是测试文件(test.c)用于测试代码逻辑。
下面是我们顺序表要实现的所以功能,看着复杂,其实每一个小点都很简单,让小编为大家一一介绍。
//判空和增容
void SLCheckCapacity(SL* ps);
//打印顺序表
void SLPrint(SL* ps);
//顺序表初始化
void SLInit(SL* ps);
//顺序表销毁
void SLDestroy(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);
//删除pos位置的数据
void SLErace(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLDataType x);
注意事项
1、在这里我们顺序表结构体传参都应该传地址,因为不论是初始化、插入还是删除,都需要改变实参,只传结构体本身形参就是实参的临时拷贝,改变形参不会影响实参。
2、每一个功能函数最开始都需要对传过来数据的进行判断,因为若传过来的为空地址,会对后续操作产生影响,秉持代码写少不写多的原则,用assert断言最方便,删除操作不仅要判断结构体地址是否为空,还要判断size,若size为0则不能删除。
3、realloc的返回值为void*,需要强制类型转换为我们需要的类型,并且要用临时变量来接受realloc的返回值,避免开辟失败返回空指针。realloc后一个参数单位为字节,所以还需要将它乘以sizeof(SLDataType)。
4、增加需要size++,删除需要size–,并且删除要注意删除顺序,避免将数据覆盖。
初始化和销毁
当我们定义一个顺序表后最先做的工作应该是初始化。
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
在使用完顺序表后由于是动态顺序表,内部一定会有动态内存申请,那么销毁(free)操作也是必要的,在free之前先判断该内存空间是否为空,不为空再free,free之后记得把指针置空。
void SLDestory(SL* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
打印和顺序表扩容
由于每一插入操作都需要判断空间大小是否足够,不够需要扩容(扩容我们一般是成倍数扩容,这里小编采用二倍)故我们把该操作封装成函数
void SLCheckCapacity(SL* ps)
{
//判断空间是否为足够
if (ps->capacity == ps->size)
{
//判断capacity是否为空
int NewCapacity = (ps->capacity == 0) ? 4 : 2 * ps->capacity;
//增容
SLDataType* tmp = (SLDataType * )realloc(ps->arr, NewCapacity * sizeof(SLDataType));
//判断realloc返回值是否为空
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
ps->arr = tmp;
ps->capacity = NewCapacity;
}
}
在完成一个功能后,判断是否正确把它打印出来是最直观的,打印顺序表函数如下
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
头插尾插
插入操作分尾插和头插,顾名思义尾插就是在顺序表最后一个元素后面插入,头插在第一个元素之前插入。插入之前需要判断空间是否足够,头插需要将所有元素向后整体后移一位,插入后不要忘记size++。
尾插:
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//判断空间是否为足够
SLCheckCapacity(ps);
//若空间足够
ps->arr[ps->size++] = x;
}
头插:
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
//判断空间是否为足够
SLCheckCapacity(ps);
//让其他元素后往前挪
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//将数据插入头部
ps->arr[0] = x;
ps->size++;
}
在这里我们可以发现头插由于要遍历整个顺序表元素,故它是时间复杂度为O(n),尾插不需要则为O(1)。当我们后面介绍链表会发现这刚好和顺序表相反,在这里小编先卖个关子。
头删尾删
删除操作和插入一样,也分为头删和尾删,删除操作不仅要判断传来的结构体是否为空,也需要判断有效数据个数size,删除操作并不需要给元素赋值,尾插只需要size–,后续如果需要再把它赋值为想要的值就行了,至于头删只需要把除了第一位元素其他元素整体向前挪动一位就行了,自然第一位元素就被覆盖(删除)了,删除后不要忘记size–。
尾删:
void SLPopBack(SL* ps)
{
//ps:判断传递的参数不为空
//ps->size:判断顺序表不为空
assert(ps && ps->size);
ps->size--;
}
头删:
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
指定位置(下标)之前插入和指定位置删除
除了顺序表头部和尾部,顺序表中间任意位置和可以实现插入和删除的操作,原理相同,只是需要多判断一下指定位置(pos)是否合法,即是否在有效数据范围内,当指定位置尾0是就等同于头插和头删,当为size是即为尾插,当为size-1时即为尾删。
插入:
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
//判断pos是否在有效范围
assert(pos >= 0 && pos <= ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos之后数据整体后移一位
for (int i = ps->size-1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size++;
}
删除:
void SLErace(SL* ps, int pos)
{
assert(ps && ps->size);
//判断pos是否在有效范围
assert(pos >= 0 && pos < ps->size);
//将pos之后是数据整体前移一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
查找
查找指定元素功能也能实现,只不过需要在测试模块用一个整型变量来接受查找函数的返回值,并对返回加以判断来确定结果。
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
顺序表源码
seqlist.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//定义动态顺序表结构
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size; //有效数据个数
int capacity; //空间容量
}SL;
//判空和增容
void SLCheckCapacity(SL* ps);
//打印顺序表
void SLPrint(SL* ps);
//顺序表初始化
void SLInit(SL* ps);
//顺序表销毁
void SLDestroy(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);
//删除pos位置的数据
void SLErace(SL* ps, int pos);
//查找
int SLFind(SL* ps, SLDataType x);
seqlist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps)
{
//判断空间是否为足够
if (ps->capacity == ps->size)
{
//判断capacity是否为空
int NewCapacity = (ps->capacity == 0) ? 4 : 2 * ps->capacity;
//增容
SLDataType* tmp = (SLDataType * )realloc(ps->arr, NewCapacity * sizeof(SLDataType));
//判断realloc返回值是否为空
if (tmp == NULL)
{
perror("realloc");
exit(1);
}
ps->arr = tmp;
ps->capacity = NewCapacity;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//判断空间是否为足够
SLCheckCapacity(ps);
//若空间足够
ps->arr[ps->size++] = x;
}
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
//判断空间是否为足够
SLCheckCapacity(ps);
//让其他元素后往前挪
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
//将数据插入头部
ps->arr[0] = x;
ps->size++;
}
void SLPopBack(SL* ps)
{
//ps:判断传递的参数不为空
//ps->size:判断顺序表不为空
assert(ps && ps->size);
ps->size--;
}
void SLPopFront(SL* ps)
{
assert(ps && ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
//判断pos是否在有效范围
assert(pos >= 0 && pos <= ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
//pos之后数据整体后移一位
for (int i = ps->size-1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size++;
}
void SLErace(SL* ps, int pos)
{
assert(ps && ps->size);
//判断pos是否在有效范围
assert(pos >= 0 && pos < ps->size);
//将pos之后是数据整体前移一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;
}
}
return -1;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
test01()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushFront(&sl, 5);
SLPushFront(&sl, 6);
SLPushFront(&sl, 7);
SLPushFront(&sl, 8);
SLPrint(&sl);
int find = SLFind(&sl, 5);
int get = SLFind(&sl, 8);
if (find >= 0)
{
printf("找到了,下标为 % d\n", find);
}
else
{
printf("未找到\n");
}
SLInsert(&sl, find, 100);
SLPrint(&sl);
SLErace(&sl,get);
SLPrint(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPopFront(&sl);
SLPrint(&sl);
SLDestroy(&sl);
}
int main()
{
test01();
return 0;
}
结尾
顺序表的所有功能板块小编就介绍完了,读者想自己动手实践时不要忘了每完成一个模块都要自测一下哦,顺序表我们会发现是有一定缺陷的,头部的插入删除操作的时间复杂度比较高,并且每次都是二倍扩容,势必会造成空间浪费,那有没有一种数据结构能用多少空间就开辟多少空间呢,敬请期待小编介绍的下一种数据结构—链表!
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~