每日壁纸分享(壁纸出处:极简壁纸_海量电脑桌面壁纸美图_4K超高清_最潮壁纸网站)
前言
在学习过语法之后若是想要继续对该语言深入学习,那就要开始着手数据结构与算法方面的学习啦。在本篇文章中,作者将带大家实现C语言中最简单的一个数据结构:顺序表
目录
顺序表
在开始顺序表之前,按照惯例,我先简单介绍一下什么是数据结构与为什么要有这么多数据结构的原因。
数据结构的概念
1,数据结构简介:
数据结构是指计算机存储、组织数据的方式。数据结构是指相互之间存在⼀种或多种特定关系的数据元素的集合。数据结构反映数据的内部构成,即数据由那部分构成,以什么⽅式构成,以及数据元素之间呈现的结构。
数据结构具备两个特点:
(1)可以存储数据
(2)储存的数据方便查找
2,为什么需要数据结构:
程序中如果不对数据进⾏管理,可能会导致数据丢失、操作数据困难、野指针等情况。通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏增删改查等操作。
其实数组就是一个最简单的数据结构,但是为什么有了数组这类的数据结构,还要其他的数据结构呢?
主要的原因是数组不满足于复杂场景数据的管理,弊端如下:
(1)数组仅能存储同类型的数据
(2)数组可提供的接口不足以支撑复杂场景的数据处理
顺序表的概念
在讲顺序表之前,我不得不先提一下线性表,因为顺序表就是线性表的一种。如果说线性表是水果,那么顺序表就是香蕉。
1,线性表的概念
线性表指的是具有相同特性的一类数据结构的统称,何为相同特性呢?如下:
在逻辑结构上一定是线性的,在物理结构上不一定是线性的
很简单,我们可以将其理解为在逻辑上,线性表数据结构是像一条线一样头尾相连,环环相扣;但是在内存存放上,可能是分开存放。
常⻅的线性表有:顺序表、链表、栈、队列、字符串..
2,顺序表:
那么,通过前文,我们已经能够理解顺序表是线性表的一种,接下来就开始引入正题。
顺序表的概念:
顺序表是线性表的一种,并且因为顺序表的底层结构是数组,所以顺序表在逻辑结构上是线性的,在物理结构上也是线性的。 顺序表就是对数组进行封装,并实现了常用的增删改查等接口。
而顺序表也分为两种,一种是静态顺序表,一种是动态顺序表。
(1)静态顺序表是由定长数组实现的,其弊端较为明显:内存空间固定,给多了容易浪费,给少了又不够用。基于以上的原因,静态顺序表的适用场景较小,故本文主要讲的是动态顺序表的实现。
(2)动态顺序表即是动态开辟空间,可以对空间进行调节,避免出现浪费或者空间不够用的情况,同时在其动态内存的基础上增加了增删改查等接口,实现了对数据初步的管理。
动态顺序表的实现
在正式开始实现之前,我需要对本次代码实现的方式进行说明,以便读者能够更好地阅读本文。
本次顺序表的实现将采用声明、引用分开的方式,如下图:
我们可以将头文件设想为一个目录,里面记录了我们需要实现的功能,这样就可以在实现之前将需要实现的功能先写在头文件中,再一一实现,这样有助于我们去思考与编写代码。
(1)头文件
在头文件中,除了包含需要的头文件之外,剩下的就是函数声明及结构申明了,我将以逻辑思维的顺序往后写。
1)) 顺序表的结构,如下:
2)):增删改查功能,如下:
当我们完成顺序表的基础结构后,接下来就要着手考虑需要实现哪些接口,先写出声明,再依照申明一一实现。
到现在为止,顺序表的头文件我们便已经写好了,接下来的工作便是在源文件中照猫画虎,再根据实际碰见的问题增加其他函数。
头文件源码:
此头文件不代表最终的头文件,在后续代码中若是新增函数也会在该头文件中声明。
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
size_t size; //顺序表中有效元素个数
size_t capacity;//顺序表当前内存空间大小
}SL;
//顺序表初始化
void SLInit(SL* ps);
//重置顺序表
void SLReset(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//头插
void SLpushFront(SL* ps, SLDataType x);
//尾删
void SLPopBack(SL* ps);
//头删
void SLPopFront(SL* ps);
//指定位置删除
void SLErase(SL* ps, int pos);
//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
//查找
void SLFind(SL* ps, SLDataType target);
//打印
void SLprint(SL* ps);
(2)源文件
在源文件中,我们就是需要将头文件中所声明的函数一一实现。
在这里我将会对每个函数进行深入讲解其原理。
第一步:一定要先在 SeqList.c(源文件)中包含我们刚才写的头文件!!!
1))初始化、重置
由于初始化与重置函数较为简单,于是我便将这两个函数放在一起进行讲解。
如下:
2))尾插、判空扩容
尾插,顾名思义即是从历史数据的后面开始存放数据,不需要做其他操作。
而只要插入数据就涉及到两种情况:当前空间足够时和当前空间不足时,解决方法如下:
既然要扩容,那么如何扩容?每次扩容多大呢?答:一般来说,每次扩容原空间的1.5倍或者2倍
1,判空扩容函数实现
那么现在目标明确,我们便加上一个扩容函数,如下(声明同步到头文件中):
2,尾插函数实现
在上文中已经实现了判空扩容函数,于是我们在尾插函数中便可以直接调用该函数,函数实现如下:
3))头插
头插即是将元素插入到下表为0的位置,需要先将历史数据全部向后移一位。
如何移动呢?从后向前移!
如图,当使用头插时,需要从后向前依次(不会对没有移动的数据进行覆盖)向后赋值一位以实现历史数据整体后移。当整体后移后,即可将下标为0的位置赋值为x。
代码如下:
4))尾删、头删
在实现之前,需要大家仔细想一下:若是顺序表中没有数据时是否可以删除呢?
答案是不行!所以我们需要增加一个函数,来判定函数是否含有数据。
1,判定是否有值 函数实现
2,尾删函数实现
3,头删函数实现
5))指定位置删除
在实现之前,我还是想请大家思考一个问题:指定位置删除,那么这个指定位置可以是任意的数字吗?
答案是当然不能!所以我们还需要对指定位置的变量进行限制。
再者就是,指定的位置是按照下标的位置还是按照元素排列的位置呢?两者都行,但是我们需要在这里指定一个标准,我在这是按照元素排列的次序为指定位置实现的。
6))指定位置插入
与上文同理,以元素排列次序为基准进行指定。
7))查找,打印
这两个函数实现就较为简单了,查找只需要直接遍历所有元素找出指定元素,打印函数为常见的遍历打印。
(3)函数功能测试
实际上我是在实现一个功能后就会立马对其进行调试,但是在文章中展示调试过程就显得比较冗长,故我将所有函数功能实现完毕之后再对其进行一个总调试,以让大家看到其功能实现。
当然,测试并不止这些,一般需要使用特殊值进行测试,我这里仅仅是对其功能进行简单的使用。
SeqList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
size_t size; //顺序表中有效元素个数
size_t capacity;//顺序表当前内存空间大小
}SL;
//顺序表初始化
void SLInit(SL* ps);
//重置顺序表
void SLReset(SL* ps);
//尾插
void SLPushBack(SL* ps, SLDataType x);
//头插
void SLpushFront(SL* ps, SLDataType x);
//尾删
void SLPopBack(SL* ps);
//头删
void SLPopFront(SL* ps);
//指定位置删除
void SLErase(SL* ps, int pos);
//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
//查找
void SLFind(SL* ps, SLDataType target);
//打印
void SLprint(SL* ps);
//判空扩容
void SLCheckCapacity(SL* ps);
//判定是否有值
bool SLEmpty(SL* ps);
SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->a = NULL;
ps->capacity = ps->size = 0;
}
//重置
void SLReset(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
//当有效元素==空间大小,说明此时空间已经满了,需要扩容
if (ps->capacity == ps->size)
{
//当第一次进入该函数时,函数内存容量为零,需要我们对其进行赋值,我给其赋值4
size_t NewCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
ps->capacity = NewCapacity;
//设置临时指针接收其返回值,并对其返回值进行判断,若是不为空再赋值
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity);
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps->a = tmp;
tmp = NULL;
}
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size++] = x;
}
//头插
void SLpushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
for (int i = (ps->size - 1); i >= 0; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[0] = x;
ps->size++;
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
assert(!SLEmpty(ps));
ps->size--;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(!SLEmpty(ps));
for (int i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//判定是否有值
bool SLEmpty(SL* ps)
{
return ps->size == 0;
}
//指定位置删除
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(!SLEmpty(ps));
assert(pos > 0 && pos <= ps->size);
for (int i = (pos - 1); i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos > 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size - 1; i >= pos - 1; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[pos - 1] = x;
ps->size++;
}
//查找
void SLFind(SL* ps, SLDataType target)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == target)
{
printf("找到了!\n");
return;
}
}
printf("没找到!\n");
return;
}
//打印
void SLprint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}