上一篇我们学习了线性表的顺序存储结构:https://blog.csdn.net/black_carbon/article/details/81084932
总结一下:线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中任一元素,它的存储位置可用一个简单、直观的公示来表示。
然而,从另一方面来看,这个特点也铸成了这种存储结构的弱点,在做插入或删除操作时,需移动大量元素。针对这个弱点,我们来学习一下线性表的另一种表示方法——链式存储结构。
(2)线性表的链式表示:线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)
基于链式线性表的这种存储方式,为了表示每个数据元素与其直接后继数据元素的逻辑关系,对于每个数据元素来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据的元素的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域被称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称做指针或链。n个结点的链结成一个链表,即为线性表的链式存储结构。
又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。
线性表的单链表存储结构:
/*
线性表的单链表存储结构
*/
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
整个链表的存取必须从头指针开始进行,头指针指示链表中第一个结点的存储位置。由于最后一个数据元素没有直接后继,则线性链表中最后一个结点的指针为空(NULL).
有时,我们在单链表的第一个结点之前附设一个结点,这个结点称为头结点。头结点的数据域可以不存储任何信息,也可存储如线性表长度等类的附加信息。头结点的指针域存储指向第一个结点的指针。此时,头指针指向头结点。
(头结点与头指针的区别:头结点是附加在第一个结点之前的结点,一般只有指针域存储第一个结点的位置信息,头结点可加可不加。头指针是一个结点指针,当有头结点时,头指针指向头结点;当没有头结点时,头指针指向第一个结点)
注意:第一个结点不是头结点,第一个结点是指存储了数据元素信息的第一个结点,而头结点不存储数据元素信息。
下面我们来看一个实例:获取单链表中第i个元素的值
/*
单链表中获取第i个元素的值
*/
Status GetElem_L(LinkList L, int i, ElemType &e)
{
//L为带头结点的单链表的头指针
//i的合法值为1<=i
if(i < 1)
exit(ERROR);
LinkList p = L->next; //p指向第一个结点,头结点的下一个结点
int k = 1;
while(p && k<i)//顺序查找,直到p为空或者p指向第i个元素
{
p = p->next;
++k;
}
if(p)//p指向第i个元素
{
e = p->data;
return OK;
}
else
return ERROR;
}//GetElem_L
与顺序表不同,单链表需要从第一个结点开始遍历,直到第i个结点,所以这个算法的时间复杂度为O(n)。
在带头结点的单链线性表中第i个位置之前插入元素e:
/*
单链表中在第i个位置插入元素e
*/
Status ListInsert_L(LinkList &L, int i, ElemType e)
{
//在带头结点的单链线性表L中第i个位置之前插入元素e
//i的合法值1<=i<=表长
if(i < 1)
exit(ERROR);
LinkList p = L;
int k = 0;
while(p && k<i-1)//寻找第i-1个结点
{
p = p->next;
++k;
}
if(!p)//到不了i-1
return ERROR;
LinkList s = (LinkList)malloc(sizeof(LNode));//找到i-1
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}//ListInsert_L
在带头结点的单链线性表L中,删除第i个元素,并由e返回其值:
/*
单链表中删除第i个元素
*/
Status ListDelete_L(LinkList &L, int i, ElemType &e)
{
//在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
//i的合法值1<=i<=length
if(i<1)
return ERROR;
int k=0;
LinkList p = L; //指向头结点
while(p->next && k<i-1)//寻找第i个结点,p指向第i-1个结点
{
p = p->next;
++k;
}
if(!p->next)//i>length
return ERROR;
LinkList q = p->next;
e = q->data;
p->next = q->next;
free(q); //释放第i个结点
q = NULL;//防止q成为野指针
return OK;
}//ListDelete_L
不难看出,在单链表中插入和删除的算法时间复杂度为O(n)。
下面是一个从表尾到表头逆向建立单链表的算法,时间复杂度也是O(n):
/*
单链表从表尾到表头逆向建立
*/
void CreateList_L(LinkList &L, int n)
{
//逆位序输入n个元素的值,建立带表头结点的单链线性表L
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;//只含有表头结点的空链表
for(int i=0; i<n; ++n)
{
LinkList p = (LinkList)malloc(sizeof(LNode));//生成新结点
cin>>p->data;
p->next = L->next;//插入表头
L->next = p;
}
}//CreateList_L
下面我们来讨论一下如何将两个有序链表ab并为一个有序链表c:
在顺序表中,我们将两个有序顺序表并为一个有序顺序表采用的是新创建一个表c,将ab两个表的元素依序插入c表的表尾,从而避免了移动表中元素。
然而单链表在任意位置的插入并不需要移动其他数据元素,根据这个特点,我们可以在a表的基础上,将b表的数据依数据元素的大小插入在a表的适当位置,从而将a表改造成c表,实现两个表的合并。
下面是具体的算法实现:
/*
两个有序链表合为一个有序链表
*/
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc)
{
//已知带有头结点的单链线性表La和Lb的元素按值非递减排列,归并得到Lc,也按值非递减排列
//此题可直接把La改成Lc
LinkList pa = La->next;
LinkList pb = Lb->next;//pa,pb都初始指向第一个结点
Lc = La;//将La改造成Lc
LinkList pc = Lc;
while(pa && pb)
{
if(pa->data <= pb->data)//插入pa结点
{
pc = pc->next;//因为是在La上改造Lc,所以插入pa结点只需向后移
pa = pa->next;
}
else
{
pc->next = pb;
pc = pc->next;
pb = pb->next;
}
}
pc->next = (pa) ? pa : pb;//一次性插入剩余的结点
free(Lb);
Lb = NULL;
La = NULL;
}//MergeList_L
静态链表:用一维数组来描述线性链表
/*
线性表的静态单链表存储结构
*/
# define MAXSIZE 1000
typedef struct {
ElemType data;
int cur;//游标,即数组下标
}component, SLinkList[MAXSIZE];
这种描述方法便于在不设“指针”类型的高级程序设计语言中使用链表结构,数组的一个分量表示一个结点,同时用游标代替指针指示结点在数组中的位置。数组的第0分量可看成头结点。链表的尾结点的游标值为0,代表空。
在静态链表中查找第一个值为e的元素:
/*
在静态单链线性表中查找第一个值为e的元素
*/
int LocateElem_SL(SLinkList S, ElemType e)
{
//静态单链表数组下标为0的元素作为链表的头结点,只存第一个结点的数组位序
int i = S[0].cur;//i为第一个结点的游标
while(i && S[i].data!=e)
i = S[i].cur;
return i;//这里的i有两层意思
}//LocateElem_SL
上面算法中“retuen i”,当没找到元素e时,i为链表尾结点的游标值0,此时函数返回0;当找到元素e时,i为当前结点所在数组中的下标,此时函数返回元素在L中的位序。
集合A-B并B-A的静态链表算法:
由终端输入集合元素,先建立表示集合A的静态链表S,而后在输入集合B的元素的同时查找S表,若存在和B相同的元素,则从S表中删除,否则将此元素插入S表。
首先先写出三个算法:
- 将整个数组空间初始化成一个链表
/* 将一维数组连成一个备用链表 */ void InitSpace_SL(SLinkList &space) { //space[0].cur为头指针,指向第一个结点,“0”表示空指针 for(int i=0; i<MAXSIZE-1; ++i) space[i].cur = i+1; space[i].cur = 0; }//InitSpace_SL
- 从备用空间取得一个结点
/* 请求分配结点,查询备用链表 */ int Malloc_SL(SLinkList &space) { //向备用链表space申请分配结点 int i = space[0].cur; if(i) space[0].cur = space[i].cur; return i; }//Malloc_SL
3. 将空闲结点链接到备用链表上
/*
回收空闲结点到备用链表
*/
void Free_SL(SLinkList &space, int k)
{
//将空闲结点插入space到第一个位置
space[k].cur = space[0].cur;
space[0].cur = k;
}//Free_SL
求A-B并B-A算法
/*
在静态链表space中建立集合A和集合B,并求A-B并B-A
*/
void difference(SLinkList &space, int &S)
{
//输入集合A,B,求A-B并B-A
InitSpace_SL(space);
int m, n;
cin>>m>>n;
S = Malloc_SL(space);//申请S的头结点
int p = S;//p指向当前最后一个结点
for(int i=0; i<m; ++i)//输入集合A
{
int k = Malloc_SL(space);//申请结点k
space[p].cur = k;//连接结点k
p = k;//指向k结点
cin>>space[k].data;//输入k结点的值
}
space[p].cur = 0;//尾结点的指针为空
for(int j=0; j<n; ++j)
{
ElemType b;
cin>>b;
int q = S;//指向头结点(前一个结点)
p = space[S].cur;//指向第一个结点(当前结点)
while(space[p].data != b && p != 0)//查找,直到找到或者结束
{
q = p;
p = space[p].cur;
}
if(p == 0)//没找到插入
{
int k = Malloc_SL(space);//申请结点
space[q].cur = k;
space[k].cur = 0;
space[k].data = b;
}
else//找到删除
{
space[q].cur = space[p].cur;
Free_SL(space, p);
}
}
}//difference
循环链表:循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
双向链表:双向链表有两个指针域,一个指向其直接后继,另一个指向其直接前驱,双向链表克服了单链表只能单向查找的缺点。
双向链表的存储结构:
/*
线性表的双向链表存储结构
*/
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;