本文已收录至《数据结构》专栏,欢迎大家 点赞 + 收藏 + 关注 !
目录
前言
正文
顺序表的分类和操作
静态顺序表
动态顺序表
顺序表的操作接口(动态/静态)
数据结构和预处理
实现前的预处理
数据结构
顺序表的实现
动态顺序表初始化功能
顺序表容量检查功能
动态顺序表的增容功能
顺序表的销毁功能
顺序表的尾插功能(在表尾插入元素)
顺序表的头插功能(在表头插入元素)
顺序表的尾删功能(删除最后一个元素)
顺序表的头删功能(删除第一个元素)
打印顺序表中所有元素功能
顺序表的查找功能
顺序表的指定位置插入功能
顺序表指定位置删除功能
顺序表的优缺点:
顺序表的优点:
顺序表的缺点:
总结
初识数据结构,我们首先了解的数据结构就是线性表。
线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构。
常见的线性表:顺序表、链表、栈、队列、字符串等。
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以顺序结构(数组)和链式结构的形式存储。
本次我们先介绍线性表的顺序结构-顺序表。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表的分类和操作
顺序表的实现有两种:一种是静态顺序表,另一种是动态顺序表。
静态顺序表
静态顺序表是使用定长数组存储元素。
//静态顺序表 #define N 100 typedef int SLDataType; typedef struct SeqList { SLDataType a[N];//N个元素的数组 int size;//当前顺序表存储的元素个数 }SeqList;
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空 间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。
动态顺序表
动态顺序表是使用动态开辟的数组存储。当存储空间不足时可以自动增容,这样就能实现动态的空间存储。
//动态顺序表 typedef int SLDataType; typedef struct SeqList { SLDataType* a;//顺序表首地址 int size;//顺序表当前存储元素个数 int capacity;//顺序表当前空间大小 }SeqList;
顺序表的操作接口(动态/静态)
// 对数据的管理:增删查改 //初始化 void SeqListInit(SeqList* ps); //动态增容 void SLCheckCapacity(SeqList* ps); //销毁 void SeqListDestroy(SeqList* ps); //检查容量是否已满 bool SLSpaceFull(SeqList* ps); //打印元素 void SeqListPrint(SeqList* ps); //尾插 void SeqListPushBack(SeqList* ps, SLDateType x); //头插 void SeqListPushFront(SeqList* ps, SLDateType x); //头删 void SeqListPopFront(SeqList* ps); //尾删 void SeqListPopBack(SeqList* ps); // 顺序表查找 int SeqListFind(SeqList* ps, SLDateType x); // 顺序表在pos位置插入x void SeqListInsert(SeqList* ps, int pos, SLDateType x); // 顺序表删除pos位置的值 void SeqListErase(SeqList* ps, int pos);
数据结构和预处理
实现前的预处理
我们需要typedef重定义需要的数据类型,例如重定义int为其取别名为;为什么要取别名?因为关键字是编译器已经定好了的,我们无法更改,如果我们的代码中需要许多该数据类型的关键字,那么一旦数据类型更改了,例如原来的数据类型是int要改为char,就需要逐一更改,非常费时费力且容易改漏,如果重定义了,需要改代码中所有的数据类型只需要改typedef中重定义的数据类型即可,这样会方便很多。
当然头文件的引用也不能少,顺序表的实现中将引用到stdio.h和stdlib.h以及stdbool.h这三个头文件。
数据结构
//动态顺序表 typedef int SLDataType; typedef struct SeqList { SLDataType* a;//顺序表首地址 int size;//顺序表当前存储元素个数 int capacity;//顺序表当前空间大小 }SeqList;
动态顺序表需要一个数据指针a,以及记下数据个数的size和空间大小capacity。
顺序表的实现
对于动态顺序表,我们有以下功能的实现。主要是围绕增删查改!
动态顺序表初始化功能
首先为什么要对顺序表初始化?我们创建SeqList结构体时,编译器会给结构体中的变量赋随机值,那么指针a就会存储一个随机值,如果不进行置空那么会造成野指针的访问,对计算机的内存造成一定损害,所以初始化是非常必要的!
我们初始化的目标主要是对指针a置空且将记录数据存储个数的size变量和记录空间大小capacity变量置为0,这样就完成了顺序表的初始化。
//初始化 void SeqListInit(SeqList* ps) { assert(ps);//检查指针是否为空 ps->a = NULL;//结构体成员指针a置空 ps->capacity = ps->size = 0;//空间大小和元素个数计数器置为0 }
顺序表容量检查功能
容量检查是一个非常重要的功能,对于数据插入时需要检查空间大小,如果不检查空间大小可能会造成数组的越界,这是非常危险的。
对于容量检查功能,我们检测size是否等于capacity即可,返回结果的bool值。
//检查容量是否已满 bool SLSpaceFull(SeqList* ps) { assert(ps);//检测空指针 //空间满了返回true否则返回false return ((ps->capacity) == (ps->size)); }
对于如此简短的代码,可能有人问:一句代码也需要一个函数去实现吗?这里我想要说的是,对于程序来说,能不直接访问程序中的数据成员就不直接访问,这样可以让程序的封装程度更好。顺序表的操作中经常涉及顺序表的插入,会经常使用该功能,写成函数更加方便我们去调用,其他程序员也可以一看便知。
动态顺序表的增容功能
动态增容功能算是动态顺序表中非常重要的函数接口之一,这个函数接口可以帮助我们在空间已满时及时扩容使我们更加方便的插入数据。
//动态增容 void SLCheckCapacity(SeqList* ps) { assert(ps); if (SLSpaceFull(ps))//判断空间是否为满 { //如果从来没有扩容则分配4个空间否则翻倍 int newcap = (ps->capacity) == 0 ? 4 : 2 * (ps->capacity); //分配newcap个SLDateType类型的顺序空间 SLDateType* tmp = (SLDateType*)realloc(ps->a, newcap*sizeof(SLDateType)); if (tmp == NULL)//判断是否分配成功 { perror("realloc fail!\n"); exit(-1); } ps->a = tmp;//交付空间 ps->capacity = newcap; } }
顺序表的销毁功能
我们在动态内存管理中知道,内存申请和销毁是成对出现的,对于我们运行结束时应该将内存返还给系统,这样既可以保证内存的合理使用也可以防止数据泄露。
这里我们使用free销毁并返还申请的内存给系统,并将size和capacity置为0,将指针a置空。
当然在释放空间前需要使用assert检测是否为空指针且指针a是否不为空。
//销毁顺序表 void SeqListDestroy(SeqList* ps) { assert(ps);//检查空指针 if (ps->a)//检查是否需要释放 { free(ps->a);//释放空间 ps->a = NULL;//指针置空 ps->capacity = ps->size = 0; } }
顺序表的尾插功能(在表尾插入元素)
顺序表的尾插功能是将数据插入顺序表的最后一个位置,顺序表插入功能中最简单的就是尾插功能,不需要对表中的其他元素进行操作,直接在下标处进行插入,然后元素个数加一即可。
//尾插 void SeqListPushBack(SeqList* ps, SLDateType x) { assert(ps); if (SLSpaceFull(ps))//检查空间是否已满 { SLCheckCapacity(ps);//扩容 } (ps->a)[(ps->size)++] = x;//尾插 }
顺序表的头插功能(在表头插入元素)
顺序表的头插功能是在表头插入数据。其实现是通过迭代挪动表中元素空出表头实现的,表中所有元素从最后一个开始向后移动一位,将表头位置空出,然后将插入元素放入表头。如果表为空表则直接插入就行。
//头插 void SeqListPushFront(SeqList* ps, SLDateType x) { assert(ps); if (SLSpaceFull(ps))//检查空间是否已满 { SLCheckCapacity(ps); } if ((ps->size) == 0) { (ps->a)[(ps->size)++] = x;//如果元素个数为0则直接插入 } else { (ps->size)++;//size先自加 for (int i = (ps->size) - 1; i > 0; --i)//所有元素向后移动一位 { (ps->a)[i] = (ps->a)[i - 1]; } (ps->a)[0] = x;//插入头位 } }
顺序表的尾删功能(删除最后一个元素)
顺序表的尾删功能是删除顺序表中最后一个元素,尾删功能同尾插功能一样简单,让当前元素数size自减1即可,如果存储的是重要数据,还可以将尾部数据块置空防止数据泄露。但在此之前也有一项重要的地方需要检查,就是表是否为空,如果顺序表已经为空,则没必要删空气导致越界了!
//尾删 void SeqListPopBack(SeqList* ps) { assert(ps); if ((ps->size) != 0)//数据个数不为0 { (ps->a)[(ps->size) - 1] = 0;//置空尾元素 --(ps->size);//size自减 } }
顺序表的头删功能(删除第一个元素)
顺序表的头删功能是将顺序表中第一个表头元素删除,较好的方式是覆盖第一个元素,让第二个元素成为表头。具体实现是通过迭代,从第二个元素开始向前覆盖,一直到最后一个元素,然后size自减1。这里也需要注意空表删空气越界问题!
//头删 void SeqListPopFront(SeqList* ps) { assert(ps); if ((ps->size) != 0) { if ((ps->size) == 1) {//如果只有一个元素直接删除即可 (ps->a)[(ps->size) - 1] == 0; --(ps->size); } else { for (int i = 0; i < (ps->size) - 1; ++i) {//迭代覆盖首元素 (ps->a)[i] = (ps->a)[i + 1]; } (ps->a)[(ps)->size-1] = 0;//置空最后一个元素 --(ps->size); } } }
打印顺序表中所有元素功能
打印表中全部的元素功能是将表中所有元素逐一按下标顺序输出,其实现是通过迭代将逐一访问下标元素并通过printf输出。
//打印元素 void SeqListPrint(SeqList* ps) { assert(ps); //提示打印元素 printf("表元素:"); //for循环使用下标访问并打印每个元素 for (int i = 0; i < ps->size; ++i) { printf("%d ", (ps->a)[i]); } printf("\n"); }
顺序表的查找功能
顺序表的查找功能是在表中查找指定值,若查找到返回其下标,若没找到则返回-1,如果是空表返回-2,这些返回值都会交给主函数,在主函数中进行逐一处理。实现是通过迭代遍历顺序表。
//查找 int SeqListFind(SeqList* ps, SLDateType x) { assert(ps);//检查空指针 if ((ps->size) != 0)//是否为空表 { for (int i = 0; i < (ps->size); ++i) {//迭代查找 if ((ps->a)[i] == x) { return i; } } return -1;//没有找到则返回-1 } return -2;//空表返回-2 }
顺序表的指定位置插入功能
顺序表指定下标插入功能是实现任意位置的插入,首要前提是下标没有越界且指针不为空,对于插入函数,首先要判断的就是空间是否已满,如果空间已满则先申请空间,其次为了降低此功能在一些重复功能上的时间复杂度,例如头插(在1位置插入)和尾插(在size位置插入),在进行插入前会对下标再次进行判断是否为表的两端点,然后再执行插入。这里要注意的是pos下标在实际使用时需要减1,因为数组下标从0开始。
具体实现是先size自加1为当前增加一个空间,然后迭代从size-1(最后一个数据空间)开始向后覆盖一位,从而将指定的pos位置空出。
//插入指定位置之前 void SeqListInsert(SeqList* ps, int pos, SLDateType x) { if (pos >= 1 && pos <= (ps->size))//检查位置是否合法 { assert(ps); if (SLSpaceFull(ps))//检查空间是否已满 { SLCheckCapacity(ps); } if ((ps->size) == 0)//空表进行头插 { (ps->a)[(ps->size)++] = x; } else { if ((ps->size) == pos)//如果位置是表尾 { SeqListPushBack(ps, x);//进行尾插 } else if (1 == pos)//如果位置在表头则进行头插 { SeqListPushFront(ps, x);//头插 } else //插入指定位置 { ++(ps->size); //指定位置的值之前插 for (int i = (ps->size) - 1; i > pos-1; --i) { (ps->a)[i] = (ps->a)[i - 1]; } (ps->a)[pos-1] = x; } } } else { printf("位置不存在!\n"); } }
顺序表指定位置删除功能
顺序表的指定位置删除功能是我们输入一个位置,系统将该位置的值删除的功能。在删除前,我们需要判断空表避免删空气导致越界。特殊位置的删除(例如头和尾)作特殊处理进行调用。
具体实现是首先判断输入的位置是否合理(没有越界),然后让该下标的后一个元素覆盖自己,紧接着后面的元素覆盖当前坐标的后一个元素一直到最后一个表尾元素覆盖其前一个元素,将旧表尾位置0,然后size自减1完成删除。这里也要注意的是pos下标在实际使用时需要减1,因为数组下标从0开始。
//指定删除 void SeqListErase(SeqList* ps, int pos) { if (pos >= 1 && pos <= (ps->size))//检查位置是否合法 { assert(ps); if ((ps->size) != 0) { if ((ps->size) == 0)//特殊位置特殊处理 { SeqListPopFront(ps);//头删 } else if ((ps->size) == pos) { SeqListPopBack(ps);//尾删 } else { for (int i = pos-1; i < (ps->size)-1; ++i)//从pos位置对应下标开始覆盖 { (ps->a)[i] = (ps->a)[i + 1]; } --(ps->size); } } } else { printf("位置不存在!\n"); } }
顺序表的优缺点:
顺序表的优点:
1、无须为表示表中元素之间的逻辑关系而添加额外的存储空间。
2、能够高速的存取表中任一位置的元素。
顺序表的缺点:
1、插入和删除操作须要移动大量的元素。
2、当线性表长度变化较大时,难以确定存储空间的容量。
3、造成存储空间的“碎片化”。
本次我们介绍了数据结构中的线性表的顺序结构,介绍了顺序表的静态和动态存储结构以及各种功能的实现,让我们对数据结构有了一定的认识。当然,线性表的知识还不止于此,我们还有链式结构没有介绍,在下一篇线性表的文章中我将为大家详细介绍线性表的链式结构,请大家敬请期待!
本次线性表顺序结构的知识分享就暂时先到这里啦,喜欢的读者可以多多点赞收藏和关注。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
其他文章阅读推荐
C语言之数据结构初级<时间和空间复杂度>_ARMCSKGT的博客-CSDN博客
C语言入门<分支语句>_ARMCSKGT的博客-CSDN博客
C语言入门<循环语句>_ARMCSKGT的博客-CSDN博客
欢迎读者多多浏览多多支持!