单链表的定义和表示
n
线性表链式存储结构的特点
Ø
用一组
任意的
存储单元存储线性表的数据元素。
l
存储结点的地址
可以连续或不连续
l
即:逻辑上相邻的数据元素在物理上不一定相邻
Ø
为此,对线性表中的每一个数据元素,都需用两部分来存储:
l
一部分用于
存放数据元素
本身的信息,称为
数据域
(Data Field)
;
l
另一部分用于
存放直接前驱或直接后继的地址
(
指针
)
,称为
指针域
(Link Field)
。
n
与链式存储有关的术语
Ø
结点
(Node)
:
数据元素的存储映像
l
由数据域和指针域两部分组成
Ø
链表:
n
个结点通过指针域链结而成的线性表
l
链表的逻辑示意图:
何谓首元结点、头结点和头指针?
Ø
首元结点
l
是指链表中存储第一个数据元素
a
1
的结点
Ø
头结点
l
是在链表的首元结点之前附设的一个结点,其指针域指向首元结点
l
头结点不计入链表长度,其数据域通常不存储信息
Ø
头指针
l
指向链表中第一个结点
(
为
头结点
或
首元结点
)
的指针
如何表示空表?
n
无头结点
Ø
空表:
头指针为
NULL
时
Ø
非空表
n有
头结点
Ø
空表:
当头结点的指针域为空时
Ø
非空表
n在链表中设置头结点有什么好处?
Ø
便于
首元结点
的处理
l
增加头结点后,首元结点的地址保存在头结点的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。
Ø
便于
空表和非空表
的统一处理
l
增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。
l
对于不带头结点的单链表,插入或删除操作需要区分操作的位置是第一个结点还是其他结点。两种情况的操作不同,第一种情况需要更改头指针。
n
单链表的命名
Ø
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名
Ø
若头指针名是
L
,则把链表称为表
L
n指针变量和结点变量的区别
Ø
指针变量
p
:表示结点地址
Ø
结点变量*
p
:表示一个结点
n单链表如何获得后继元素
n1. 初始化单链表
Ø
目标:构造一个空表
Ø
算法步骤:
l
生成新结点作头结点,用头指针
L
指向头结点。
l
头结点的指针域置空。
Status InitList(LinkList &L){
L=new LNode;
L->next=NULL;
return OK;
}
n2. 销毁单链表
Status DestroyList(LinkList &L)
{
LinkList p;
while(L)
{
p=L;
L=L->next;
delete p;
}
return OK;
}
n3. 清空单链表
![](https://i-blog.csdnimg.cn/blog_migrate/73ecdb3f2d0d691725763fe082f3746d.png)
Status ClearList(LinkList L)
{// 将L重置为空表
LinkList p,q;
p=L->next; //p指向首元结点
while(p) //没到表尾
{q=p->next; delete p; p=q;}
L->next=NULL; //头结点指针域为空
return OK;
}
n4. 求单链表的长度
![](https://i-blog.csdnimg.cn/blog_migrate/33866266f3eeabc5281a7f5635b0bc1c.png)
int ListLength(LinkList L)
{ //返回L中数据元素个数
LinkList *p=L->next; //p指向首元结点
int count=0;
while(p){//遍历单链表,统计结点数
++count;
p=p->next;
}
return count;
}
n5. 判断单链表是否为空
bool ListEmpty(LinkList L)
{//若L为空表,则返回true,否则返回false
return L->next==NULL;
}
n6.获取单链表中第i个数据元素的内容
Ø
算法步骤:
l
用指针
p
指向首元结点,用
j
做计数器初值赋为
1
l
从首元结点开始依次顺着链域
next
向后访问,只要指向当前结点的指针
p
不为空,并且没有到达序号为
i
的结点,则循环执行以下操作:
ü
p
指向下一结点,同时计数器
j
加
1
l
当循环执行完成时,如果指针
p
为空,或者计数器
j
大于
i
,说明指定的序号
i
值不合法,取值失败返回
ERROR
;否则取值成功,此时有
j =
i
,
p
所指的结点就是要找的第
i
个结点。
Status GetElem(LinkList L,int i,ElemType &e){
//当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
p=L->next; j=1; //初始化
while(p && j<i)
{//向后扫描,直到p指向第i个元素或p为空
p=p->next; ++j;
}
if(!p || j>i) return ERROR; //第i个元素不存在
e=p->data; //取第i个元素
return OK;
}
算法的平均时间复杂度为O(n)
n7. 查找值为e的数据元素
Ø
算法步骤
l
用指针
p
指向首元结点
l
从首元结点开始依次顺着链域
next
向后访问,只要指向当前结点的指针
p
不为空,并且
p
所指结点的数据域不等于
e
,则循环执行以下操作:
ü
p
指向下一结点
l
循环执行完成后
返回
p
。若查找成功,
p
此时即为目标结点的地址值;若查找失败,
p
的值为
NULL
。
LNode * LocateElem (LinkList L,ElemType e) {
p=L->next;
while(p && p->data!=e)
p=p->next;
return p; //返回L中值为e的数据元素的位置,查找失败返回NULL
}
算法的平均时间复杂度为O(n)
n8.在单链表的第i个位置插入一个数据元素
Ø
目标:
将值为
e
的新结点插入到表的第
i
个结点的位置上,即插入到
a
i-1
与
a
i
之间
Ø
算法步骤
l
找到
a
i-1
存储位置
p
l
生成一个新结点*
s
l
将新结点*
s
的数据域置为
e
l
新结点*
s
的指针域指向结点
a
i
l
令结点*
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(!p||j>i−1) return ERROR; //i大于表长 + 1或者小于1
s=new LNode; //生成新结点*s
s->data=e; //将结点*s的数据域置为e
s->next=p->next; //将结点*s的指针域指向ai
p->next=s;
return OK;
}
算法的平均时间复杂度为O(n)
n9. 删除线性表中第i个数据元素
Ø
目标:将表的第
i
个结点删除,结果放在
e
中
Ø
算法步骤
l
查找结点
a
i-1
,
令指针
p
指向该结点
l
临时保存待删除结点
a
i
的地址在
r
中,以备释放
l
将结点
*p
的指针域指向
a
i
的直接后继结点
l
将
a
i
的值保留在
e
中,释放结点
a
i
的空间
Status ListDelete(LinkList L,int i,ElemType &e){
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; //删除位置不合理
r=p->next; //临时保存被删结点的地址以备释放
p->next=r->next; //改变删除结点前驱结点的指针域
e=r->data; delete r;//释放结点
return OK;
}
算法的平均时间复杂度为O(n)
n
10
创建单链表
Ø
注意,初始化操作创建的是只有一个头结点的空链表。而前述的算法都假定链表中有多个结点。
Ø
那么如何创建包含多个结点的单链表呢?
Ø
根据结点插入位置的不同,创建链表的方法:
l
前插法
l
后插法
前插法:
n
前插法是通过将新结点逐个插入链表的头结点之后来创建单链表。
n
每次首先申请一个新结点,然后将读入的数据存放到新结点的数据域中,最后将新结点插入到头结点之后。
n
算法步骤
Ø
创建一个只有头结点的空链表
Ø
根据待创建链表包含的元素个数
n
,循环
n
次执行以下操作
l
生成一个新结点
*p
l
将读入的数据存放到新结点
*p
的数据域中
l
将新结点
*p
插入到头结点之后
后插法:
n
后插法是通过将新结点逐个插入到链表的尾部来创建链表
n
算法步骤
Ø
创建一个只有头结点的空链表
Ø
尾指针
r
初始化,指向头结点
Ø
根据创建链表包括的元素个数
n
,循环
n
次执行以下操作:
l
生成一个新结点*
p
l
将读入的数据存放到新结点*
p
的数据域中
l
将新结点*
p
插入到尾结点
*r
之后
l
尾指针
r
指向新的尾结点
*p
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->next=NULL;
r->next=p; //将新结点*p插入尾结点*r之后
r=p; //r指向新的尾结点*p
}
}
n链表的优缺点
Ø
优点
l
数据元素的个数可以自由扩充
l
插入、删除等操作不必移动数据,只需修改链接指针
Ø
缺点
l
存储密度小
存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问
n循环链表
n
循环链表
Ø
对于循环链表,从任何一个结点位置开始,都可以找到其他所有结点。
l
单链表做不到。
Ø
循环链表中没有明显的尾端,如何在遍历时避免死循环?
l
单链表的循环条件
p!=NULL
l
循环链表的循环条件
p!=L
n
循环链表
Ø
对于循环链表,有时不给出
头指针
,而是给出
尾指针
Ø
优点:
方便同时找到
首元结点
和
终端结点
Ø
如何找到
头结点、首元结点
和
终端结点?
l
头结点:
rear->next
l
首元结点:
rear->next->next
l
终端结点:
rear
n循环链表的合并
n双向链表
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList
双向链表
n双向链表的插入
Status ListInsert_DuL(DuLinkList L,int i,ElemType e){
// 在带头结点的双向循环链表 L 的第i个位置之前插入元素e.
p=L->next; j=1;
while(p!=L && j<i) { p=p->next; j++; } //在L中确定第i个元素的位置指针p
if((p==L && i!=1) || j>i) return ERROR;
s=new DuLNode;
s->data=e;
s->prior=p->prior; p->prior->next=s;
s->next=p; p->prior=s;
return OK;
}
n
双向链表的删除
顺序表和链表的比较
n有序表的合并
已知线性表A和B中的数据元素按值非递减有序排列,现要求将A和B归并为一个新的线性表C,且C中的数据元素仍按值非递减有序排序。
示例:A=(3,5,8,11),B=(2,6,8,9,11)
合并后:
C =( 2 , 3 , 5 , 6 , 8 , 8 , 9 , 11,11 )
Ø假定线性表 A和B分别由单链表 La 和 Lb 存储
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc)
{ //已知单链表La和Lb的元素按值非递减排列
// 归并La和Lb得到新的单链表Lc,Lc的元素也按值排序非递减排列
pa=La->next; pb=Lb->next; Lc=pc=La; //初始化
while(pa && pb) //将pa 、pb结点按大小依次插入C中
{
if (pa->data<=pb->data)
{ pc->next=pa; pc=pa; pa=pa->next;}
else
{ pc->next=pb; pc=pb; pb=pb->next}
}
pc->next = pa?pa:pb ; //插入剩余部分
delete Lb; //释放Lb的头结点
}
顺序有序表的合并
void MergeList_Sq(List La,List Lb,List &Lc)
{
InitList(Lc);
i=j=1;
La_len=ListLength(La);
Lb_len=ListLength(Lb);
while(i<=La_len&&j<=Lb_len)
{
GetElem(La,i,a[i]);
GetElem(Lb,j,b[j]);
if(a[i]<b[j])
{
ListInsert(Lc,++k,a[i]);
++i;
}
else
{
ListInsert(Lc,++k,b[j]);
++j;
}
}
while(i<=La_len)
{
GetElem(La,i++,a[i]);
ListInsert(Lc,++k,a[i]);
}
while(j<=Lb_len)
{
GetElem(Lb,j++,b[j]);
ListInsert(Lc,++k,b[j]);
}
}
n
一元多项式的运算
n
顺序存储结构表示多项式存在问题
Ø
存储空间分配不灵活
Ø运算的空间复杂度高
因此采用链式存储:
typedef struct PNode
{
float coef;//系数
int expn; //指数
struct PNode *next; //指针域
}PNode,*Polynomial;
多项式创建
① 创建一个只有头结点的空链表。
② 根据多项式的项的个数n,循环n次执行以下操作:
l
生成一个新结点
*
s
;
l
输入多项式当前项的系数和指数赋给新结点
*
s
的数据域;
l
设置一前驱指针
pre
,用于指向待找到的第一个大于输入项指数的结点的前驱,
pre
初值指向头结点;
l
指针
q
初始化,指向首元结点;
l
循链向下逐个比较链表中当前结点与输入项指数,找到第一个大于输入项指数的结点
*
q
;
将输入项结点*s插入到结点*q之前
void CreatePolyn(Polynomial &P,int n)
{//输入n项的系数和指数,建立表示多项式的有序链表P
P=new PNode;
P->next=NULL; //先建立一个带头结点的单链表
for(i=1;i<=n;++i) //依次输入n个非零项
{
s=new PNode; //生成新结点
cin>>s->coef>>s->expn; //输入系数和指数
pre=P; //pre用于保存q的前驱,初值为头结点
q=P->next; //q初始化,指向首元结点
while(q && q->expn<s->expn) //找到第一个大于输入项指数的项*q
{
pre=q;
q=q->next;
} //while
s->next=q; //将输入项s插入到q和其前驱结点pre之间
pre->next=s;
} //for
}
一元多项式相加:
依次比较Pa和Pb所指结点中的指数项,依次比较Pa->expn =、<、>Pb->expn等情况,再决定是将两系数域的数值相加(并判其和是否为0),还是将较低指数项的结点插入到新表c中。
图书信息管理系统
Ø
存储结构定义
typedef struct Book
{
char id[20];//ISBN
char name[50];//书名
float price;//定价
} ElemType;
typedef struct
{//顺序表
ElemType *elem;
int length;
} SqList;
typedef struct LNode
{//链表
ElemType data;
struct LNode *next;
}LNode,*LinkList;
小结:
n
线性表
Ø
一个线性表是
n≥0
个数据元素
a
1
,a
2
,…,a
n
的有限序列。
n
线性表的顺序存储结构
Ø
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。
n
线性表的链式存储结构
Ø
线性表的链式存储结构就是用一组任意的存储单元存储线性表的数据元素。
l
表中每一个数据元素,都由
数据域和指针域
组成。
n
单链表
Ø
指链表中的每一个结点中只包含一个指针域指向直接后继。
n
循环链表
(Circular Linked List)
Ø
将单链表最后一个结点的指针指向链表的表头结点,整个链表形成一个环,从表中任一结点出发都可找到表中其他结点。
n
双向链表
Ø
每一个结点除了数据域外,还包含两个指针域,一个指针(
next
)指向该结点的后继结点,另一个指针(
prior
)指向它的前驱结点。
n
除上述基本概念以外,还应该熟悉:
Ø
线性表的基本操作
(
初始化、插入、删除、查找
)
Ø
顺序存储结构的表示
l
初始化、插入操作、删除操作
Ø
链式存储结构的表示
l
单链表的初始化、单链表的插入、单链表的删除
l
双向循环链表的插入、删除
一、判断题
1.
线性表的逻辑顺序与物理顺序总是一致的。F
2.
线性表的顺序存储表示优于链式存储表示。F
3.
线性表若采用链式存储表示时
,
所有结点的地址可连续可不连续。T
4.
每种数据结构都应具备三种基本运算:插入、删除和搜索。T
5.
线性表的特点是每个元素都有一个前驱和一个后继。F
6.
顺序存储方式插入和删除时效率太低,因此它不如链式存储方式好。 F
二、填空题
1.
线性表(
a
1
,a
2
,
…
,a
n
)有两种存储结构:
(
A
)和(
B
)。(
A
)存储密度较大,(
B
)存储利用率较高,(
A
)可随机存取,(
B
)不可随机存取,(
B
)插入和删除操作比较方便。
●A顺序存储结构
●B链式存储结构
(链表对内存的利用率比较高,无需连续的内存空间,即使有内存碎片,也不影响链表的创建)
2.
在单链表中,删除指针
p
所指结点的后继结点的语句是:( p->next=p->next->next )
3.
带头结点的单循环链表
Head
的判空条件是( Head->next=Head )。
4.
画出下列数据结构的图示:①顺序表 ②单链表 ③双链表 ④循环链表
5.
在一个长度为
n
的顺序表中第
i
个元素(
1<=
i
<=n
)之前插入一个元素时,需向后移动( n-i+1 )个元素。
6.
对于双向链表
,
在两个结点之间插入一个新结点需修改的指针共( 4 )个,单链表为( 2)个。
7.
带头结点的双循环链表
L
中只有一个元素结点的条件是:
( L->next->next==L )
8.
在单链表
L
中,指针
p
所指结点有后继结点的条件是:
(p->next!=null )
9.
当线性表的元素总数基本稳定,且很少进行插入和删除操作,但要求以最快的速度存取线性表中的元素时,应采用( 顺序 )存储结构。