线性表
知识框架
线性表抽象数据类型定义
线性表的数据对象集合为{a1,a2,a3,a4...an},每个元素的类型均为Datatype。其中,除了第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
线性表的顺序存储结构
定义:线性表的顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素。
一,顺序表
特点:逻辑上相邻的数据元素,物理上也是相邻的;
只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,由于线性表中的每个数据元素类型都相同,所以我们可以用c语言的一位数组来实现顺序存储结构。
1,顺序存储结构代码
//头文件
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 20 //线性表存储空间的初始分配量
#define ok 1 //成功标识
#define error 0 //失败标识
typedef int Status; //Status是函数的类型,其值是函数结果状态代码,如OK等
#define MAXSIZE 20 //存储空间的初始分配量 typedef int ElemType; //定义ElemType类型,这里假定为int类型 typedef struct { ElemType data[MAXSIZE];//数组,存储数据元素; int length; //线性表当前的长度 }SqList;
2.建立一个空的顺序表L
status InitList(SqList*L)
{
L->data=(ElemType*)malloc(MAXSIZE*sizeof(ElemType));
//如果没有开辟内存,返回error
if(L->data==NULL)
{
return error;
}
L->length=0;
return ok;
}
3.顺序表的插入
/*初始条件:顺序线性表L存在
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(SqList* L,int i,ElemType e)
{
int k;
if(L->length==MAXSIZE)//顺序表已经满了
{
return error;
}
if(i<1||i>L->length+1)//i不符合插入的条件
{
return error;
}
if(i<=L->length)//i插入的位置不在表尾
{
for(k=L->length-1;k>=i-1;k--)//
{
L->data[k+1]=L->data[k];//将要插入位置后的元素向后移一位
}
L->data[i-1]=e;//将新的元素插入
L->length++;//表长加1
return ok;
}
4.顺序表的删除操作
/*初始条件:顺序线性表L存在;
操作条件:删除表L的第i个数据元素,并用e返回其值,L的长度减一;*/
Status ListDelete(SqList* L,int i,ElemType* e)
{
int k;
if(L->length==0)//表L为空
return error;
if(i<1||i>L->length)//删除位置不对
return error;
*e=L->data[i-1];
if(i<L->length)//删除元素不是最后一个
{
for(k=i;k<L->length;k++)
{
L->data[k-1]=L->data[k];//将删除位置的后继元素前移
}
}
L->length--;//表L减一
return ok;
}
5.获取元素的操作
/*初始条件:顺序线性表存在
操作条件:用e返回L中第i个数据元素的值,注意i的位置,数组下标从0开始;*/
Status GetElem(SqList* L,int i,ElemType* e)
{
if(i<1||L->length==0||i>L->length)
return error;
*e=L->data[i-1];
return ok;
}
6.打印顺序线性表的所有元素
void OutPut(SqList* L)
{
for(int i=0;i<L->length;i++)
{
printf("%d",L->data[i]);
printf("\n");//打印一个数换行
}
}
小结
1.顺序线性表的时间复杂度
从代码实现过程来说,线性表的顺序存储结构,在读取数据时,时间复杂度为O(1);插入或删除时,时间复杂度为O(n);
2.线性表顺序存储结构的优缺点
优点:无须为表示表中元素之间的逻辑关系而增加格外的存储空间; 可以快速地存取表中任意位置的元素;
缺点:插入和删除操作需要移动大量元素;当线性表长度变化比较大时,很难确定存储空间的容量;造成存储空间的”碎片“
线性表的链式存储结构
在链式结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址。为了我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域;
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,a3,...an)的链式存储结构,链表的每个结点中只包含一个指针域,所以叫单链表;单链表正是通过每个节点的指针域将线性表的数据元素按逻辑次序链接在一起;
有时为了方便对链表进行操作,会在单链表的第一个结点前附设一个节点,称为头结点,此时头指针指向的结点就是头结点。
空链表,头结点的直接后继为空。
假设p是指向线性表第i个数据元素的指针,p->data表示第i个位置的数据域,p->next表示第i+1个位置的指针域,则第p+i个数据元素可表示为:
1.单链表的存储结构
typedef struct Node
{
ElemType data;
struct Node* next;
}Node;
typedef struct Node* LinkList;//定义LinkList
2.单链表的读取
/*初始条件:链式线性表L存在;
操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(LinkList L,int i,ElemType* e)
{
int j;
LinkList p;//声明结点p
p=L->next;//让p指向链表L的第一个结点
j=1;//j为计数器
while(p&&j<i)//p不为空或者计数器还没有等于i时,循环继续
{
p=p->next;//让p指向下一个节点
++j;
}
if(!p||j>i)
{
return error;
}
*e=p->data;//取第i个元素的数据
return ok;
}
3.单链表的插入
假设存储元素e的结点为s,将结点s插入到结点p和结点p->next之间,
s->next=p->next//将p的后继结点赋值给s的后继
p->next=s;//将s赋值给p的后继
代码实现
/*初始条件:链式线性表L存在
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert(LinkList* L,int i,ElemType e)
{
int j;
LinkList p,s;
p=*L;
j=1;
while(p&&j<i)//寻找第i个结点
{
p=p->next;
++j;
}
if(!p||j>i)
return error;
s=(LinkList)malloc(sizeof(Node));//生成新节点(c语言标准函数)
s->data=e;
s->next=p->next;//将p的后继结点赋值给s的后继
p->next=s;//将s赋值给p的后继
return ok;
}
4.单链表的删除
设存储元素ai的结点为q,要实现将结点q删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继指针即可。
p->next=p->next->next;
或者
将q代替p->next
则q=p->next;
p->next=q->next;//将q的后继赋值给p的后继
代码实现
/*初始条件:链式线性表L存在;
操作条件:删除L的第i个数据元素,并用e返回其值,L的长度减一;*/
Status ListDelete(LinkList* L,int i,ElemType* e)
{
int j;
LinkList p,q;
p=*L;
j=1;
while(p->next&&j<i)//遍历寻找第i个元素
{
p=p->next;
++j;
}
if(!(p->next)||j>i)
return error;
q=p->next;
p->next=q->next; //将q的后继赋值给p的后继
*e=q->data;//将q结点中的数据给e
free(q); //让系统回收此结点,释放内存
return ok;
}
5.单链表的整表创建
//随机产生n个元素的值,建立带表头节点的单链表L(头插法)
void CreateListHead(LinkList* L,int n)
{
LinkList p; //声明一指针p和计数器i
int i;
srand(time(0)); //初始化随机数;
*L=(LinkList)malloc(sizeof(Node));//初始化空链表L
(*L)->next=NULL; //先建立一个带头节点的单链表
for(i=0;i<n;i++)
{
p=(LinkList)malloc(sizeof(Node)); //生成新节点
p->data==rand()%100+1;//随机生成100以内的数字
p->next=(*L)->next;
(*L)->next=p; //插入到表头
}
}
//随机产生n个元素的值,建立带表头节点的单链表L(尾插法)
void CreateListTail(LinkList* L,int n)
{
LinkList p,r;
int i;
srand(time(0));
*L=(LinkList)malloc(sizeof(Node));//初始化空链表L
r=*L; //r为指向尾部的结点
for(i=0;i<n;i++)
{
p=(Node*)malloc(sizeof(Node));//生成新节点
p->data=rand()%100+1;//随机产生100以内的数字
r->next=p;//将表尾终端结点的指针指向新节点
r=p;将当前的新节点定义为表尾终端结点
}
r->next=NULL;//表示当前链表结束
}
6,单链表的整表删除
//初始条件:链式线性表L存在
//操作结果:将L重置为空表
Status ClearList(LinkList* L)
{
LinkList p,q; //声明指针p,q
p=(*L)->next; //p指向第一个结点
while(p) //没到表尾,继续循环
{
q=p->next; //将下一个结点赋值给q
free(p); //释放p
p=q; 将q赋值给p
}
(*L)->next=NULL; //头节点指针域为空
return ok;
}
7.遍历打印单链表
void OutPut(LinkList L)
{
Node *p=L->next->next ;
for(int i=0;i<L->lenght;i++)
{
printf("%d ",p->data );
p=p->next ;
}
printf("\n");
}
小结
单链表和顺序存储结构的对比
存储分配方式:
顺序存储结构用一段连续存储单元依次存储线性表的数据元素;
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素;
时间性能:
查找:顺序存储结构O(1);单链表O(n);
插入和删除:顺序存储结构需要平均移动表长一半的元素,时间复杂度为O(n);
单链表在找出位置的指针后,插入和删除时间复杂度为O(1);
空间性能:
顺序存储结构需要预分配存储空间,分大了,浪费,分小了,易发生上溢;
单链表不需要分配,元素个数也不受限制;
若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构;
静态链表
一,静态链表的基本概念
用数组描述的链表叫做静态链表,数组的元素都是由两个数据域组成,data和cur。数据域data用来存放数据元素,cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。
静态链表的定义
为了方便插入数据,我们通常会把数组建立得大一些,以便有空间插入时不至于溢出.
#define MAXSIZE 1000//存储空间初始分配量
typedef struct
{
ElemType data;
int cur; //游标(cursor),为0时表示无指向
}Component,StaticLinkList[MAXSIZE];
在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现,然而静态链表中,操作的是数组,所以我们需要自己实现这两个函数,才可以完成插入和删除的操作。了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新节点。
代码实现
//如果备用空间链表非空,则返回分配的结点下标,否则返回0;
int Malloc_ssl(StaticLinkList space)
{
int i=space[0].cur; //当前数组第一个元素的cur存的值
if(space[0].cur) //由于要拿出一个分量来使用了,所以我们就要拿它的下一个分量用来备用
space[0].cur=space[i].cur;
return i;
}
静态链表的插入操作
例如:在乙和丁中插入丙
/**
* 得到静态列表的长度
* 初始条件:静态列表L已存在。操作结果:返回L中数据元素的个数
*/
int ListLength(StaticLinkList L){
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i){
i = L[i].cur;
j++;
}
return j;
}
/**
* 在L中第i个元素之前插入新的元素e
*/
Status ListInsert(Component *L, int i, ElemType e){
int j,k,l;
k = MAXSIZE - 1; //注意k首先是最后一个元素的下标
if(i<1 || i>ListLength(L) + 1){
return ERROR;
}
j = Malloc_SLL(L);
if(j){
L[j].data = e; //将数据赋值给此分量的data
for(l=1; l<= i-1; l++){
k = L[k].cur; //找到第i个元素之前的位置
}
L[j].cur = L[k].cur; //把第i个元素之前的cur赋值给新元素的cur
L[k].cur = j; //把新元素的下标赋值给第i个元素之前元素的cur
return OK;
}
return ERROR;
}
静态链表的删除操作
删除甲操作
/**
* 删除在L中第i个数据元素e
*/
Status ListDelete(Component *L, int i){
int j,k;
if(i<1 || i>ListLength(L)+1){
return ERROR;
}
k = MAXSIZE - 1;
for(j=1; j<=i-1; j++){
k = L[k].cur; //找到第i个元素之前的位置
}
j = L[k].cur;
L[k].cur = L[j].cur;
OUTPUT(L);
Free_ssl(&L, j);
return OK;
}
循环链表
1、循环链表的基本概念
将单链表中终端节点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表带有头结点的空链表如下图所示:
循环链表和单链表的差异在循环的判断条件上,单链表是判断p->next是否为空,循环链表则是p->next不等于头节点,则循环继续。
从上图可以看到,终端节点用尾指针rear指示,则查找终端节点是O(1),而开始节点,其实就是rear->next->next,其时间复杂度也是O(1)。
举个程序的例子,要将两个循环链表合成一个表时,有了尾指针就非常简单了。比如下面的这两个循环链表,它们的尾指针分别是rearA和rearB。
实现代码
p=rearA->next; //保存A表的头节点,即第一步
rearA->next=rearB->next->next; //将本是指向B表的第一个结点(不是头节点),赋值给rearA->next,即第二步
q=rearB->next;
rearB->next=p; //将原A表的头节点赋值给rearB->next,即第三步
free(q); //释放q
双向链表
1.双向链表的概念
双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
//线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DuLNode* prior;/直接前驱指针
struct DuLNode* next;//直接后继指针
}DulNode,*DuLinkList;
链表示意图如下所示:
双向链表中,对于链表中的某一个结点p,它的后继的前驱以及它的前驱的后继都是它自己,即:
p->next-prior = p = p->prior-next
2、双向链表的插入操作
在双链表中p所指的结点之后插入结点*s,其指针的变化过程如下图所示:
//第一步:把p赋值给s的前驱
s->prior = p;
//第二步:把p->next赋值给s的后继
p->next = p->next
//第三步:把s赋值给p->next的前驱
p->next->prior = s;
//第四步:把s赋值给p的后继
p->next = s;
3、双向链表的删除操作
如果要删除q结点,只需下面两步:
//第一步
p->next = q->next;
//第二步
q->next->prior = p;
free(q);