1. 线性表的定义
线性结构的特点是数据元素之间是一种线性关系
线性表的定义是相同数据类型的n个数据元素的有限序列,每个数据元素有且仅有一个直接前驱和一个直接后驱。
需要说明的是:通常将线性表中数据元素的类型抽象为ElemType,ElemType的类型根据具体情况而定(可以通过typedef语句在使用前把它定义为任何一种具体类型)
例如若把它定义为整数类型
typedef int ElemType
线性表的基本操作有:初始化,求长,取表元等等等等
2.线性表的顺序储存结构及其运算
2.1线性表的顺序储存结构
一般采用一维数组来表示顺序表的数据存储区域
若线性表的第一个元素a1的存储地址为Loc(a1),则第i个元素的(ai)的存储地址为
Loc(ai)=Loc(a1)+(i-1)*d 1<=i<=n
在定义一个线性表的顺序储存类型时,需要定义 一个数组来储存线性表中的所有元素,并定义一个整型变量来存储线性表的长度。为了便于后续操作,可以将这两个变量同时说明在一个结构类型中,则顺序表的定义如下:
#define MaxSize 100 //假设线性表的最大容量,假设为100
typedef int Elemtype //每个元素的类型Elemtype可以为任何类型,假设为int
typedef struct SqList{
Elemtype data[MaxSize];
int length; //线性表长度
};//顺序表的数据类型为SqList
上述代码的线性表如下所示:
2.2顺序表的基本运算
2.2.1顺序表的初始化
线性表的存储空间可以通过malloc(sizeof(SqList))动态分配获得
具体算法如下
SqList* init_SqList(){
//构造一个空的顺序表L
SqList L;
L=(SqList*)malloc(sizeof(SqList)); //动态分配储存空间
if(!L) exit(1); //存储分配失败,则中止程序运行,该函数在 stdlib.h 中有定义
L->length=0; //线性表为空,长度为0
return L;
}
2.2.2插入运算
插入后会使原表长为n的线性表成为表长为n+1的线性表,而由于顺序表中的元素在计算机中是连续存放。因此若想要第i个位置上插入一个值为x的新元素,就必须将表中第i,i+1....个数据元素依次向后移动一位,将第i个位置空出来,同时线性表的长度变为length+1。
当然,如果从算法健壮性考虑,设计算法时还要考虑存储空间在未插入前是否已经处于满状态。同时还需要考虑插入位置是否合理,i的合理取值范围为1<=i<=n+1(n为表长)
综上所述,在运算表上实现插入运算可以按照以下步骤:
1. 判断插入位置i是否合理,不合理提示错误并中=中止程序
2. 检查顺序表的储存空间是否已满,若满则提示用户不能再做插入
3. 将原表中ai~an元素依次后移一个存储单元(向表尾移动),为新元素腾出位置
4. 将x置入空出的第i个位置
5. 修改表的长度,使长度加1
具体算法实现如下:
SqList* Insert_SqList(SqList* L,int i,Elemtype x){
//在顺序表L的第i个位置上插入一个值为x的新元素
if((i<1)||(i>length+1)){ //检查插入位置的正确性
printf("插入位置i不合理!");
exit(1); //该函数在stdlib.h中有定义
}
if(L->length=L->MaxSize-1){ //顺序表已满(这里假设首地址为a0)
printf("顺序表已满,不能再插入!");
exit(1);
}
for(int m=L->length-1;m>=i-1;m--)
L->data[m+1]=L->data[m]; //节点后移
L->data[i-1]=x; //新元素插入
L->length++; //表长加1
return L;
}
插入算法的时间性能分析:
时间主要消耗在数据的移动上,平均来说需要移动表中一半的数据元素,因此该算法的时间复杂度为O(n) (具体计算过程略)
2.2.3删除运算
该算法与插入算法相似,将长度减1,并将元素前移
从算法健壮性考虑,主要考虑i值的正确性,i的合理取值范围是1<=i<=n,其中n为表长
删除表的基本操作为
1. 检查删除位置i是否合理,若不合适提示错误并中止程序
2. 将表中a(i+1)~an依次前移一个存储单元(向表头方向)
3. 修改表的长度,使长度减1
算法具体如下:
SqList* Delete_SqList(SqList* L,int i,Elemtype e){
//删除顺序表中L的第i个元素
if((i<1)||(i>L->length)){//检查位置的正确性
printf("插入位置不合理!");
exit(1);
}
e=L->data[i-1]; //删除该元素后,该数据就不存在,如过需要,先取出保存
for(i;i<=L->length-1;i++)
L-data[i-1]=L->data[i];//节点前移
L->length--;
return L;
}
时间性能分析:
时间复杂度与插入算法类似,空间复杂度也为O(n)
2.2.4按值查找
完成该运算最简单的方法是,从一个元素开始与x进行比较,直到找到一个与x值相等的数据元素,并返回它在顺序表中的存储下标,如果查询完整个表都没找到与x相等的元素,表明查找失败,并返回-1
具体算法如下:
int LocateElem_Sq(SqList* L,Elemtype x){
//在顺序表中查找值为x的元素,查找成功就返回元素存储位置,失败就返回-1
int i=1;
for(;i<=L->length;i++){
if(L->data[i-1]==x)
return i;//这里返回的是元素在第几个位置
}
return -1;
}
算法的时间复杂度为O(n)
3. 线性表的链式存储结构及其运算
线性表链式存储结构不需要地址连续的存储单元来实现,它是通过“链”建立起数据元素之间的逻辑关系。链表可分为单链表,循环链表,双向链表
3.1单链表
链表运用指针实现线性表中各元素之间的逻辑关系,因此对于每个数据元素ai,除了存放数据元素自身的信息外,还需要存放其直接后继a(i+1)的存储地址。因为每个节点只有一个指向直接后继的指针,所以称其为单链表。
单链表对应的链式存储结构如图:
单链表最后一个节点没有后继,其指针域不指向任何结点,通常用NULL或^表示,表明链表到此结束。线性表中第一个节点的地址需要存放到一个指针变量(如head)中,这样才能找到链表的第一个节点。通常将第一个节点的指针称为头指针。
单链表节点的数据类型定义如下:
typedef struct LNOde{
ElemType data; //数据域
struct LNode* next; //指针域
}LNode;
定义头节点指针
LNode *head;
接下来我们都将LNode类型节点的指针声明为LinkList类型,即
typedef LNode* LinkList;
头节点可定义为
LingList head;
在实际应用中,通常在单链表的第一个结点之前再增加一个相同的节点,称为”表头节点“或者”头节点“,其他节点称为”表节点“。头节点的数据域可以不存放任何数据,也可以存放一些特殊数据,如链表的长度。
表节点中第一个节点和最后一个节点分别称为首元节点和尾节点。
3.2建立单链表
建立带头节点单链表的常用方法有两种:头插入建立法和尾插入建立法。前者的节点输入顺序和链表中元素顺序相反,后者则相同
3.2.1用头插法建立链表
链表与顺序表不同,它是一种动态管理的存储结构,链表中每个节点的存储空间是运行时系统根据需求生成的。因此建立单链表从空表开始,每读入一个数据元素则申请一个新节点,将读入的数据存到新节点的数据域中,然后把新节点作为首元结点插到链表的头节点之后,重复上述过程,知道输入结束标志为0为止。可以使用malloc()为新节点动态分配存储空间。
下面是头插法的过程图(直接插入到头节点之后,所以读入顺序和链表中元素顺序相反)
下面是头插法创建单链表的具体算法:
LinkList CreateList_L1(){
LinkList L;
LinkList p;
int x; //设数据元素的类型为int
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL; //建立一个空链表
scanf("%d",&x);
while(x!=0){
p=(LinkList)malloc(sizeof(LNode)); //为新节点分配存储单元
p->data=x;
p->next=L->next;L->next=p; //修改链接关系
scanf("%d",&x);
}
return L;
}
3.2.2用尾插法建立链表
尾插法的基本思想也是从空链表开始,每读入一个数据元素申请一个新节点,将读入的数据存到新节点的数据域中,然后把新节点插入到当前节点的尾节点之后,重复上述过程,知道输入结束标志0为止。又因为每次都需要将新节点插入到单链表的尾部,所以我们可以加入一个指针r来始终指向单链表的未节点。
下面是过程图
下面是尾插法的具体算法
LinkList Create_L2(){
LinkList L,p,r;
int x; //设数据元素为int
r=L=(LinkList)malloc(sizeof(LNode));
L->next=NULL; //建立一个空链表
scanf("%d",&x);;
while(x!=0){
p=(LinkList)malloc(sizeof(LNode));
p->data=x;
p->next=NULL;r->next=p; //修改连接关系(r修改的原尾节点)
r=p; //r指向新的尾节点
scanf("%d",&x);
}
return L;
}
3.3求表长
由于单链表的构成中没有给给出单链表的长度,因此如果需要获取长度需要遍历单链表,对被访问的节点进行计数,最后返回计数值
算法思路:从表头节点开始,依次向下访问并进行计数,知道最后一个节点为止(注意:对于带头节点的单链表,链表的长度不包括头节点)
算法具体如下:
int LinkList_L(){
LinkList p;
int i=0;
p=l; //p指向头节点
while(p->next!=NULL){
i++;
p=p->next;
}
return i;
}
3.4查找操作
链表的遍历和查找不能像顺序表那样随机访问一个节点,而只能从头指针head出发,顺着指针域next逐个节点向后搜索,直到找到所需要的节点或者当链表为空时结束查找。
3.4.1按序号查找
按序号查找是指在长度为n的单链表里查找第i个数据元素
算法思路:从单链表第一个元素节点起,判断当前节点是否为第i个,若是则返回该节点的指针,否则继续向后寻找下一个节点,知道表结束为止。没有第i个节点就返回空
具体算法如下:
LinkList Get_LinkList(LinkList L,int i){
LinkList p=L;
int j=0;
while(p->next!=NULL&&j<i){
p=p->next;
j++;
}
if(j==i) return p;
else return NULL;
}
3.4.2按值查找(即定位)
按值查找是指在单链表中寻找是否存在值为x的元素
算法思路:从单链表第一个元素节点起,判断当前节点的值是否等于x,若是,返回该节点的指针,否则继续后一个,直到表结束为止。找不到时返回空值。
算法具体如下:
LinkList Locate_LinkList(LinkList L,Elemtype x){
LinkList p;
p=L->next;
while(p!=NULL&&p->data!=x){
p=p->next //向后查找
}
return p;
}
3.5插入运算
在单链表中插入新节点可以采用前插入或后插入的方法。
后插法的示意图如下:
采用后插法的具体操作如下(将s插到p后):
s->next=p->next;
p->next=s; //这两步操作的顺序不能交换
前插法的示意图如下:
后插法与前插法不同的是,需要先找到*p的前驱*q,然后再完成在*q后插入*s。假设单链表的头指针为L,具体操作如下:
q=L;
while(q->next!=p){
q=q->next; //找*p的前驱*q
}
s-next=q-next;
q-next=s;
明显可知,后插操作的时间复杂度为O(1),前插为O(n)
下面将演示如何在第i个元素前插入一个新元素x。
LinkList ListInsert(LinkList L,int i,Elemtype x){
LinkList p,s;
int j=0;
p=L;
while(p!=NULL&&j<i-1){
p=p->next;
j++; //寻找第i-1个节点
}
if(p==NULL||j>i-1){
printf("参数i错误");
exit(1); //第i-1个不存在,无法插入
}
s=(LinkList)malloc(sizeof(LNode));
s->data=x;
s->next=p->next;
p-next=s; //插入新节点
return L;
}
3.6删除运算
单链表中的删除操作和插入类似,不需要移动元素,只需要修改元素之间的链接关系。
示意图如下:
若要完成上图的操作,只需要修改*p的指针域,修改语句为
q->next=p->next;
下面将演示如何删除第i个数据节点
LinkList ListDelete(LinkList L,int i,Elemtype &e){
LinkList p,q;
int j=0;
p=L;
while(p->next&&j<i-1){
p=p->next;
j++; //查找第i-1个节点
}
if(p==NULL||j>i+1){
printf("参数i错误");
exit(1); //节点不存在,不能删除
}
q=p->next; //q指向第i个节点
p->next=q->next; //修改链接关系,删除第i个节点
e=q->data; //保存被删除节点的值
free(q); //释放*q
return L;
}
4. 循环链表
单向循环链表是单链表的变形。如果将单链表最后一个节点的指针指向链表的头节点,则使得单链表头尾节点相连,形成一个环形,这样的链表就构成了单循环链表。单循环链表只要知道表中任一节点的位置,就可以搜寻到所有其他节点的值。
单向循环链表和单链表的主要差别在于:判断是否到达队尾的条件不同。在循环链表中,以节点的指针域是否等于表头节点作为判断到达队尾的标志。
5. 双向链表
5.1双向链表的定义
如果我们希望从表中快速找到某节点的前驱节点,我们可以在单链表的每个节点里在增加一个指向其直接前驱的指针域prior,结构如图所示:
双向链表的节点定义如下:
typedef struct DuLNode{
Elemtype data; //数据域
struct DuLNode *prior,*next; //指针域
}DuLNode;
typedef DuLNode *DuLinkList; //指向双向链表节点的指针类型
与单链表类似,双向链表通常也是用头节点标识,可以是双向链表,也可以是双向循环链表。两者的示意图如下
双向链表中的部分操作,比如求长,寻值等与单链表相同,故接下来具体说明插入和删除的实现
5.2双向链表中节点的插入
在双向链表中进行插入,思路与单链表相同,只是由于指针域的不同,故具体操作不同。插入的示意图如下:
具体的操作如下:
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p-prior=s;
指针操作不是唯一的,也不是任一的。只需要满足第一步在第四步之前实现。
5.3双向链表中节点的删除
示意图如下:
删除其中*p节点的具体操作如下:
p->prior->next=p->next;
p->next->prior=p->prior
6. 顺序表和链表的比较
6.1顺序表的优缺点
优点: a. 方法简单
b. 不用为表示各节点间的逻辑关系而增加额外的存储开销
c. 能按照元素序号随机访问
缺点:a. 在插入和删除运算时,平均移动表内一半的元素,对n比较大的顺序表效率较低
b. 事先需要预分配足够大的空间,可能会导致大量闲置
6.2链表的优缺点
优点:插入,删除运算方便
缺点:a. 要占用额外的存储空间,存储密度降低
b. 不是一种随机存取结构,不能随机存取元素