声明:文章内容源于严蔚敏、吴伟民《数据结构第二版(C语言版)》,仅供学习参考使用。
顺序存储结构的特点是,逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中任一元素。缺点是,插入或删除时需要大量移动元素。
链式存储结构则没有顺序存储结构所具有的弱点,但同时也失去了顺序表可随机存取的优点。
1.单链表的定义和表示
1.1-单链表的定义
线性表的链式存储结构的特点是,用一组 任意的 存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
链表中,数据元素之间的逻辑关系是由节点中的指针指示的,也即指针为数据元素之间的逻辑关系的映像,则逻辑上相邻的两个数据元素存储的物理位置不要求紧邻。因此,这种存储结构为非顺序映像或链式映像。
因此,为了表示每个数据元素 ai 与其后继数据元素 ai+1 之间的逻辑关系,对数据元素 ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息。这两部分信息组成数据元素 ai 的存储映像,称为 节点 。它包括两个域:1)数据域,存储数据元素信息的域;2)指针域,存储其直接后继存储位置的域。指针域中存储的信息称为 指针 或 链 。
n个节点的存储映像链结成一个 链表 ,即为线性表的链式存储结构。由于链表中每个节点只包含一个指针域,故又称 线性链表 或 单链表。
注:根据链表节点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表多用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
1.2-单链表的表示
图2.7所示为线性表的单链表存储结构,整个链表的存取必须从 头指针 开始进行,头指针指示链表中第一个结点(即第一个数据元素的存储映像)的存储位置。同时由于最后一个数据元素没有直接后继,则线性链表中最后一个结点的指针为“空”(NULL)。

通常将链表画成用箭头相链接的节点的序列,节点之间的箭头表示链域中的指针。图2.7所示的单链表可画成如图2.8所示的形式,这是因为在使用链表时,关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。

由上述可见,单链表可由头指针唯一确定,在C语言中可用“结构指针”来描述。
// 线性表的单链表存储结构
typedef struct LNode{
ElemType data; //节点的数据域
struct LNode *next; //节点的指针
}LNode,*LinkList; //LinkList为指向结构体LNode的指针类型
说明:
(1)这里定义的是单链表中每个节点的存储结构,它包括两部分 :存储节点的数据域data,其类型用通用类型标识符 ElemType 表示,存储后继节点位置的指针域 next,其类型为指向节点的指针类型 LNode *。
(2)为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList 与LNode *,两者本质上是等价的。通常习惯上用 LinkList 定义单链表,强调定义的是某个单链表的头指针 ;用 LNode * 定义指向单链表中任意节点的指针变量。例如,若定义 LinkList L,则 L 为单链表的头指针,若定义 LNode *p,则 p 为指向单链表中某个节点的指针,用 *p 代表该节点。当然也可以使用定义 LinkList p,这种定义形式完全等价于 LNode *p。
(3)单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。若头指针名是 L,则简称该链表为表 L。
(4)注意区分指针变量和节点变量两个不同的概念,若定义 LinkList p 或 LNode *p,则 p 为指向某节点的指针变量,表示该节点的地址 ;而 *p 为对应的节点变量,表示该节点的名称。
一般情况下,为了方便处理,在单链表的第一个节点之前附设一个节点,称之为 头节点 。头结点的数据与可以不存放任何信息,也可以存放线性表的附加信息(如链表长度)。图2.8的单链表增加头节点后如图2.9所示。

易错辨析(首元节点、头节点、头指针)
(1)首元节点是指链表中 存储第一个数据元素 a1 的节点。如图 2.8 或图 2.9 所示的节点“ZHAO”。
(2)头节点是在首元节点之前附设的一个节点,其指针域指向首元节点。头节点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整型时,头节点的数据域中可存放该线性表的长度。
(3)头指针是指向链表中第一个节点的指针。若链表设有头节点,则头指针所指节点为线性表的头节点 ;若链表不设头节点,则头指针所指节点为该线性表的首元节点。
1.3-链表增加头节点的作用
(1)便于首元节点的处理
增加了头节点后,首元节点的地址保存在头节点(其“前驱”节点)的指针域中,则对链
表的第一个数据元素的操作与对其他数据元素的操作相同,无须进行特殊处理。若没有头结点,则在插入或删除时,需要判断操作是否发生在第一个数据元素,若是,则需要修改头指针。
(2)便于空表和非空表的统一处理
当链表不设头节点时,假设L为单链表的头指针,它应该指向首元节点,则当单链表为长度n为0的空表时, L指针为空(判定空表的条件可记为:L = = NULL)。
增加头节点后,无论链表是否为空,头指针都是指向头节点的非空指针。如图2.10(a)所示的非空单链表,头指针指向头节点。若为空表,则头节点的指针域为空(判定空表的条件可记为:L −>next = = NULL),如图2.10(b)所示。

