线性表链式存储结构的特点是: 用一组仁义的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
为了表示数据元素ai与其直接后继数据元素a i+1之间的逻辑关系,对于数据元素ai来说,除了存储其本身的信息之外,话需要存储一个治时期直接后继的信息(即直接后继的存储位置)。这连个部分信息组成数据元素ai的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域称为数据域; 存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成一个链表,即为线性表的链式存储结构。
根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。 其中单链表,循环链表和双向链表用于实现线性表的链式存储结构。
单链表
下图为线性表的单链表存储结构
扩展:
- 头指针是以确定线性表中第一个元素对应的存储位置,一般用于处理数组、链表、队列等数据结构。单链表可以用头指针的名字来命名。单链表中头指针指向第一个结点。
- 链式存储时只要不是循环链表,就一定存在头指针。
整个链表的存取必须从头指针开始进行,头指针指示链表中的第一个结点的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针也为空(NULL).
用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻。由此,这种存储结构为非顺序映像或链式映像。
因为在使用链表时,关心的是它所表示的线性表中数据元素之间的逻辑顺序,并非每个元素在存储器中的实际位置。因此,通常将链表画成用箭头相链接的结点的的序列。 第一张图可以画成下面这种形式
由此可见,单链表可由头指针唯一确定,在C语言中可用“结构指针”来描述:
typedef struct
{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
}LNode,*LinKList; //LinKList为指向结构体LNode的指针类型
这里定义的是单链表中每个结点的存储结构,包括两部分:
- 存储结点的数据域date:其类型用通用类型标识符ElemType表示。
- 存储后继结点位置的指针域next:其类型为指向结点的指针类型LNode*。
通常,用LinkList定义单链表,强调定义的是某个单链表的头指针;用==LNode*==定义指向单链表中任意结点的指针变量。
区分指针变量和结点变量:若定义LinkList p或LNodep,则p为指向某结点的指针变量,表示该结点的地址;而p为对应的结点变量,表示该结点的名称。
一般,为了方便,在单链表的第一结点之前附设一个结点,称之为头节点。第二张图增加头节点后如下图
注:
首元结点:是链表中寻相互的第一个数据元素;
头节点:是在首元结点之前附设的的一个结点,其指针域指向首元结点。(头结点的数据域可以不存储任何信息,也可以存储与数据元素类型相同的其他附加信息)
头指针:是指向链表中的第一个结点的指针。
头结点的作用:
- 首元结点的地址保存在头结点的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需特殊处理。
- 便于空表和非空表的统一处理
增加头结点后,无论链表是否为空,头指针都指向头结点的非空指针,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件为:L->next = = NULL)。
单链表
1.初始化
单链表的初始化就是构造一个空表。
【算法步骤】
- 生成新结点作为头结点。
- 头结点的指针域置空。
【算法描述】
Status InitList(LinkList)
{
L=new LNode; //生成新结点作为头结点,用头指针L指向头结点
L->next = NULL; //头结点的指针域置空
return OK;
}
2.取值
从链表的首元结点出发,顺着链域next逐个结点找下去
【算法步骤】
- 用指针p指向首元结点,用j做计数器初值付给l。
- 从首元结点开始一次顺着链域next向下访问,只要指向当前结点的指针p不为空,并且没有到达序号为i的结点,则循环执行以下操作:
p指向下一个结点;
计数器j相应加i。 - 退出循环时如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0),取值失败返回ERROR;否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。
【算法描述】
Statu GetElem(LinkList L,int i,Elemtype &e)
{
p=L->next:j=i; //初始化,p只指向首元结点,计数器j初赋值为1
while(P&&j<i) //p指向下一个结点
{
p=p->next;
++j; //计数器j相应加1
}
if(p||j>i) return ERROR; //i值不合法i>n或i<=0
e=p->date; //取第i个结点数据域
return OK;
}
[算法分析】
ASL=(n-1)/2
单链表取值算法的平均时间复杂度为O(n)。
3.查找
从链表的首元结点出发,依次将结点值和给定值e进行比较,返回查找结果。
【算法步骤】
- 用指针p指向首元结点;
- 从首元结点开始一次顺着链域next往下找,只要当前的指针域p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。
- 返回p。如查找成功,p此时即为结点的地址值;如查找失败,p的值即为NULL。
【算法描述】
LNode * LocateElem (LinkList L,ElemType e)
{
p=L->next; //初始化,p指向首元结点
while(p G& p->data!=e) //顺链域向后扫描,直到p为空或p所指结点的数据域等于e
p=p->next; //p指向下一个结点
return p; //查找成功返回值为e的结点、地址P,查找失败p为NULL
}
【算法分析】
平均时间复杂度也为O(n)。
4.插入
在两个数据元素a和b之间插入一个数据元素x,已知p为其单链表的存储结构中指向结点a的指针。
【算法步骤】
将值为e的新结点插到第i个结点的位置上,即插入到结点ai-1与ai之间。
- 查找结点ai-1并由指针p指向该结点。
- 生成一个新结点*S。
- 将新结点*s的数据域置为e。
- 将新结点*s的指针城指向结点ai。
- 将结点p的指针域指向新结点s。
【算法描述】
Status ListInsert (LinkList &L,int i,ElemType e)
{
p=L;j=0;
while(P && (j<i-1))
{
p=p->next; ++j;
} //查找第i-1个结点,P指向该结点
if(!p11j>i-1) return ERROR; //i>n+1或者i<1
s=new LNode ; //生成新结点*s
S->data=e; //将结点*s的数据域置为e
s->next-p->next; //将结点*s的指针域指向结点ai
p->next=s; //将结点*p的指针域指向结点*s
return OK;
}
【算法分析】
单链表的插入操作的平均时间复杂度为O(n)。
5.删除
在单链表中删除元素b时,应该首先找到其前驱点a。要想删除元素b,除需要修改a的指针域外,还要释放结点b所占的空间。因此修改指针前,应该引入另一个指针q,临时保存结点b的地址已备释放。
【算法步骤】
- 查找结点ai-1并由指针p指向该结点。
- 临时保存待删除结点ai的地址在q中,以备释放。
- 将结点*p的指针域指向ai的直接后继结点。
- 释放结点ai的空间。
【算法描述】
Status ListDelete (LinkList &L,int i)
{
p=L;j=0;
while ((p->next) &&(j<i-1)) //查找第i-1个结点,p指向该结点
{
p=p->next;
++j;
}
if(!(p->next)||(j>i-1)) return ERROR; //当i>n或i<1时,删除位置不合理
q=p->next; //临时保存被删结点的地址以备释放
p->next=*q->next;//改变删除结点前驱结点的指针域
delete q; //释放删除结点的空间
return OK;
}
【算法分析】
删除算法的平均时间复杂度为O(n)。
6.创建单链表
建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。
前插法
通过将新结点逐个插人链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
【算法步骤】
- 创建一个只有头结点的空链表。
- 根指待创建链表包括的元素个数n,循环次执行以下操作:
生成一个新结点p;
输入元素值赋给新结点p中的数据域;
将新结点*p中插入到头结点之后。
注:因为每次插入在链表的头部,所以应该逆序输入数据,输入顺序和线性表中的逻辑顺序是相反的。
【算法描述】
void CreateList H(LinkList L,int n)
{
L=new LNode ;
L->next=NULL; //先建立一个带头结点的空链表
for(i=0;i<n;++i)
{
p=new LNode; //生成新结点*p
cin>>p->data; //输人元素值赋给新结点*p的数据域
p->next=L->next;L->next=p; //将新结点*p 插人到头结点之后)
}
}
后插法
通过将新结点逐个插人到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读人相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。
【算法步骤】
- 创建一个只有头结点的空链表。
- 尾指针r初始化,指向头结点。
- 根据创建链表包括的元素个数n,循环n次执行以下操作:
生成一个新结点p;
输人元素值赋给新结点“的数据域;
将新结点p 插人到尾结点r之后:
尾指针r指向新的尾结点p
注:读人数据的顺序和线性表中的逻制顺序是相同的。
【算法描述】
void CreateList R(LinkList &L,int n)
{
L=new LNode;
L->next=NULL; //先建立一个带头结点的空链表
r=L; //尾指针r指向头结点
for(i=0;i<n;++i)
{
p=new LNode; //生成新结点
cin>>p->data; //输人元素值赋给新结点*p的数据域
p->next=NULL; r->next=p; //将新结点*p插人尾结点*r之后
r=p; //r指向新的尾结点*p
}
}
循环链表
**特点:**是表中最后一个结点的指针域指向头结点,整个链表形成一个环。因此,从表中的任一结点出发均可找到表中其他结点。
下图为单链的循环链表
循环单链表和单链表的区别:当链表遍历时,判别当时指针p是否指向表尾结点的终止条件不同。 在单链表中,判别条件为p!=NULL或p->next!=NULL;循环单链表的判别条件为p!=L或p->next!=L。
在某些情况下,若在循环链表中设立尾指针而不设头指针,可使一些操作简化。
双向链表
在单链表中,查找直接后继结点的执行时间为O(l),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表。
在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
在C语言中可描述如下;
typedef struct DuLNode
{
ElemType data; //数据域
struct DuLNode *prior; //指向直接前驱
struct DuLNode *next; //指向直接后继
}DuLNode,*DuLinkList;
下面是双向链表的循环表
注:图(b)是只有一个表头结点的空表。
在双向链表中,在插入结点时需要修改四个指针,在删除结点时需要修改两个指针。二者的时间复杂度均为O(n)。
在双向链表中插入和删除结点时的指针变化状况如下图
【算法描述】
双向链表的插入
Status ListInsert DuL(DuLinkList &L,int 1,ElemType e)
{
if(!(p=GetElem DuL(L,i))) //在 L中确定第i个元素的位置指针p
return ERROR; //p为NULL时,第i个元素不存在
s=new DuLNode; //生成新结点*S
s->data=e; //将结点*s数据域置为e
s->prior=p->prior; //将结点*s插入L中,此步对应图中1
p->prior->next=s; //对应图中4
s->next=p; //对应图中2
p->prior=s; //对应图中3
return OK;
}
双向链表的删除
Status ListDelete_ DuL (DuLinkList &L,int i)
{
if(! (p=GetElem_DuL(L,i)) //在L 中确定第i个元素的位置指针p
return ERROR; //p为NULL时,第i个元素不存在
p->prior->next=p->next; //修改被删结点的前驱结点的后继指针,对应图中2
p->next->prior=p->prior; //修改被删结点的后继结点的前驱指针,对应图中3
delete p; // //释放被删结点的空间
return OK;
}