定义:
线性结构即为数据元素之间构成了一个有序的序列。线性结构拥有逻辑上的序列,而不一定是物理上的。比方说我们在现实生活中排队,那么队列就是一个线性结构,有前后之分。倘若我们在网络上排队(例如某游戏排队进入服务器)虽然我们在物理上并没有紧紧相挨,但是依旧在逻辑上属于有序的序列,故也是线性结构。线性结构是一个一维的结构,具有“一对一”关系。
最简单的线性结构——数组
数组毫无疑问是一个线性结构,且十分简单。由于在之前已经介绍过了,所以此处省略她。
进化后的线性结构——线性表
数组元素是在物理上相互连接的,但是在线性表中,就不一定如此。根据线性表的数据存储方式,我们可以将线性表分为顺序表和链表,分别对应了顺序存储结构和链式存储结构。
数组的进化——顺序表
顺序表本质上来看还是通过数组实现的线性存储结构,我们一起来瞅一眼定义。
#define MAXSIZE 1000
// 定义MAXSIZE为1000
typedef int ElementType;
typedef int Position;
// 用户自定义的类型Position,在代码中表示"位置",即为数组的下标,从0开始。
typedef struct LNode * PtrToLNode;
// 定义了一个叫做PtrToLNode的玩意,用于替换LNode结构体的指针类型
// 所以接下来的PtrToLNode是个指针类型哈
struct LNode{
ElementType Data[MAXSIZE];
// 用于存放数据的数组
Position Last;
// 指示最后一个数据的下标
};
typedef PtrToLNode List;
// 再给PtrToLNode换个名字叫List,估计是原来那个太长了吧。
这边咱们定义了一个结构体(就是传说中的顺序表)看起来似乎有一丢丢复杂,其实非常简单。就是把一个数组和一个下标组合了起来,下标从0开始,跟数组并没有多大差别。
接下来我们看看顺序表的初始化,要用到之前介绍的malloc函数。
List MakeEmpty(){
List L;
L=(List)malloc(sizeof(struct LNode));
L->Last=-1;
return L;
}
接下来咱们玩点好玩的,大家想想这个诡异的结构怎么插入数据x到位置i上呢。
首先我们肯定要判断位置i是不是合法的,我们知道i肯定不能小于1,因为i不同于下标,第0位没有意义,不能向0位插入内容。同时,i也要小于Last值+2,以此保证顺序表内的数据是连续的。同时因为Last的值是目前最后一个元素的下标,如果i=Last+1的话,即为往最后插入。(这里如果现在不理解,可以等看完下面的插入逻辑再回头来看)
所以,我们能够得到第一个判断。
if(i<1||i>L->Last+2){
printf("位序不合法");
return false;
}
同时我们也需要判断顺序表是否已经满了,如果满了肯定不能进行插入。此时Last的值必须小于数组容量最大值-1,因为Last是下标,从0开始。
if(L->Last==MAXSIZE-1){
printf("表满");
return false;
}
所以我们来看看插入函数的逻辑是什么。首先我们从插入位序i所储存的元素开始,将后面的元素向后移动一格。
然后我们将位置为2的储存空间赋值x并且让最后一位的下标Last++即可。
接下来我们来看看这一步的代码细节。
我们需要从后往前去进行位移,因为如果从前往后就会导致后面的数据被前面的所覆盖。
Position j;
// 这里j是下标,下一行j>=i-1是因为i是位序,需要-1转换成数组下标
for(j=L->Last;j>=i-1;j--){
L->Data[j+1]=L->Data[j];
// 将当前下标的数组的值赋值给数组下一个位置
}
L->Data[i-1]=x;
L->Last++;
这里给出完整的代码
bool Insert(List L, ElementType x, int i){
if(i<1||i>L->Last+2){
printf("位序不合法");
return false;
}
if(L->Last==MAXSIZE-1){
printf("表满");
return false;
}
Position j;
// 这里j是下标,下一行j>=i-1是因为i是位序,需要-1转换成数组下标
for(j=L->Last;j>=i-1;j--){
L->Data[j+1]=L->Data[j];
// 将当前下标的数组的值赋值给数组下一个位置
}
L->Data[i-1]=x;
L->Last++;
return true;
}
当插入过程了然于心后,删除某个位置的元素也变得简单了起来。我们只要判断删除的那个位置是否合法,然后将后面的元素前移,最后将Last--即可。废话不多说,上代码。
bool Delete(List L, int i){
if(i<1||i>L->Last+1){
printf("位序%d不存在元素", i);
return false;
}
Position j;
for(j=i-1;j<=L->Last;j++){
L->Data[j]=L->Data[j+1];
}
L->Last--;
return true;
}
最后我们来瞅一眼查找函数。此函数需要我们找到顺序表中第一个值为x的元素并且输出它的下标。
其实这个函数非常简单,我们只要遍历整个顺序表,找到元素的时候停止遍历然后输出下标即可。如果遍历完成但是没找到元素,就输出错误码。
Position Find(List L, ElementType x){
Position i=0;
while(i<=L->Last && L->Data[i]!=x){
// i从0开始,一直到Last结束
i++;
}
// 如果没有找到x的话,i=Last+1
if(i>L->Last){
// 通过这个判断x是否有被找到
return -1;
}else{
return i;
}
}
冰糖葫芦——链表
链表长得就像不那么直的冰糖葫芦。她在内存中并不是连续的,像是一颗颗珍珠,然后用指针相连。一个链表节点分为两个部分:数据域和指针域
顾名思义,在数据域内,我们可以存放数据,指针域则存放指针。
链表长得像这个样子:
所以,链表的结构定义就非常简单了,一个结构体,两部分组成,一个成员变量是Data,一个是指针。
typedef int ElementType;
typedef struct LNode * PtrToLNode;
struct LNode{
ElementType Data;
PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;
链表的每一个节点都需要我们去malloc出来,然后将指针指向已有的节点。所以就出现了头插法和尾插法。头插法是将新的节点指针指向原来的一条链表的第一个节点,尾插法自然就是原链表最后一个节点的指针指向新节点。
头插法:
List InsertH(List L, ElementType x){
Position tmp;
// 新建一个节点的指针变量
tmp=(List)malloc(sizeof(struct LNode));
// 为其分配空间
tmp->Data=x;
// 填充数据域
tmp->Next=L;
// 填充指针域
return tmp;
}
尾插法跟接下来要展示的插入差不多,所以不做演示了,请各位看官继续看。
事实上,链表也有几种分类。最简单的是带头结点和不带头结点的链表。头结点是一个固定的节点,代表了链表的开头。它的数据域不存放数据,指针域指向链表的第一个节点。这里我们以带头结点的链表为例,展示一下链表的插入函数。
首先来分析一下,往链表某位置插入元素非常简单。我们需要链表的头结点L,需要插入的元素x,以及插入的位序i。当然,我们需要留意i的合法性,即i这个位置可能不在链表内。然后我们找到此位置,并且将前面一个链表的指针域指向新节点,将新节点的指针域指向原本在此位置上的节点。听起来有点绕,上图!
画图水平很差,请各位见谅。蓝色连线会被取消,取而代之的是红色的连线。
话不多说,我们来瞅瞅代码。
bool Insert(List L, ElementType x, int i){
Position tmp, pre;
// tmp保存新的节点,pre保存前一个节点。
int cnt=0;
// 计数器
pre=L;
// 从头开始
while(pre && cnt=i-1){
pre=pre->Next;
// 将pre更新为下一个节点
cnt++;
// 计数器+1
}
if(pre == NULL || cnt!=i-1){
printf("位置错误");
return false;
}else{
tmp=(List)malloc(sizeof(struct LNode));
// 分配空间
tmp->Data=x;
// 填充数据域
tmp->Next=pre->Next;
// 新节点的指针指向前一个节点的指针所指向的地方(即为后一个节点)
pre->Next=tmp;
// 将前一个节点的指针指向新节点
return true;
}
}
领悟了插入,那么删除也就十分简单了。我们只需要找到i位序对应的节点b,将前一个节点a的指针指向b指针域所指向的节点c,然后free(b)将b节点的空间释放就行了。代码跟插入基本一致。
bool Insert(List L, ElementType x, int i){
Position tmp, pre;
// tmp保存要删除的节点,pre保存前一个节点。
int cnt=0;
// 计数器
pre=L;
// 从头开始
while(pre && cnt=i-1){
pre=pre->Next;
// 将pre更新为下一个节点
cnt++;
// 计数器+1
}
if(pre == NULL || cnt!=i-1 || pre->Next==NULL){
// 注意这里多了一个条件,因为插入可以向最后插,但是删除不能删除一个不存在的东西。
printf("位置错误");
return false;
}else{
tmp=pre->Next;
// 将tmp赋值为要删除的节点
pre->Next=tmp->Next;
// 将前一个节点的指针指向要删除节点的后一个节点
free(tmp);
// 她自由了
return true;
}
}
领悟了插入,让我们将插入的过程再次拆解,就能获得两个新的函数。分别是插入的前段——求链表长度;和插入的后段——找链表内元素。
求表长:
int Length(List L){
Position tmp;
int cnt=0;
tmp=L;
while(tmp){
tmp=tmp->Next;
cnt++;
}
return cnt;
}
按位序查找元素:
ElementType FindKth(List L, int i){
Position tmp;
int cnt=0;
tmp=L;
while(tmp){
tmp=tmp->Next;
cnt++;
}
if((cnt==i)&&tmp){
return tmp->Data;
}else{
printf("位置错误");
return -1;
}
}
我们如果在遍历链表的过程内插入一个判断,就可以获得查找元素值的函数:
int Find(List L, ElementType x){
Position tmp;
tmp=L;
int cnt=0;
while(tmp){
// 开始遍历
tmp=tmp->Next;
cnt++;
if(tmp->Data=x){
// 如果找到了就返回
return cnt;
}
}
return -1;
// 遍历完成都没有找到,返回-1
}