前面我们了解了什么是数据结构,现在我们正式开始数据结构的学习。古人说,工欲善其事,必先利其器。想要用C语言实现数据据结构,必须先准备好集成开发环境。在这里我给大家推荐以下几款:微软的Visual Studio Code(下面简称vs code),Jet Brains的Clion,以及微软的Dev C++。其中vs code需要大量自己手动配置的步骤,可能比较麻烦,但是它的界面比较美观。Dev C++的界面简陋一些,但是安装了就能用,比较方便。大家可以根据自己的需要和喜好来自行选择,总的来说,任何一个可以运行C语言的软件都可以。
学习数据结构,就要从最基础的开始,我们先来认识一下线性表。
线性表是一种典型的线性结构,也是一种最常用的数据结构。线性表的例子不胜枚举,例如英文字母表(A,B,...,Z)是一个线性表,表中的每个英文字母都是一个数据元素;又如成绩单是一个线性表,表中的每个成绩记录是一个数据元素,每个数据元素又是由学号,姓名,成绩等数据项组成的。线性表的顺序存储和链式存储两种,顺序存储的线性表我们称为顺序表,链式存储的线性表我们称之为链表。
线性表是具有相同特性的数据元素的一个有限序列。它的逻辑结构模型如下:
从线性表的定义可以看出,它具有以下特性:
- 有穷性:一个线性表中的元素个数是有限的。
- 一致性:一个线性表中所有元素的性质相同。从实现的角度看,所有元素具有相同的数据类型。
- 序列性:一个线性表中所有元素的相对位置是线性的,即存在唯一的开始元素和终端元素,除此之外,每个元素只有唯一的前驱和后继元素。各个元素在线性表中的位置只取决于它们的序号,所以在一个线性表中可以存在两个值相同的元素。
线性表的抽象数据类型的描述如下:
大家可能会发现,所谓线性表,不就是我们以前常说的一维数组吗?写一个线性表要那么长的代码,为什么不直接用一个一维数组呢?多省时省力,线性表不是多此一举吗?别急,听我慢慢道来。
线性表的作用主要体现在两个方面,当一个线性表实现后,程序员可以用它来直接存放数据,即和一维数组一样,作为存放数据的容器。另外,程序员可以直接使用我们人为给它定义的基本运算来完成更复杂的功能。举个栗子,我们想要在一个一维数组里面特定位置插入一个元素,吧嗒吧嗒打好一堆代码,成功插入了。如果是两个一维数组呢?十个甚至更多呢?难道每一个一维数组都要敲一遍代码?这样会浪费掉很多的时间和精力,人生苦短,及时行乐嘛。这时候,我们就可以派出强力武器线性表了,我们可以在其中自己定义函数来实现一些我们常用的基本功能,只要想要对数据元素进行操作,只需简单地调用函数即可,没必要花大量精力再去写重复的代码,省下来的时间吃两把鸡,给王者上上分不香吗?我觉得这就有点像我们中国唐朝的雕版印刷术。如果只要印刷一篇文章,随你怎么雕刻。如果印刷大量文章的话,一个字一个字刻的话,光工人的劳动报酬都够工场喝一壶的了,细思极恐。于是雕版印刷术应运而生,只要雕刻出一个模板,蘸上墨汁,想印刷多少文章都可以!这省下来了多少人力财力啊!所以说学习计算机,有时候以史为鉴还是挺有必要的。哈哈。
下面的内容涉及到线性表的具体操作,对大家的知识功底有说高不高,说低不低的要求。C语言里的指针和结构在这里显得尤其重要,当然还有一些C语言的基本语法。如果你太久没有碰C语言的话,建议读一读以前的教材,熟练掌握了再来学习数据结构也不迟。
那么我们开始。
数组大小MaxSize一般定义为一个整型常量。如果估计一个线性表不会超过50个元素,则可以把MaxSize定义为50:
#define MaxSize 50
在声明线性表的顺序存储类型时,定义一个data数组来存储线性表中的所有元素,还定义一个整型变量length来存储线性表的实际长度,并且用结构体类型定义SqList表示如下:
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
其中Elemtype是我们自定义的数据类型,假设它是int类型,我们使用以下语句:
typedef int ElemType;
注意:在下面的算法中,线性表元素的逻辑序号是从1开始的,而对应顺序表的data[]数组下标是从0开始的(这种下标称为物理序号),因此要注意它们之间的转换。
首先我们要知道如何创建顺序表:
SqList* CreateList(SqList* L,ElemType a[],int n){
//函数的返回值是一个指向结构体类型SqList的指针。
//函数的形式参数是一个指向结构体类型SqList的指针,一个ElemType类型的一维数组,以及数组的元素个数。
int i=0,k=0;
//定义两个标记元素i和k,分别记录数组元素的下标和线性表的长度。
L = (SqList*)malloc(sizeof(SqList));
//由于L未得到初始化,所以其指向的位置是随机的,我们称之为“野指针”。
//所以必须给这个“野指针”L动态分配一块内存。
//动态分配内存所用的malloc函数在头文件<stdlib.h>和<malloc.h>中都有定义。
while(i < n){
L->data[i]=a[i];//给结构数组的每一个位置赋予对应位置的数组元素的值。
i++; //一个元素赋值结束再赋值下一个元素。
k++; //记录结构数组的长度,循环每执行一次,长度就加1。
}
L->length=k;//给结构数组的长度赋值为k。
//->运算符是*和.运算符的结合,这里可以等价于*L.length=k;
//翻译成中文就是指针L所指向的结构里面的length为k。
return L;//返回这个指向结构体类型的指针。
}
接着是九种基本操作:
- 初始化线性表
SqList* InitList(SqList* L){ //函数的返回值是一个指向结构体类型SqList的指针。 L=(SqList*)malloc(sizeof(SqList)); //给指针L分配了存放线性表的空间。 L->length=0;//将线性表的长度置零。 return L; }
- 销毁线性表
void DestroyList(SqList* L){ free(L);//释放malloc函数为L分配的空间。 //该函数在头文件<stdlib.h>和<malloc.h>里面都有定义。 }
- 判断线性表是否为空
bool ListEmpty(SqList* L){ return(L->length==0); //当L->length为0的时候,顺序表为空,返回true。反之线性表中有元素,返回false。 }
- 求线性表的长度
int ListLength(SqList* L){ return(L->length); //直接将线性表的长度作为返回值返回。 }
- 输出线性表
void DispList(SqList* L){ for(int i=0;i<L->length;i++) //从0到L->length-1,是顺序表元素的下标。遍历顺序表输出元素值。 printf("%d\n",L->data[i]); }
- 求线性表中某个数据元素的值
bool GetElem(SqList* L,int i,ElemType *e){ //形式参数是一个指向结构体类型的指针L,要取出的数据元素的逻辑位置i,以及指向取出元素值类型的指针。 if(i<1||i>L->length)return false; //数据元素的位置只可能是1~L->length,如果位置错误的话,返回false。 *e=L->data[i-1]; //把要找的目标元素通过指针e带出来。 //至于为什么形式参数的e必须用指针形式,而不能用值类型,这里给出解释: //如果变量e是值类型,那么函数内部的形式参数e只是实际参数的一份拷贝,对e操作并不相当于对实际参数操作。 //因此仅仅通过值类型形式参数是不能够把需要的值带出来的。 //如果传进去的是地址,那么直接对内存进行操作,就一定可以通过内存访问到我们所需的值。 //这里类似于之前学习过的“交换两个变量的值”,可以好好回忆回忆。 return true; }
- 按元素值查找
int LocateElem(SqList* L,ElemType e){ int i=0;//标记元素,标记着顺序表数组的下标(物理序号)。 while(i<L->length&&L->data[i]!=e) //当“下标还没增加到顺序表数组最后一个元素”和“尚未找到目标元素”两个条件同时成立是,继续循环。 //也就是说,无论是“数组遍历结束”还是“找到了目标元素”,都会立刻结束循环。 i++; if(i>=L->length)return 0; //当(i>=L->length)时,说明是数组已经遍历完了并且元素没有找到导致的循环结束,返回0。 else return i+1; //反之,就是元素找到了,并且将物理序号转化为逻辑序号返回。 }
- 插入数据元素
bool ListInsert(SqList* L,int i,ElemType e){ //在顺序表的第i个位置插入元素e。 int j;//定义一个数组标记元素j。 if(i<1||i>L->length+1||L->length==MaxSize)return false; //当位置非法或者线性表已满时,返回false。合法的位置是1~L->length+1。 i--;//将插入的逻辑位置转换为物理位置。 for(j=L->length;j>i;j--) L->data[j]=L->data[j-1]; //从顺序表的最后一个元素开始,每个元素往后移动一个位置,直到L->data[i]位置留下空白。 L->data[i]=e;//将L->data[i]位置的空白处填补上想要插入的那个元素。 L->length++;//顺序表的长度要加一。 return true; }
- 删除数据元素
bool ListDelete(SqList* L,int i,ElemType* e){ int j;//定义一个顺序表数组下标的标记元素j。 if(i<1||i>L->length)return false; //删除位置非法时,返回false。 i--;//将要删除元素的逻辑位置转化为物理位置。 *e=L->data[i];//将要删除的元素通过指针e带出函数。 for(j=i;j<L->length-1;j++) L->data[j]=L->data[j+1]; //从被删除元素的下一个元素开始,将每个元素往回移一个位置。 L->length--;//删除一个元素之后,线性表的长度减一。 return true; }
以上操作,我在代码里面已经写了非常详细的注释,我认为没有必要过多地做解释。由于注释破坏了代码的连贯性和美观,我在这里给出完整的代码,供大家查看:
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#define MaxSize 50
typedef int ElemType;
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
SqList* InitList(SqList* L){
L=(SqList*)malloc(sizeof(SqList));
L->length=0;
return L;
}
void DestroyList(SqList* L){
free(L);
}
bool ListEmpty(SqList* L){
return(L->length==0);
}
int ListLength(SqList* L){
return(L->length);
}
void DispList(SqList* L){
for(int i=0;i<L->length;i++)
printf("%d\n",L->data[i]);
}
bool GetElem(SqList* L,int i,ElemType *e){
if(i<1||i>L->length)return false;
*e=L->data[i-1];
return true;
}
int LocateElem(SqList* L,ElemType e){
int i=0;
while(i<L->length&&L->data[i]!=e)
i++;
if(i>=L->length)return 0;
else return i+1;
}
bool ListInsert(SqList* L,int i,ElemType e){
int j;
if(i<1||i>L->length+1)return false;
i--;
for(j=L->length;j>i;j--)
L->data[j]=L->data[j-1];
L->data[i]=e;
L->length++;
return true;
}
bool ListDelete(SqList* L,int i,ElemType* e){
int j;
if(i<1||i>L->length)return false;
i--;
*e=L->data[i];
for(j=i;j<L->length-1;j++)
L->data[j]=L->data[j+1];
L->length--;
return true;
}
SqList* CreateList(SqList* L,ElemType a[],int n){
int i=0,k=0;
L = (SqList*)malloc(sizeof(SqList));
while(i < n){
L->data[i]=a[i];
i++;
k++;
}
L->length=k;
return L;
}
还有什么疑问呢?欢迎留言交流!