目录
源文件(Contiguous.cpp)中的函数功能具体实现:
在Test文件中定义一个head的顺序表,并对其进行初始化操作:
引言:
在前面数据结构的第一篇文章中介绍了数据结构中的基础概念和时间复杂度的相关概念,对数据结构有了一个基本的认识,前几天系统的学习了顺序表,刚好今天将顺序表从无到有手动自己敲了一遍,我在代码中也仔细写了顺序表中几乎每一个功能和每一条语句的注释,这篇文章对顺序表的知识进行学习并总结。
数据结构学习目录:
数据结构系列学习(一) – An Introduction to Data Structure
学习:
顺序表的简单介绍:
在计算机内部存储一张线性表(线性结构的数表),最为方便简单的就是用一组连续的地址的内存单元来存储整张线性表。这种存储结构称为顺序存储结构。这种存储结构下的线性表就称为顺序表。
一张顺序表包括这几个特征:
有一个唯一的表名来标识该顺序表;
内存单元连续存储,也就是说一张顺序表必须要占据一块连续的内存空间;
数据顺序存放,元素之间有先后关系;
定义一张顺序表也就是在内存中开辟一段连续的存储空间,同时我们定义的顺序表也应该实现增删查改这四个功能。
文件之间的关系:
在编程过程中,文件编译的顺序一般是从上往下的,如果我们编写的函数在main函数之前,那么在main函数里面我们可以直接进行调用,但是如果我们把函数写在主函数之后,当我们继续在主函数里调用函数时,程序就会报错,这里我们举一个简单的例子。
例如我在这里定义一个Add函数,但是我把它写在了主函数的后面:
程序报错显示使用了未定义的关键字Add,此时我们要消除掉这个报错的话,方法就是在主函数前面进行函数的声明,添加语句:
int Add(int a,int b);
注意后面必须要有分号!
在文件中,同样也需要进行函数声明,函数声明和函数功能的实现分别是在不同的文件中完成的。这里引入两个文件形式,分别是:.h和.cpp后缀的文件:
.h文件形式名为头文件,当我们在实现一个功能比较复杂的程序的时候,我们可以将所有的函数声明都写在.h文件里面,并在文件中进行结构体的设计以及常变量和宏常量的定义;
然后我们再在cpp文件里面再进行具体函数功能的实现,并在开头以头文件的形式对,h文件进行引用,这样下来的一个组合就称之为编译器中的一个工程项目,例如我们在接下来即将耀实现的顺序表。
头文件中符号 “” 和 <> 的区别问题:
注意,在这里我们必须要分清楚在引用头文件时 “” 和 <>的区别。
但是我们如果直接这样写:#include<Contiguous_List.h>的话,是会报错的,原因就在于使用<>符号时,编译器会直接在系统变量路径中查找头文件,例如我们平时书写的程序前面就有#include<stdio.h>标准输入输出的头文件,而Contiguous_List.h文件是我们自定义的头文件,系统变量中是没有的,这时我们就要使用””符号来将文件名包括进去:
#include"Contiguous.h"
这样书写程序就不会报错了,但是我们在引用C本身的头文件时其实也是可以用””符号的,这里就牵扯到了一个先后顺序的问题:
<>符号只用于直接在系统变量路径中查找头文件;
“”符号既可以用于查找用户自定义的头文件,也可以用于查找系统变量路径中的头文件,但在查找顺序上,优先进行用户项目路径查找,然后再进行系统变量路径查找。
所以当我们在上方引用用户自定义头文件之后添加#include”stdio.h”也是不会报错的。
顺序表功能说明:
我们首先应该明确的是一个完整的顺序表应该具有的功能:
操作函数(fuction):
初始化函数(Init_Sqlist)
清空函数(Clear)
销毁函数(Destroy)
扩容函数(Inc)
打印函数(Show)
插入函数(Insert):
头部插入函数(Insert_head)
尾部插入函数(Insert_tail)
指定位置插入函数(Insert_pos)
删除函数(Delete):
头部删除函数(Delete_head)
尾部删除函数(Delete_tail)
指定位置删除函数(Delete_pos)
指定值删除函数(Delete_val)
判断函数(Is):
判断顺序表是否已满函数(Is_Empty)
判断顺序表是否已空函数(Is_Full)
头文件(Contiguous.h)中的函数声明:
在设计结构体之前,我们首先得定义顺序表的初始长度:
#define LIST_INIT_SIZE 100//顺序表的初始长度
首先我们得定义一个符合顺序表元素类型的指针变量用来存放顺序表的存储空间基址;
顺序表当前的长度;
顺序表当前的总容量;
我们用Elemtype来重新定义int类型的参数,用length来表示顺序表当前的长度、用listsize来表示顺序表当前的总容量,结构体可以这样来表示:
typedef int Elem_type;//对int类型起一个别名
typedef struct Sqlist
{
Elem_type * elem;//存储空间基址(用来接收malloc返回在堆上申请的连续空间块开辟)
int length;//当前有效长度
int listsize;//当前总空间大小
}Sqlist,*PSqlist;//结构体末尾的重命名相当于后面这两个语句:
//typedef struct Sqlist Sqlist;
//typedef struct Sqlist* PSqlist;
头文件中的所有函数声明:
void Init_Sqlist(struct Sqlist* sq);//初始化
bool Insert_head(struct Sqlist* sq,Elem_type val);//头插(插入需要判满,自然,删除也需要判空)
bool Insert_tail(struct Sqlist* sq,Elem_type val);//尾插
bool Insert_pos(struct Sqlist* sq,int pos,Elem_type val);//按位置插
bool Delete_head(struct Sqlist* sq);//头删
bool Delete_tail(struct Sqlist* sq);//尾删
bool Delete_pos(struct Sqlist* sq,int pos);//按位置删
bool Delete_val(struct Sqlist* sq,Elem_type val);//按值删(找这个值在顺序表中第一次出现的位置,然后删除它)
bool Is_Empty(struct Sqlist* sq);//判空
bool Is_Full(struct Sqlist* sq);//判满
void Inc(struct Sqlist* sq);//扩容函数
int Search(struct Sqlist* sq,Elem_type val);//查找(找这个值在顺序表中第一次出现的位置)
void Clear(struct Sqlist* sq);//清空(将数据晴空,认为没有有效值)
void Destroy(struct Sqlist* sq);//销毁(将数据存储空间都释放掉)
void Show(struct Sqlist* sq);//打印
源文件(Contiguous.cpp)中的函数功能具体实现:
初始化函数(Init_Sqlist):
(1)按照需要为线性表分配一个预定义大小的存储区域LIST_INIT_SIZE,也就是顺序表的最大容量,如果存储分配失败,则给出错误信息。
(2)设置线性表的初始长度为0
(3)设置线性表的当前存储容量为顺序表的最大容量
代码:
void Init_Sqlist(struct Sqlist* sq)//初始化
{
sq->elem = (Elem_type*)malloc(LIST_INIT_SIZE * sizeof(int));
assert(sq->elem != NULL);//安全性判断
sq->length = 0;//将顺序表初始长度设为0
sq->listsize = LIST_INIT_SIZE;//将顺序表初始总容量设置为前面的宏定义
}
判断顺序表是否为空函数(IsEmpty) :
这个函数比较好理解,当顺序表的长度为0时,顺序表自然也就是空的顺序表。
代码:
bool Is_Empty(struct Sqlist* sq)//判空
{
//1 安全性处理
assert(sq != NULL);
//2 顺序表的长度为0时,判断顺序表为空
return sq->length == 0;
}
判断顺序表是否已满函数(IsFull):
同样,当顺序表的长度等于顺序表的总容量时,顺序表就是已满的状态。
代码:
bool Is_Full(struct Sqlist* sq)//判满
{
//1 安全性处理
assert(sq != NULL);
//2 当顺序表的长度等于顺序表的总容量时判为满
return sq->length == sq->listsize;
}
查找函数(Search) :
这个函数其实就是广义上的顺序查找,从顺序表中的第一个元素开始向后遍历,找到了就返回元素的下标,找不到就直接返回-1.
代码:
int Search(struct Sqlist* sq,Elem_type val)//查找(找这个值在顺序表中第一次出现的位置)
{
//1 安全性处理
assert(sq != NULL);
//2 顺序查找,找到了返回下标,找不到了直接返回-1
for(int i = 0;i < sq->length;i++){
if(val == sq->elem[i]){
return i;
}
}
return -1;
}
扩容函数(Inc):
扩容函数需要用到<malloc.h>头文件中的realloc函数,当顺序表满了且我们还需要再向顺序表中添加元素,我们就要执行扩容操作,命名一个sq->elem用来存放realloc函数扩容之后的空间的新的内存基址,默认2倍扩容,顺序表的存储容量 * 2。
代码:
void Inc(struct Sqlist* sq)//扩容函数
{
//1 安全性处理
assert(sq != NULL);
//2 使用sq->elem保存realloc函数扩容后的新内存基址
sq->elem = (Elem_type*)realloc(sq->elem,(sq->listsize*sizeof(int))* 2);
//3 安全性处理
assert(sq->elem != NULL);
//4 sq->length并不需要改变,需要改变的是顺序表总的长度
sq->listsize *= 2;
}
头部插入函数(Insert_head):
当我们执行头部插入函数的时候,首先要明确的是,会产生两种情况(Opt分别代表两种情况):
首先我们要执行的是判满操作,如果顺序表已满,我们就要进行扩容操作,扩容操作完成之后,将所有元素相后统一迁移一个单位(注意是从最后一个元素开始,如果是从第一个元素开始,那么存储顺序和地址就会被破坏),然后在执行头部插入操作;如果顺序表没满,我们就直接将所有元素向后迁移一个单位,然后再进行插入操作。
代码:
bool Insert_head(struct Sqlist* sq,Elem_type val)//头插
{
//1安全性处理
assert(sq != NULL);
//2判满操作
if(Is_Full(sq)){
Inc(sq);
}
//3向后挪动元素(从尾部向前进行移动)
for(int i = sq->length - 1;i >= 0;i--){
sq->elem[i + 1] = sq->elem[i];
}
//4插入值
sq->elem[0] = val;
//5length+1
sq->length++;
return true;
}
尾部插入函数(Insert_tail):
与头部插入函数不同的是,尾部插入函数的执行并不需要元素的迁移,只需要直接再顺序表后面进行插入操作即可,所以它这个函数的时间复杂度为O(1),以下是流程图:
与头部插入函数一样都需要判满,判满为真执行扩容函数,扩容函数之后进行尾部插入操作;判满为假直接进行插入操作。
代码:
bool Insert_tail(struct Sqlist* sq,Elem_type val)//尾插
{
//尾插并不需要挪动元素,如果满了就扩容,再在尾部进行添加,如果没满直接在后面插就行
//1安全性处理
assert(sq != NULL);
//判满操作
if(Is_Full(sq)){
Inc(sq);
}
sq->elem[sq->length] = val;
//length+1
sq->length++;
return true;
}
按位置插入函数(Insert_pos):
执行按位置插入函数时,也会产生两种情况,与头插函数对应,如果顺序表满了就进行扩容操作,扩容完了从指定位置(下标)开始讲元素统一向后迁移一个单位,然后再在指定位置(下标)进行插入元素操作;要是顺序表没满,则直接进行迁移操作并在指定位置进行元素的插入操作,以下是流程图:
代码:
bool Insert_pos(struct Sqlist* sq,int pos,Elem_type val)//按位置插
{
//默认当pos == 0时为头插,所以当pos == length的时候也就是尾插了
//1安全性处理
assert(sq != NULL);
//2判满
if(Is_Full(sq)){
Inc(sq);
}
//3移动元素位置
for(int i = sq->length;i >= pos;i--){
sq->elem[i + 1] = sq->elem[i];
}
//4将值放进去
sq->elem[pos] = val;
//5length+1
sq->length++;
return true;
}
头部删除函数(Delete_head):
插入需要判满,删除需要判空,如果一个顺序表本身就是空的,那么自然也就不存在删除操作。所以头部删除函数也会衍生出两种情况分别是顺序表已空的情况和顺序表不空的情况,以下是流程图:
代码:
bool Delete_head(struct Sqlist* sq)//头删
{
//1安全性处理
assert(sq != NULL);
//2判空
if(Is_Empty(sq)){
return false;
}
//3移动元素位置
for(int i = 0;i < sq->length - 1;i++){
sq->elem[i - 1] = sq->elem[i];
}
//4length-1
sq->length--;
return true;
}
尾部删除函数(Delete_tail):
与头部删除不同,尾部删除函数不需要进行移动,所以时间复杂度为O(1),但同样需要判空,以下是流程图:
代码:
bool Delete_tail(struct Sqlist* sq)//尾删
{
//1 安全性判断
assert(sq != NULL);
//2 判空
if(Is_Empty(sq)){
return false;
}
//3 length - 1
sq->length--;
return true;
}
按位置删除函数(Delete_pos):
首先进行判空操作,如果为空直接返回false,如果不为空,将待删除元素后面的值统一向前迁移一个单位,长度减1,以下是流程图:
这里需要注意的是,当pos值为0时,其实执行的是头部删除操作,所以我们需要添加一个判定条件就是pos >= 0,这里与按位置插入操作类似,当pos == 0时执行的也是头插操作。
代码:
bool Delete_pos(struct Sqlist* sq,int pos)//按位置删
{
//1 安全性处理
assert(sq != NULL);
//2 将待删除节点后面的节点统一向前移一位,假设pos = 0时为头删判断pos的合法性
assert(pos >= 0 && pos < sq->length);
//3 判空
if(Is_Empty(sq)){
return false;
}
//4 假设让i指向被覆盖者的一方
for(int i = pos;i < sq->length - 1;i++){
sq->elem[i] = sq->elem[i + 1];
}
//假设让i指向覆盖者的一方
// for(int i = pos + 1;i < sq->length;i++){
// sq->elem[i - 1] = sq->elem[i];
// }
//5 length - 1
sq->length--;
return true;
}
按值删除函数(Delete_val):
按值删除函数是建立在顺序表不为空的前提下,所以不用进行判空操作。我们先定义一个临时变量temp用来保存在顺序表中查找到元素的下标,然后再在这里直接调用按位置删除函数(Delete_pos)即可,以下是流程图:
代码:
bool Delete_val(struct Sqlist* sq,Elem_type val)//按值删(找这个值在顺序表中第一次出现的位置,然后删除它)
{
//1 安全性处理
assert(sq != NULL);
//2 定义临时变量保存要删除元素的下标(先查找)
int temp = Search(sq,val);
//如果找到了,temp保存这个值的下标,如果没找到temp返回-1
if(temp == -1){
return false;
}
//3 如果代码执行到了这一行
return Delete_pos(sq,temp);
}
清空顺序表函数(Clear):
清空顺序表的意思也就是将顺序表的长度直接赋值为零。
代码:
void Clear(struct Sqlist* sq)//清空(将数据晴空,认为没有有效值)
{
//1 安全性处理
assert(sq != NULL);
//2 将顺序表的长度直接赋值为0
sq->length = 0;
}
销毁顺序表函数(Destroy):
销毁顺序表的意思也就是将初始化函数中使用malloc申请的那一片内存空间(扩容与否都是)直接释放掉,顺序表自然也就不存在了。
代码:
void Destroy(struct Sqlist* sq)//销毁(将数据存储空间都释放掉)
{
//1 安全性处理
assert(sq != NULL);
//2 释放掉申请开辟的空间
free(sq->elem);
//3 将顺序表的长度和总容量全部置为0
sq->length = 0;
sq->listsize = 0;
}
打印顺序表函数(Show) :
这个函数比较简单,其实就是遍历然后再输出。
代码:
void Show(struct Sqlist* sq)//打印
{
//1 安全性处理
assert(sq != NULL);
//2 打印操作
for(int i = 0;i < sq->length;i++){
printf("%3d",sq->elem[i]);
}
printf("\n");
}
测试文件(Test.cpp)中对实现的功能进行测试:
在Test文件中定义一个head的顺序表,并对其进行初始化操作:
struct Sqlist head;
Init_Sqlist(&head);
初始化之后向顺序表中填充100个元素:
for(int i = 0;i < 100;i++){
Insert_pos(&head,i,i + 1);
}
测试头插函数和扩容函数(Insert_gead):
在顺序表的头部插入元素100并打印顺序表的长度和存储空间长度,顺序表满了首先要进行扩容然后再进行插入操作:
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
printf("\n");
Insert_head(&head,100);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
如图,我们的初始总容量设置为100个单位,当我们需要将元素100添加值顺序表的头部,先要执行判满操作,然后对顺序表进行二倍扩容,再将元素相后迁移,然后再将100添加至顺序表头部,运行结果是正确的。
测试尾插函数(Insert_tail):
将元素100插入至顺序表的尾部,此时因为顺序表满了,首先要扩容,然后再插入,listsize变量变为原来的2倍也就是200:
Insert_tail(&head,100);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试按位置插函数(Insert_pos):
指定2号下标位置插入5元素:
Insert_pos(&head,2,5);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试头删函数(Delete_Head):
对顺序表的头部元素进行删除操作:
Delete_head(&head);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试尾删函数(Delete_tail):
对顺序表的尾部元素进行删除操作:
Delete_tail(&head);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试按位置删函数(Delete_pos):
我要删掉顺序表中2号下标的元素,如图:
Delete_pos(&head,2);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试按值删函数(Delete_val) :
我要删掉的值为2,首先先在顺序表中查找2是否存在,存在就将2元素后面的值向前进行覆盖:
Delete_val(&head,2);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试查找函数(Search):
我要查找的值为10,并使用temp保存查找函数的返回值,如果在顺序表中找到了元素10则返回下标,如果没有则返回-1:
int temp = Search(&head,10);
printf("result = %d\n",temp);
运行结果:
测试清空函数(Clear):
Clear(&head);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
测试销毁函数(Destroy):
Destroy(&head);
Show(&head);
printf("\nlength = %d",head.length);
printf("\nlistsize = %d",head.listsize);
运行结果:
总结:
顺序表的优势在于可以直接访问,同时它的删除和插入操作主要集中在尾部。但有优势就代表着有相应的劣势,顺序表再头部和中间位置的插入和删除操作,操作的次数非常多,随之而来的也是较高的时空开销,且频繁的扩容的操作所产生的代价也是相对较高的。
至此顺序表的实现已经基本完成,这篇文章用于对老师在课上所讲内容的总结,如果还有其他功能还可以补充,顺序表是我学习的第一种数据结构,并综合了C语言中的知识,例如:动态内存管理、结构体、指针,写顺序表的代码也相当于是对这些知识的回顾,后续在严蔚敏 – 《数据结构(C语言版)》中线性表的其他功能我会在后期进行补充。我会将顺序表的完整代码上传至资源里。
参考资料:
严蔚敏 – 《数据结构(C语言版)》