2-单链表基本操作的实现
2.1-初始化
单链表的初始化操作就是构造一个如图2.10(b)所示的空表。
【算法步骤】
① 生成新节点作为头节点,用头指针L指向头节点。
② 头节点的指针域置空。
Status InitList(LinkList &L)
{//构造一个空的单链表L
L=new LNode; //生成新节点作为头节点,用头指针L指向头节点
L->next=NULL; //头节点的指针域置空
return OK;
}
2.2-取值
链表中逻辑相邻的节点并没有存储在物理相邻的单元中,因此,对于给定的节点位置序号 i,只能从链表的首元节点出发,顺着链域next逐个节点向下访问。
【算法步骤】
① 用指针p指向首元节点,用j做计数器初值赋为1。
② 从首元节点开始依次顺着链域next向下访问,只要指向当前节点的指针p不为空(NULL),并且没有到达序号为i的节点,则循环执行以下操作:
▷ p指向下一个节点;
▷ 计数器j相应加1。
③ 退出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0),取值失败返回ERROR;否则取值成功,此时j = i时,p所指的节点就是要找的第i个节点,用参数e保存当前节点的数据域,返回OK。
Status GetElem(LinkList L,int i,ElemType &e)
{//在带头节点的单链表L中根据序号i获取元素的值,用e返回L中第i个数据元素的值
p=L->next;j=1; //初始化,p指向首元节点,计数器j初值赋为1
while(p && j<i) //顺链域向后查找,直到p为空或p指向第i个元素
{
p=p->next; //p指向下一个节点
++j; //计数器j相应加1
}
if(!p||j>i)return ERROR; //i值不合法i>n或i<=0
e=p->data; //取第i个节点的数据域
return OK;
}
算法的基本操作是比较 j 和 i 并后移指针p,while循环体中的语句频度与位置 i 有关。若 1≤i≤n,则频度为 i − 1,一定能取值成功;若 i >n,则频度为n,取值失败。因此最坏时间复杂度为O(n)。平均时间复杂度也为O(n)。
2.3-按值查找
链表中按值查找的过程和顺序表类似,从链表的首元节点出发,依次将节点值和给定值e进行比较,返回查找结果。
【算法步骤】
① 用指针p指向首元节点。
② 从首元节点开始依次顺着链域next向下查找,只要指向当前节点的指针p不为空,并且p所指节点的数据域不等于给定值e,则循环执行以下操作:p指向下一个节点。
③ 返回p。若查找成功,p此时指向节点的地址值,若查找失败,则p的值为NULL。
LNode *LocateElem(LinkList L,ElemType e)
{//在带头节点的单链表L中查找值为e的元素
p=L->next; //初始化,p指向首元节点
while(p && p->data!=e) //顺链域向后查找,直到p为空或p所指节点的数据域等于e
p=p->next; //p指向下一个节点
return p; //查找成功返回值为e的节点地址p,查找失败p为NULL
}
该算法的执行时间与待查找的值e相关,其平均时间复杂度也为O(n)。
2.4-插入
假设要在单链表的两个数据元素a和b之间插入一个数据元素x,已知p为其单链表存储结构中指向节点a的指针,如图2.11(a)所示。

为插入数据元素x,首先要生成一个数据域为x的节点,然后将之插入单链表中。根据插入操作的逻辑定义,还需要修改节点a中的指针域,令其指向节点x,而节点x中的指针域应指向节点b,从而实现3个元素a、b和x之间逻辑关系的变化。插入后的单链表如图2.11(b)所示。假设s为指向节点x的指针,则上述指针修改用语句描述即为:
s->next = p->next; p->next = s;
【算法步骤】
将值为e的新节点插入表的第i个节点的位置,即插入节点ai−1与ai之间,具体插入过程如图2.12所示,图中对应的5个步骤说明如下。
① 查找节点ai−1并由指针p指向该节点。
② 生成一个新节点s。
③ 将新节点s的数据域置为e。
④ 将新节点s的指针域指向节点ai。
⑤ 将节点p的指针域指向新节点*s。

