线性表的链接存储结构
- 在链式存储中,每个存储节点不仅包含有所存元素本身的信息(称之为数据域),而且包含有元素之间逻辑关系的信息,即前驱节点包含有后继节点的地址信息,这称为指针域,这样可以通过前驱节点的指针域方便地找到后继节点的位置,提高数据查找速度。
一般地,每个节点有一个或多个这样的指针域。若一个节点中的某个指针域不需要任何节点,则仅它的值为空,用常量NULL表示。 - 由于顺序表中的每个元素至多只有一个前驱元素和一个后继元素,即数据元素之间是一对一的逻辑关系,所以当进行链式存储时,一种最简单也最常用的方法是:
在每个节点中除包含有数据域外,只设置一个指针域,用以指向其后继节点,这样构成的链接表称为线性单向链接表,简称单链表。
由于链表的每个结点带有指针域,从存储密度来讲,这是不经济的。所谓存储密度是指结点数据本身所占的存储量和整个结点结构中所占的存储量之比,即:
存储密度=节点数据本身占用的空间/节点占用的空间总量;
一般地,存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为1,而链表的存储密度小于1。例如,若单链表的结点数据均为整数,指针所占的空间和整数相同,则单链表的存储密度为50%。若不考虑顺序表中的空闲区,则顺序表的存储空间利用率为100%。
单链表的缺点:
在这样的链表中,由于每个节点只包含有一个指向后继节点的指针,所以当访问过一个节点后,只能接着访问它的后继节点,而无法访问它的前驱节点。
另一种可以采用的方法是:
在每个节点中除包含有数值域外,设置有两个指针域,分别用以指向其前驱节点和后继节点,这样构成的链接表称之为线性双向链接表,简称双链表。
一:单链表
1.在单链表中,假定每个节点类型用LinkList表示,它应包括存储元素的数据域,这里用data表示,其类型用通用类型标识符ElemType表示,还包括存储后继元素位置的指针域,这里用next表示。
LinkList类型的定义如下:
typedef struct LNode //定义单链表节点类型
{ ElemType data;
struct LNode *next; //指向后继节点
} LinkList;
2.在单链表中,假定每个节点类型用LinkList表示,它应包括存储元素的数据域,这里用data表示,其类型用通用类型标识符ElemType表示,还包括存储后继元素位置的指针域,这里用next表示。
LinkList类型的定义如下:
typedef struct LNode //定义单链表节点类型
{ ElemType data;
struct LNode *next; //指向后继节点
} LinkList;
3.单链表的运算
①. 插入节点和删除节点操作
插入操作是将值为x的新节点插入到单链表的第i个节点的位置上。先在单链表中找到第i-1个节点,再在其后插入新节点。
单链表插入节点的过程如下图所示。
插入操作语句描述如下:
s->next=p->next;
p->next=s;
特点:只需修改相关节点的指针域,不需要移动节点
②.删除操作是将单链表的第i个节点删去。先在单链表中找到第i-1个节点,再删除其后的节点。删除单链表节点的过程如下图所示。
删除操作语句描述如下:
p->next=p->next->next;
特点:只需修改相关节点的指针域,不需要移动节点。
一般情况下,在删除一个节点后还需要释放其存储空间,实现删除上述b节点并释放其存储空间的语句描述:
q=p->next; /*q临时保存被删除节点*/
p->next=p->next->next; /*从链表中删除节点*/
free(q); /*释放节点q的空间*/
4.建立单链表
先考虑如何建立单链表。假设我们通过一个含有n个数据的数组来建立单链表。建立单链表的常用方法有如下两种:
(1)头插法建表
该方法从一个空表开始,读取字符数组a中的字符,生成新节点,将读取的数据存放到新节点的数据域中,然后将新节点插入到当前链表的表头上,直到结束为止。(链表的节点顺序与逻辑次序相反)
void CreateListF(LinkList *&L,ElemType a[],int n)
{ LinkList *s;
int i;
L=(LinkList *)malloc(sizeof(LinkList));
L->next=NULL;
//创建头节点,其next域置为NULL
for (i=0;i<n;i++)
//循环建立数据节点
{ s=(LinkList *)malloc(sizeof(LinkList));
s->data=a[i];
//创建数据节点*s
s->next=L->next;
//将*s插在原开始节点之前,头节点之后
L->next=s;
}
}
(2)尾插法建表*
头插法建立链表虽然算法简单,但生成的链表中节点的次序和原数组元素的顺序相反。若希望两者次序一致,可采用尾插法建立。该方法是将新节点插到当前链表的表尾上,为此必须增加一个尾指针r,使其始终指向当前链表的尾节点。
void CreateListR(LinkList *&L,ElemType a[],int n)
{ LinkList *s,*r;
int i;
L=(LinkList *)malloc(sizeof(LinkList)); //创建头节点
r=L; //r始终指向尾节点,开始时指向头节点
for (i=0;i<n;i++) //循环建立数据节点
{ s=(LinkList *)malloc(sizeof(LinkList));
s->data=a[i]; //创建数据节点*s
r->next=s; //将*s插入*r之后
r=s;
}
r->next=NULL; //尾节点next域置为NULL
}
5.线性表基本运算在单链表中的实现
(1)初始化线性表InitList(L)
该运算建立一个空的单链表,即创建一个头节点。
void InitList(LinkList *&L)
{ L=(LinkList *)malloc(sizeof(LinkList));
//创建头节点
L->next=NULL;
}
(2)销毁线性表DestroyList(L)
释放单链表L占用的内存空间。即逐一释放全部节点的空间。
void DestroyList(LinkList *&L)
{ LinkList *pre=L,*p=p->next; //pre指向*p的前驱节点
while (p!=NULL) //扫描单链表L
{ free(pre); //释放*pre节点
pre=p; //pre、p同步后移一个节点
p=pre->next;
}
free(pre); //循环结束时,p为NULL,pre指向尾节点,释放它
}
(3)判线性表是否为空表ListEmpty(L)
若单链表L没有数据节点,则返回真,否则返回假。
bool ListEmpty(LinkList *L)
{
return(L->next==NULL);
}
(4)求线性表的长度ListLength(L)
返回单链表L中数据节点的个数。
int ListLength(LinkList *L)
{ int n=0;
LinkList *p=L; //p指向头节点,n置为0(即头节点的序号为0)
while (p->next!=NULL)
{ n++;
p=p->next;
}
return(n); //循环结束,p指向尾节点,其序号n为节点个数
}
(5)输出线性表DispList(L)
逐一扫描单链表L的每个数据节点,并显示各节点的data域值。
void DispList(LinkList *L)
{ LinkList *p=L->next; //p指向开始节点
while (p!=NULL) //p不为NULL,输出*p节点的data域
{ printf("%d ",p->data);
p=p->next; //p移向下一个节点
}
printf("\n");
}
(6)求线性表L中指定位置的某个数据元素
GetElem(L, I , &e)
思路:在单链表L中从头开始找到第i个节点,若存在第i个数据节点,则将其data域值赋给变量e。
bool GetElem(LinkList *L,int i,ElemType &e)
{ int j=0;
LinkList *p=L; //p指向头节点,j置为0(即头节点的序号为0)
while (j<i && p!=NULL) //找第i个节点
{ j++;
p=p->next;
}
if (p==NULL) //不存在第i个数据节点,返回false
return false;
else //存在第i个数据节点,返回true
{ e=p->data;
return true;
}
}
(7)按元素值查找LocateElem(L,e)
思路:在单链表L中从头开始找第1个值域与e相等的节点,若存在这样的节点,则返回位置,否则返回0。
int LocateElem(LinkList *L,ElemType e)
{ int i=1;
LinkList *p=L->next; //p指向开始节点,i置为1
while (p!=NULL && p->data!=e)
{ p=p->next; //查找data值为e的节点,其序号为i
i++;
}
if (p==NULL) //不存在元素值为e的节点,返回0
return(0);
else //存在元素值为e的节点,返回其逻辑序号i
return(i);
}
(8)插入数据元素ListInsert(&L,i,e)
思路:先在单链表L中找到第i-1个节点*p,若存在这样的节点,将值为e的节点*s插入到其后。
bool ListInsert(LinkList *&L,int i,ElemType e)
{ int j=0;
LinkList *p=L,*s; //p指向头节点,j置为0
while (j<i-1 && p!=NULL) //查找第i-1个节点
{ j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个节点,返回false
return false;
else //找到第i-1个节点*p,插入新节点并返回true
{ s=(LinkList *)malloc(sizeof(LinkList));
s->data=e; //创建新节点*s,其data域置为e
s->next=p->next; //将*s插入到*p之后
p->next=s;
return true;
}
}
(9)删除数据元素ListDelete(&L,i,&e)
思路:先在单链表L中找到第i-1个节点*p,若存在这样的节点,且也存在后继节点,则删除该后继节点。
bool ListDelete(LinkList *&L,int i,ElemType &e)
{ int j=0;
LinkList *p=L,*q; //p指向头节点,j置为0
while (j<i-1 && p!=NULL) //查找第i-1个节点
{ j++;
p=p->next;
}
if (p==NULL) //未找到第i-1个节点,返回false
return false;
else //找到第i-1个节点*p
{ q=p->next; //q指向第i个节点
if (q==NULL) //若不存在第i个节点,返回false
return false;
e=q->data;
p->next=q->next; //从单链表中删除*q节点
free(q); //释放*q节点
return true; //返回true表示成功删除第i个节点
}
}
例 有一个带头节点的单链表L={a1,b1,a2,b2,…,an,bn},设计一个算法将其拆分成两个带头节点的单链表L1和L2:
L1={a1,a2,…,an},L2={bn,bn-1,…,b1}
要求L1使用L的头节点。
解:利用原单链表L中的所有节点通过改变指针域重组成单链表L1和L2。由于L1中节点的相对顺序与L中的相同,所以采用尾插法建立单链表L1;由于L2中节点的相对顺序与L中的相反,所以采用头插法建立单链表L2。
void split(LinkList *&L,LinkList *&L1,LinkList *&L2)
{ LinkList *p=L->next,*q,*r1; //p指向第1个数据节点
L1=L; //L1利用原来L的头节点
r1=L1; //r1始终指向L1的尾节点
L2=(LinkList *)malloc(sizeof(LinkList));//创建L2的头节点
L2->next=NULL; //置L2的指针域为NULL
while (p!=NULL)
{ r1->next=p; //采用尾插法将*p(data值为ai)插入L1中
r1=p;
p=p->next; //p移向下一个节点(data值为bi)
q=p->next; //由于头插法修改p的next域,故用q保存*p的后继节点
p->next=L2->next; //采用头插法将*p插入L2中
L2->next=p;
p=q; //p重新指向ai+1的节点
}
r1->next=NULL; //尾节点next置空
}