前言:
小编在近日刚开始学顺序表,为了巩固学习,小编先写一篇关于顺序表的文章来加强记忆,写完这一篇我将继续书写C语言相关的文章,那么废话不多说,下面小编将打开数据结构的大门!顺序表来喽!
目录:
1.数据结构是什么
2.顺序表相关的概念以及结构
2.1.顺序表是什么?
2.2.顺序表分类
2.3.两类顺序表的区别
3.动态顺序表的实现
4.代码呈现
4.1.Seqlist.h
4.2.Seqlist.c
正文:
1.数据结构是什么
我们在前面已经学习了许多C语言的知识了,我们已经打好了C语言的基础了,对于什么是数据结构,小编找了两幅图片来解释一下,比较生动:
第一幅图片是散养的羊群,可以看出它们都很分散,可以把它看作成一个很乱的结构,而第二幅图片则是圈养的羊群,此时它们都不会乱跑,可以把它看成很稳定的结构,加入我们找一只叫“小话”的羊,如果他在第一幅图片,那么我们会很难找到,但是如果它在第二幅图,我们可以很轻易的找到。我们可以把羊看做成数据,把它们如何被养的方式叫做结构,此时实际就是数据结构的形象描述,下面我们进入数据结构的正式介绍:
各位理解数据结构的时候可以分两部分理解,分为数据和结构两部分进行理解:
数据,顾名思义,比如有1到10的数字,教务系统里面存放着的学生的姓名,电话,住址,以及通讯录存放的内容(这里先来波预告,通讯录会是我下一篇的主要内容,敬请期待喽)等等,这些都代表着的数据。
结构,我们在想要定义大量数据的时候,如果一个一个定义的话对于计算机来说可读性很差,所以我们可以通过数组来进行大量数据的定义以及存放,此时数组便是一种结构,我们可以把结构理解成把应有的数据联系到一起的方式。
所以数据结构其实是一种计算机存储,组织数据的一种方式,它代表着相互存在一些关系的数据的集合,数据结构反映了数据的内部构成,即数据由哪部分来构成,怎样的方式去构成,还有数据元素之间呈现的结构。
正如我上面所叙述的,数组便是一种简单的数据结构,它可以存储着大量的相同类型的数据,那么,读者朋友们有些可能会有疑问,我们都学了数组了,为什么还要去学习其他的数据结构呢?这很简单,小编来通过一个例子来进行解释:
当我们已经设置好了一个1到10的数组以后,我们如果想要在其中插入一个数据应该怎么做?我们如果想要删除一个数据的时候应该怎么做?我们想要改数据又该怎么办?我们想要查找数据的时候又该怎么做?......
所以可以看出,对于这种复杂情况的发生,普通的数组已经不容易进行操作了,所以此时迎来了我们其他的数据结构,一个基于数组的结构,顺序表的到来。
2.顺序表相关的概念以及结构
2.1顺序表是什么
2.1.1.线性表介绍
在讲顺序表之前,我们现讲它的老大哥,线性表的相关说明:
线性表是具有n个相同特性的数据元素的有限序列,它是在实际中具有广泛作用的数据结构,常见的线性表有:顺序表,链表等等。
线性表在逻辑上是线性结构的,也就是可以认为是一条连续的直线,但在物理结构上,线性表的存放不一定是连续的,线性表在存储数据的时候,通常是以数组和链式结构进行存储的。
举一个例子来帮助读者朋友来认识线性表,就比如水果可以分为苹果,香蕉,橘子等等,这些都是具有相同特性的一类数据结构的集合,这边是线性表,不过我们今天的主角不是它,下面有请我们的主角,顺序表登场:
2.1.2.顺序表的介绍以及它和数组的不同之处
前文提到,顺序表是线性表的一种,不过它的底层实现其实是数组,所以它的物理结构,对于数据的存储连续的,这里就和线性表有很大的不同,对于它和数组的不同,我们拿饭店做对比,我们把数组类比成小菜馆的炒土豆丝,那么顺序表就是米其林餐厅的龙须飞天,其实在底层(本质上)都是由土豆做成的,只不过顺序表经过的包装更多一点。顺序表其实是一个对于数组进行增,删,查,改操作的表,所以底层代码肯定是数组啦,这便是顺序表的定义,顺序表其实是要用到我们在C语言学习的时候用到的结构体知识,这里先打个预防针,下面我们来进如顺序表的分类环节。
2.2.顺序表分类
顺序表分为两类,静态顺序表和动态顺序表,下面我们来先讲讲什么是静态顺序表 :
2.2.1静态顺序表
静态顺序表关键在于静态二字,静态代表的是数据是不改变的,这里其实代表着顺序表中数组是一个确定的数组,除此之外,我们还得在设置一个变量,用来记录数组中我们使用的有效数据的个数,所以此时我们要设置一个既有数组,也有一个整形变量的变量,所以此时我们可以用一个结构体来定义顺序表,下面废话不多说,先来展示一下静态顺序表代码的书写:
typedef struct Seqlist
{
int arr[100]; //这里放置的是想要放置元素的个数
int size; //这个是存放着的是当前数组的有效个数
}S1; //这里用的typedef给这个结构体改的名,方便后期对这个顺序表的应用
上述可以很清晰的看出静态顺序表是如何进行创立的,所以读者朋友从现在一定要记住,顺序表其实就是一个结构体!下面我们再来看一下动态顺序表是如何进行实现的:
2.2.2.动态顺序表
动态顺序表关键在于动态两字,动态代表的是数据是随时可以动的,是可以被改变的,此时代表着数组是可以被修改的,这里我们就要用到之前我们C语言学过的动态内存管理相关的知识了(小编还没出这类的文章,相信在不久的将来小编一定会赶出这一篇文章),此时我们不仅要设置一个变量来记录有效个数,还要设置一个变量来表示总空间(开辟空间)的大小,下面不多说,先来对代码进行展示:
typedef struct Seqlist
{
int* arr;
int size;//这个是放置的有效元素的个数
int kongjian; //这里是指开辟的总空间的大小
}S1;
上面的代码很清晰的解释了小编刚才所说的内容,可能很多读者朋友们会想,顺序表分成了这两类,他们的区别是什么?哪个顺序表更加值得被使用?不要急,下面小编将介绍两种顺序的区别:
2.3.两类顺序表的区别
其实这俩的区别是显而易见的,一个是一个定长的数组,一个是动态的数组,静态顺序表最大的缺点其实就是这个定长,假设我们想要存放一个100个元素的数组,但是此时静态顺序表中数组里面仅仅可以存放99个元素,那么这个时候我们又得大费周章的把数组元素个数增大,这样会显得很麻烦,可读性很差,但是此时对于动态的数组可就不是问题了,只要进行相应的扩容,就可以解决这个问题了,所以通过比较来看静态顺序表的局限性太大了,动态顺序表就灵活了很多,所以之后小编更推荐大家用动态顺序表来进行代码的书写。所以下面小编将会用动态顺序表的知识来进行对数组的增删查改操作,下面是本篇文章的重点,大家一定要好好的领悟!
3.动态顺序表的实现
3.1.1.文件的创立
首先我们在写顺序表之前,需要用到和我们之前进行扫雷游戏时的一个想法,需要创建多个文件来进行代码的书写,首先我们需要一个头文件,来存储着我们在使用顺序表的时候用到的函数,先声明一下,然后还需要一个.c文件,是来实现我们所撰写的函数的,最后还需要一个.c文件,来对我们的代码进行测试,下面图是来展现我们这个的思想的:
3.1.2.动态顺序表实现的逻辑思想
首先,我们在创建顺序表的时候,需要先初始化顺序表,这个是肯定的,因为我们在创立变量的时候也会对变量进行初始化,上面已经讲述了我们如何将顺序表的创建,初始化就变得很简单了,之后我们要完成多个函数的撰写,分明是:头插,尾插,头删,尾删,指定位置插入,指定位置删除,查找指定元素,打印函数,销毁函数的撰写,看着很是复杂,其实写起来一点也不难,我们下面先一一来叙述一下如何进行撰写:
对于尾插函数,我们首先要先避一个小坑,读者朋友们知道,我们在创立顺序表的时候,设置好了总个数和实际的个数两个变量,我们在进行插入的时候,一定要记得先判断个数是否相等,如果不相等,我们可以用realloc函数来进行扩容(这属于动态内存开辟的函数的一种,小编以后会写相关的文章的),之后我们在进行插入操作就好了
对于头插函数,因为要插入到头部,所以我们得先把头部的位置给空出来,此时我们可以采用循环的知识,来做到所有元素向后移动一位,然后再把指定位置的元素插入进去就好了,其实最麻烦的就是尾删,万事开头难,越到后面越容易!
对于尾删函数,这个更是更容易,我们只要把有效个数减1,就会把末尾给去掉,就实现了尾删操作。
对于头删函数,这和头插函数有着相同的特性,不过此时是相反过来的,我们让第二个元素放到第一个元素,第三个放到第二个;这里还是用到了循环的知识,之后我们照常减少一个有效个数就好了,这样我们就实现了头删函数
对于指定位置插入函数,这里和上面的头插函数有点类似,不过这里是把指定的位置给空出来,这里是我们在利用循环的知识进行操作的,然后我们把指定位置放入我们想要的数就好了
对于指定位置删除函数,这里和上面的头删函数有点类似,不过这里是把指定位置删除,和头删一样,我们把·指定的位置后面的元素代替指定位置,依次循环以后我们就会实现我们的指定位置删除操作 。
查找指定元素函数,这里用到了我们之前所讲的循环的知识,这里我们遍历整个数组,来寻找指定的元素,如果找到了,就返回下标,如果没有找到,那么就返回负数,之后我们再通过条件表达式来判断,如果是负数,那么就说找不到,不是的话就打印下标。
销毁函数,主要是对我们动态内存开辟的数组给free掉,然后把它再次弄成空指针,之后我们再把有效个数,空间大小变为零就好了。
我们已经讲完了顺序表建立的逻辑思想,下面我们开始进行代码的书写:
3.1.3.动态顺序表的代码实现
温馨提示:大家在每写完一个函数的时候一定记着调试,这里为了让这个文章不那么复杂,小编就不写了,一定要调试!调试!调试!
首先先来我们的初始化函数的书写,此时我们在前面提到,可以把数组变成空,有效个数和总空间大小变成0就好,下面是代码实现:
void SLInit(S1* ps)
{
ps->arr = NULL;
ps->size = ps->kongjian = 0; //先初始化一下,让它们都是0
}
以上便是对于顺序表的初始化,下面我们来进行销毁操作,这俩比较简单,所以就放到一起了,销毁函数和初始化函数有异曲同工之妙,下面我们来进行代码的呈现:
void SLexit(S1* ps)
{
if (ps->arr)
{
free(ps->arr); //防止忘记清内存导致内存泄漏
ps->arr = NULL; //一定要NULL,避免变成野指针
}
ps->size = ps->kongjian = 0;
}
下面我们来进行尾插函数,在说明整个函数之前,小编先给大家说一下,顺序表并不只是针对于整型的,还有字符型,甚至结构体型的数据来进行对数组的初始化,所以为了让此顺序表灵活性变得更高, 小编就运用了typedef函数,来对int进行了更换名字,后期也可以更好的进行替换,下面是代码实现:
typedef int SLDataType;
所以等会我们对于int的数据统一这么使用,下面我们进入正题,对于尾插函数,我们首先要判断有效个数和空间个数是否相等,如果相等的化,我们就对数组继续扩容,此时运用到了realloc函数来进行扩容,对于扩容多少,各位读者朋友只用知道我们成倍数扩容就好了,这里我们要先判断总空间大小是否为0,因为这里是以总空间大小进行扩容的,所以我们用三木操作符判断是否为0,如果是0就用int来进行倍数增长,如果不为0,那么就用总空间大小来进行扩容,对于其中的内容,我们在逻辑的时候就已经说好了,所以下面直接代码呈现,小编此时把判断是否相等的函数单独放出来了,所以代码分为了两种:
扩容函数:
void S1kuorong(S1* ps)
{
if (ps->size == ps->kongjian)
{
SLDataType newkongjian = ps->kongjian == 0 ? 4 : (ps->kongjian * 2);//这里为了防止size本身等于0
SLDataType* arr1 = (SLDataType*)realloc(ps->arr, newkongjian * sizeof(SLDataType)); //对于数组进行扩容的
assert(arr1);
ps->kongjian = newkongjian;
ps->arr = arr1; //扩容并且内容改变完毕,接下来就要开始尾插了
}
}
尾插函数:
void SLPushBack(S1* ps, SLDataType x)
{
//判断有效个数和空间个数是否相等,如果相等就开辟新的空间 //这里为了后续代码的简洁,我们把下面的函数包装起来
assert(ps); //先判断结构体是否为空指针
S1kuorong(ps);
ps->arr[ps->size++] = x; //这里别忘++不然有效个数就加不了了 而且别再if的语句里不然就只能打印一个
}
下面我们来对头插函数进行介绍,前面我们说过·,此时我们需要用到循环的知识,先把头部的位置给空下来,此时我们用老大哥,for循环来实现这个步骤,老规矩,在我们进行头插之前,一定要记得使用扩容函数来判断下,下面是代码呈现:
void SLPushFront(S1* ps, SLDataType x)
{
assert(ps);
S1kuorong(ps); //日常扩容
int i = 0;
for (i = ps->size; i > 0; i--)
{
ps->arr[ps->size] = ps->arr[ps->size - 1];
}
ps->arr[0] = x;
ps->size++;
}
为了让各位更好的理解头插的原理,小编这里通过图文进行解释,下面是小编之前画过的抽象如图:
一定别忘了让有效个数加1,不然最后呈现不了我们想要的效果,下面我们先来进行尾删操作,整个操作小编认为是这些操作中算是简单的了,我们仅仅需要把有效个数减1就好了,下面不多废话,直接进入代码展示:
void SLPopBack(S1* ps)
{
assert(ps);
assert(ps->size); //一定要保证size不是0
ps->size--; //直接不要最后一个就好了,减少一个有效元素
}
下面我们来进行头删操作,这个也就稍微麻烦了一点点,只不过用到了循环的知识,其实无非就是一个很简单的循环,读者朋友们看到这里一定已经经历了循环的摧残,下面小编不卖关子了,直接进入代码环节:
void SLPopFront(S1* ps)
{
assert(ps);
assert(ps->size);
int i = 0;
for (i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
头删的代码逻辑是这样的,为了让读者朋友们更好的理解到这个代码的作用,小编这里通过图文的方式来展现头删的原理:
上面就是头删的逻辑实现,下面我们基本的删除插入操作已经讲完了,下面来进行一些复杂的操作,对于指定位置进行插入操作,这个操作的底层逻辑其实就是头插的逻辑实现,这里无非就是自己想要进行的位置进行插入操作,最根本的还是循环的知识,所以各位读者朋友一定要好好的掌握好这部分的内容,C语言我们已经学过,一定不要忘记它,数据结构还是依靠着C语言进行实现的,一定要有着丰富的C语言知识,不扯这么多了,下面来进行这个函数的代码实现:
void SLINSERT1(S1* ps, int pos, SLDataType x)
{
assert(ps);
assert(ps->size);
assert(pos >= 0 && pos < ps->size);
S1kuorong(ps); //日常扩容
int i = 0;
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1]; // arr[pos + 1] = arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
同样的,我们依然用图文来解释这个实现的逻辑:
上面便是这个函数的代码实现和图文解释,其实仔细一看,这个代码写的韩式蛮简单的,不过大家一定要知道这个的逻辑实现,以后我们做一些题目的时候,先要对题目进行思考,不要着急写代码,下面我们进入下一个环节,指定位置进行删除的操作:
指定位置进行删除,和头删又有异曲同工之妙,所以由此可以看出,头删和头插真的很重要,一定要理解这俩的代码,这里还是用到了循环的知识,我们通过for,来对数组元素进行更替,然后最后让有效个数减一就好了,下面是代码展示:
void SLErase(S1* ps, int pos)
{
assert(ps);
assert(ps->size);
assert(pos >= 0 && pos < ps->size);
int i = 0;
for (i = pos + 1; i < ps -> size; i++)
{
ps->arr[i - 1] = ps ->arr[i]; // arr[size - 2] = arr[size - 1 ]
}
ps->size--;
}
同样的,为了让读者朋友更好的理解这方面的知识,小编将采用图文的方式来对这个代码进行解释:
上面便是指定位置删除的代码和图文展示,大家也要好好理解并消化,下面来进入顺序表的最后环节,查找操作:
查找操作,可以说是仅次于尾删第二简单的函数了,这个函数就是遍历整个数组,用for循环便可以解决整个问题,次数就是有效个数,然后在进行逻辑写的相应的操作就好了,下面直接上代码:
int SLFind(S1* ps, SLDataType x)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
return i;
}
//这个是循环一圈没找到对应的数,所以返回一个小于0的坐标
return -1;
}
为了让读者朋友更加直观的了解到这个函数的使用,小编这里通过测试的方式来让读者朋友了解到,这中间牵扯到了打印顺序表操作,这个操作就是打印数组元素的操作,小编在之前文章很多次展示了如何打印数组,这里小编就不多叙述了,先来展示一下代码,然后在加上运行图:
打印函数:
void print(S1 ps) //这里直接传递了所以其实不用指针了,但我都写了所以就正常用了
{
int i = 0;
for (i = 0; i < ps.size; i++)
{
printf("%d ", ps.arr[i]);
}
printf("\n");
}
测试环节:
void Seqlist3()
{
S1 ps;
SLInit(&ps);
SLPushBack(&ps, 1);
//print(ps);
SLPushBack(&ps, 2);
//print(ps);
SLPushBack(&ps, 3);
SLPushBack(&ps, 4);
int c = SLFind(&ps, 3);
if (c < 0)
printf("没有找到想要的元素!");
else
printf("找到了,下标是 %d ", c);
SLInit(&ps);
}
展示结果:
可以看出很快就找到了相应的数,以上便是顺序表的撰写,相信读者朋友能看出顺序表比数组高级了很多,并且也更好的去操控数组了,为了让各位读者知道顺序表的完整版,小编在下面提供了头文件和源文件的展示来供各位读者阅读:
4.代码呈现
4.1.Seqlist.h
typedef int SLDataType;
typedef struct Seqlist {
SLDataType* arr;
SLDataType size; //有效个数
SLDataType kongjian;//总空间大小
}S1;
//首先要进行初始化,然后进行书写,最后进行销毁
void SLInit(S1* ps);
//进行真正的内容书写,首先是
//头部插⼊删除 / 尾部插⼊删除
void SLPushBack(S1* ps, SLDataType x); //先从尾插开始,开始了
void print(S1 ps);
void SLPopBack(S1* ps); //下面进行这个函数 //这个是尾删
void SLPushFront(S1* ps, SLDataType x); //头插
void SLPopFront(S1* ps); // 现在第一个顺序表的内容结束了,休息一会刷一会题等会 头删
void SLexit(S1* ps); //这个是销毁操作
//指定位置插入和删除
void SLINSERT1(S1* ps, int pos, SLDataType x); //插入
void SLErase(S1* ps, int pos);//指定位置删除
int SLFind(S1* ps, SLDataType x); //寻找对应坐标
4.2.Seqlist.c
#define _CRT_SECURE_NO_WARNINGS
#include"Seqlist.h"
void SLInit(S1* ps)
{
ps->arr = NULL;
ps->size = ps->kongjian = 0; //先初始化一下,让它们都是0
}
void SLexit(S1* ps)
{
if (ps->arr)
{
free(ps->arr); //防止忘记清内存导致内存泄漏
ps->arr = NULL; //一定要NULL,避免变成野指针
}
ps->size = ps->kongjian = 0;
}
void S1kuorong(S1* ps)
{
if (ps->size == ps->kongjian)
{
SLDataType newkongjian = ps->kongjian == 0 ? 4 : (ps->kongjian * 2);//这里为了防止size本身等于0
SLDataType* arr1 = (SLDataType*)realloc(ps->arr, newkongjian * sizeof(SLDataType)); //对于数组进行扩容的
assert(arr1);
ps->kongjian = newkongjian;
ps->arr = arr1; //扩容并且内容改变完毕,接下来就要开始尾插了
}
}
void SLPushBack(S1* ps, SLDataType x)
{
//判断有效个数和空间个数是否相等,如果相等就开辟新的空间 //这里为了后续代码的简洁,我们把下面的函数包装起来
assert(ps); //先判断结构体是否为空指针
S1kuorong(ps);
ps->arr[ps->size++] = x; //这里别忘++不然有效个数就加不了了 而且别再if的语句里不然就只能打印一个
}
void SLPushFront(S1* ps, SLDataType x)
{
assert(ps);
S1kuorong(ps); //日常扩容
int i = 0;
for (i = ps->size; i > 0; i--)
{
ps->arr[ps->size] = ps->arr[ps->size - 1];
}
ps->arr[0] = x;
ps->size++;
}
void SLPopBack(S1* ps)
{
assert(ps);
assert(ps->size); //一定要保证size不是0
ps->size--; //直接不要最后一个就好了,减少一个有效元素
}
void SLPopFront(S1* ps)
{
assert(ps);
assert(ps->size);
int i = 0;
for (i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
void SLINSERT1(S1* ps, int pos, SLDataType x)
{
assert(ps);
assert(ps->size);
assert(pos >= 0 && pos < ps->size);
S1kuorong(ps); //日常扩容
int i = 0;
for (i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1]; // arr[pos + 1] = arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
void SLErase(S1* ps, int pos)
{
assert(ps);
assert(ps->size);
assert(pos >= 0 && pos < ps->size);
int i = 0;
for (i = pos + 1; i < ps -> size; i++)
{
ps->arr[i - 1] = ps ->arr[i]; // arr[size - 2] = arr[size - 1 ]
}
ps->size--;
}
int SLFind(S1* ps, SLDataType x)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
return i;
}
//这个是循环一圈没找到对应的数,所以返回一个小于0的坐标
return -1;
}
void print(S1 ps) //这里直接传递了所以其实不用指针了,但我都写了所以就正常用了
{
int i = 0;
for (i = 0; i < ps.size; i++)
{
printf("%d ", ps.arr[i]);
}
printf("\n");
}
总结:
今天小编讲述的是数据结构的知识,本来小编这一篇准备写字符函数的,但为了让我更好的学会顺序表的书写,小编还是写了顺序表的内容,这是小编刚学的,可能有很多地方讲得不好或者讲错了,如果这篇文章有错误,恳请大家指出,小编一定会虚心接受大家的意见,那么,我们下一篇博客见啦!