Status ListInsert(LinkList &L,int i,ElemType e)
{//在带头节点的单链表L中第i个位置插入值为e的新节点
p=L;j=0;
while(p && (j<i −1))
{p=p->next;++j;} //查找第i−1个节点,p指向该节点
if(!p||j>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)。这是因为,为了在第i个节点之前插入一个新节点,必须首先找到第i −1个节点。
注:和顺序表一样,如果表中有 n 个节点,则插入操作中合法的插入位置有 n+1 个,即1 ≤ i ≤ n+1。当 i=n+1 时,新节点则插在链表尾部。
2.5-删除
要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱节点。如图2.13所示,在单链表中删除元素b时,应该首先找到其前驱节点a。为了在单链表中实现元素a、b和c之间逻辑关系的变化,仅需修改节点a中的指针域即可。假设p为指向节点a的指针,则修改指针的语句为:
p->next = p->next->next;

但在删除节点b时,除了修改节点a的指针域外,还要释放节点b所占的空间,所以在修改指针前,应该引入另一指针q,临时保存节点b的地址以备释放。
【算法步骤】
删除单链表的第i个节点ai的具体过程如图2.14所示,图中对应的4个步骤,说明如下。
① 查找节点ai−1并由指针p指向该节点。
② 临时保存待删除节点ai的地址在q中,以备释放。
③ 将节点*p的指针域指向ai的直接后继节点。
④ 释放节点ai的空间。

Status ListDelete(LinkList &L,int i)
{//在带头节点的单链表L中,删除第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)。
说明:删除算法中的循环条件(p->next&&j<i-1)和插入算法中的循环条件 (p&&(j<i−1)) 是有所区别的。因为插入操作中合法的插入位置有 n+1 个,而删除操作中合法的删除位置只有 n 个,如果使用与插入操作相同的循环条件,则会出现引用空指针的情况,使删除操作失败。
2.6-创建单链表
链表和顺序表不同,它是一种动态结构。整个可用存储空间可为多个链表共同享用,每
个链表占用的空间不需预先分配划定,而是由系统按需即时生成。因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素节点,并逐个插入链表。
根据节点插入位置的不同,链表的创建方法可分为前插法和后插法。
(1)前插法
前插法是通过将新节点逐个插入链表的头部(头节点之后)来创建链表,每次申请一个新节点,读入相应的数据元素值,然后将新节点插入到头节点之后。时间复杂度为O(n)
【算法步骤】
① 创建一个只有头节点的空链表。
② 根据待创建链表包括的元素个数n,循环n次执行以下操作:
生成一个新节点p;
输入元素值赋给新节点p的数据域;
将新节点*p插入到头节点之后。
图2.15所示为线性表(a,b,c,d,e)前插法的创建过程,因为每次插入在链表的头部,所以应该逆位序输入数据,依次输入e、d、c、b、a,输入顺序和线性表中的逻辑顺序是相反的。

void CreateList_H(LinkList &L,in
{//逆位序输入n个元素的值,建立带表头节点的单链表L
L=new LNode;
L->next=NULL;//先建立一个带头节点的空链表
for(i=0;i<n;n;++i)
{
p=new LNode; //生成新节点*p
cin>>g->data; //输入元素值赋给新节点*p的数据域
p->next=L->next;L->next=p; //将新节点*p插入到头节点之后
}}
(2)后插法
时间复杂度为O(n)。
【算法步骤】
① 创建一个只有头节点的空链表。
② 尾指针r初始化,指向头节点。
③ 根据创建链表包括的元素个数n,循环n次执行以下操作:
生成一个新节点p;
输入元素值赋给新节点p的数据域;
将新节点p插入尾节点r之后;
尾指针r指向新的尾节点*p。
图2.16所示为线性表(a,b,c,d,e)后插法的创建过程,读入数据的顺序和线性表中的逻辑顺序是相同的。

void CreateList_R(LinkList &L,int n)
{//正位序输入n个元素的值,建立带表头节点的单链表L
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
}
}
1052

被折叠的 条评论
为什么被折叠?



