第二章线性表--内容简介
本章将进入线性结构的学习。
线性结构是最简单、最常用的一种数据结构。
本章将学习线性表的定义、顺序和链式两种存储方式及相应存储结构上的运算实现。通过典型示例训练,掌握线性表的处理技术。在此基础上,将进一步总结提高,归纳相关技术特点,通过典型问题的解决过程贯穿线性表应用技术。
-
线性表的定义
-
线性表的顺序存储
-
线性表的链式存储
-
典型示例:一元多项式的表示及相加
-
顺序表与链表综合比较
-
总结与提高(技术点和典型问题处理)
第1讲线性表的概念--内容简介
本讲内容包括以下三点:
1、线性结构的特点
线性结构是最简单、最基本的结构,数据元素间是一一对应关系。
2、线性表定义
是由n个数据元素的有限序列。除第一个和最后一个元素以外,其余的每个元素都只有唯一的直接前驱和直接后继。
3、线性表抽象数据类型定义
线性表ADT包括抽象数据类型的名称及数据元素、结构关系、基本操作集合三部分。
第1讲线性表的概念
线性表是由n个类型相同数据元素的有限序列,记作: (ai,a2,... aj,alj+1,... an ),n是线性表长度
n=0时称为空表。
n>0,除第一个元素无直接前驱、最后一个元素无直接后继外,其余的每个数据元素只有一个直接前驱和一个直接后继,数据元素之间具有一对一的关系。
o-o-o-o-o-o图2.1线性表的逻辑结构
根据定义分析得出线性表的特点:
(1〉同一性:属于同一数据对象
(2)有穷性:有限个数据元素
(3)有序性:元素存在序偶关系
学习了线性表的逻辑结构,接下来给出线性表抽象数据类型ADT的定义。
线性表ADT包括抽象数据类型的名称LinearList,及数据元素、结构关系、基本操作集合三部分,
其中数据元素定义域为同一数据对象,结构关系为线性关系,
运算集合包括初始化、销毁、置空、判空、求长度、定位、存取、插入、删除元素9种基本运算。
1、抽象数据类型的使用:
由于抽象数据类型定义了相应模型上的基本运算集,可如同使用整型类型的加、减、乘、除运算集合一样,
只要列出线性表抽象数据类型LinearList,就可直接使用其上的基本运算集。
2、抽象数据类型的作用:
在实际问题中可利用线性表抽象数据类型的9种基本运算的组合实现对线性表进行合并、分拆、复制、排序等多种需求。
示例:要求将一个整数表拆分成一个奇数表和一个偶数表,就会从头逐一比较表中元素个数的奇偶性,决定插入到哪个表中,并需要测试表中元素是否比较完毕。
本节小结:
本节包括3点
1、线性结构的特点
线性结构是最简单、最基本的结构,数据元素间是一一对应关系。
2、线性表定义
是由n个数据元素的有限序列。除第一个和最后一个元素以外,其余的每个元素都只有唯一的直接前驱和直接后继。
3、线性表抽象数据类型定义
线性表ADT包括抽象数据类型的名称及数据元素、结构关系、基本操作集合三部分。
线性表抽象数据类型LinearList一经定义,其基本运算集就可直接引用,选择线性表基本运算的组合可以解决更为复杂的实际问题需求。
作业
线性表是具有n个( )的有限序列(n>0)
-
A.数据对象
-
B.数据元素
-
C.字符
-
D.数据项
B
线性表是一个( )。
-
A.有限序列,可以为空
-
B.有限序列,不可以为空
-
C.无限序列,可以为空
-
D.无限序列,可以为空
A
线性表的特点是每个元素都有一个前驱和一个后继。()
-
A.✓
-
B.×
B
第2讲线性表顺序存储--内容简介
本讲主要学习线性表的顺序存储结构,包括顺序存储的结构和其上的基本运算。主要包括:
1、顺序存储结构——一组连续单元(C语言的数组)依次存放线性表中的元素。
2、基本运算:查找、插入、删除运算在顺序表上的实现。
第⒉讲线性表的顺序存储与基本操作
线性表是1对1的逻辑关系,要存放在计算机中,可采用顺序、链式两种存储结构。
本讲学习线性表的顺序存储结构,包括顺序存储结构定义和其上的基本运算。
一、顺序表的类型定义
线性表的顺序存储简称为顺序表:
用一组地址连续的存储单元依次存储线性表中的各个元素逻辑上相邻的数据元素在物理上也相邻
因此:将顺序表归纳为:关系线性化,结点顺序存。给出线性表存储的线性公式:
假设线性表中有n个元素,每个元素占k个单元,第一个元素的地址为loc(a)则可通过如下公式计算出第i个元素的地址loc(ai):
loc(ai) =loc(a1)+(i-1)×k 其中loc(a1)称为基地址。
借助c语言的数组类型,来表示顺序表:
#define MAXSIZE 100/*此处的宏定义常量表示线性表可能达到的最大长度*/
#define ElemType int
typedef struct{
ElemType elem[MAXSIZE];/*线性表占用的数组空间*/
int last;/*记录线性表中最后一个元素在数组elem[ ]中的位置(下标值),空表置为-1*/
}SeqList;
二、顺序表基本运算
查找,可以有两种:按序号查找和按内容查找。
按序号查找GetData(L,i):查找线性表L中第i个数据元素。
根据顺序表L的存储特性,表中元素在L的elem数组中顺序存放,故GetData(L,i)等同于L.elem[i-1]。
按内容查找Locate (L,e)∶要求查找线性表L中与给定值e 相等的数据元素。
若在表L中找到与e相等的元素,则返回该元素在表中的序号;
若找不到,则返回一个“空序号”标识如-1。
【算法思想】
查找运算可采用顺序查找法实现,即从第一个元素开始,依次将表中元素与e相比较,若相等,则查找成功,返回该元素在表中的序号;
若e与表中的所有元素都不相等,则查找失败,返回-1。
【算法描述】线性表的查找运算
int Locate(SeqList L,ElemType e)
/*在顺序表L中依次存放着线性表中的元紊,在表中查找与e相等的元紊,若L.elem[i]=e,则找到该元紊,并返回i+1,若找不到,则返回“-1”*/
{
i = 0;/*i为扫描计数器,初值为0,即从第一个元素开始比较*/
while ((i<= L.last) && (L.elem[i] != e))/*顺序扫描表,直到找到值为key 的元紊,i++;或扫描到表尾而没找到*/
if(i<=L.last)
return(i+1);/*若找到值为e的元素,则返回其序号*/
else
return(-1);/*若没找到,则返回空序号*/
}
/*算法的时间复杂度为O(n)*/
插入操作
线性表的插入运算是指在表的第i(1≤i≤n+1)个位置前插入一个新元素e,
使长度为n的线性表(e,…,e.1,e,…,e.)变成长度为n+1的线性表(ej,"e.1,e,e,…,en)(其中n为L的表长度)。
【算法思想】
用顺序表作为线性表的存储结构时,由于结点的物理顺序必须和结点的逻辑顺序保持一致,
因此必须将原表中位置n,n-1,…,i上的结点,依次后移到位置n+1,n,…",i+1上,空出第i个位置,然后在该位置上插入新结点e。
当i=n+1时,是指在线性表的末尾插入结点,所以无需移动结点,直接将e插入表的末尾即可。
例如,已知线性表(4,9,15,28,30,30,42,51,62),需在第4个元素之前插入一个元素“21”。则需要将第9个位置到第4个位置的元素依次后移一个位置,然后将“21”插入到第4个位置,如图2.3所示。
伪代码
/*在顺序表L中第i个数据元素之前插入一个元素e。 插入前表长n=L->last+1,
i的合法取值范围是 1≤i≤L->last+2 */
int InsList(SeqList *L,int i,ElemType e)
{
int k;
if((i<1) || (i>L->last+2)) /*首先判断插入位置是否合法*/
{
printf("插入位置i值不合法");
return(ERROR);
}
if(L->last>= MAXSIZE-1)
{
printf("表已满无法插入");
return(ERROR);
}
for(k=L->last;k>=i-1;k--) /*为插入元素而移动位置*/
L->elem[k+1]=L->elem[k];
L->elem[i-1]=e; /*在C语言数组中,第i个元素的下标为i-1*/
L->last++;
return(OK);
}
【算法分析】当在表尾(i=L->last+2)插入元素时,因为循环的终值大于初值,此时不需要移动元素,
可直接在表尾插入e。当在表头(i=1)插入时,移动元素的语句L->elem[k+1]=L->elem[k]需要执行n次,
即将表中已存在的n个元素依次后移一个位置才能将e插入。因此,语句L->elem[k+1]=L->elem[k]的语句执行频度与插入位置i有关。
设为在长度为n的表中插入一元素所需移动元素的平均次数,假设P为在第i个元素之前插入元素的概率,并假设在任何位置上插入的概率相等,
即 P=1/(n+1),i=1,2,…,n+l,则有:
删除操作
线性表的删除运算是指将表的第i(1≤i≤n)个元素删去,使长度为n的线性表(el,",ei-1,ei,ei+1,…,en),变成长度为n-1的线性表(el,…;,ei-l,ei+1,…,en)。
【算法思想】
用顺序表作为线性表的存储结构时,由于结点的物理顺序必须和结点的逻辑顺序保持一致,因此当需要删除第i个元素时,
必须将原表中位置在i+1,+2,…,n-1,n上的结点,依次前移到位置i,i+1,…n-1。(其中n为L的表长度)
例如,线性表(4,9,15,21,28,30,30,42,51,62)删除第5个元素,则需将第6个元素到第10个元素依次向前移动一个位置,如图2.4所示。
伪代码
int DelList(SeqList *L,int i,ElemType *e)
/*在顺序表L中删除第i个数据元素,并用指针参数e返回其值。i的合法取值为1≤i≤L.last+1 */
{
int k;
if((i<1)||(i>L->last+1))
{
printf("删除位置不合法!");
return(ERROR);
}
*e = L->elem[i-1]; /* 将删除的元素存放到e所指向的变量中*/
for(k=i; i<=L->last; k++)
L->elem[k-1] = L->elem[k]; /*将后面的元素依次前移*/
L->last--;
return(OK);
}
【算法分析】与插入运算类似,在顺序表上实现删除运算也必须移动结点,这样才能维持线性表结点间的逻辑关系。当删除表尾(i=L->last+1)元素时,因为循环变量的初值大于终值,此时不需要移动元素,仅将表长度减Ⅰ即可。当删除表头元素(i=1)时,移动元素的语句L->elem[k+1]=L->elem[k]需执行n-1次。因此删除算法中移位语句L->elem[k-1]= L->elem[k]的执行频度与删除位置i有关
设Eae为删除一个元素所需移动元素的平均次数,Q为删除第i个元素的概率,并假设在任何位置.上删除的概率相等,即 O=1/n,,i=1,2,…,n。则有:
由以上分析可知,在顺序表中插入和删除一个数据元素时,其时间主要耗费在移动数据元素上。作一次插入或删除平均需要移动表中一半元素,当n较大时算法效率较低。
作业
若长度为n的线性表采用顺序存储结构,在其第i个位置插入一个新元素的算法的时间复杂度为( )(1<=i<=n+1)。
-
A.O(1)
-
B.O(n)
-
C.O(n*n)
-
D.O(
)
B
若长度为n的线性表采用顺序存储结构,删除第i个位置的元素,需要移动的元素个数为( )。
-
A.i
-
B.n-i
-
C.n-i+1
-
D.n-i-1
B
第3讲线性表顺序结构应用举例及小结--内容简介
本讲主要内容:
-
通过实例:两个递增有序的顺序表的合并问题来进一步对顺序表的存储特点深刻理解并综合运用。
-
顺序存储结构的优缺点分析
第 3 讲 线性表顺序结构应用举例及小结
【顺序表应用举例】有序表合并问题
下面来看合并算法的要求。有两个顺序表 LA 和 LB,其元素均为递增有序排列,编写一个算法,将它们合并成一个递增有序顺序表 LC。
例如一个表是(1, 3, 5), 另一个表是(2,4,6)将他们合并成表(1, 2, 3, 4, 5,6)。
【算法思想】
1)初始化:LC 为空表,设 LC 表的指示器 k=0
设两个指示器 i,j 分别指向表 LA 和 LB 的当前位置,初值均为 0。
2)比较循环:LA 表和 LB 表的当前元素比较,小的元素进 LC 表,且该表的指示器和 LC 表的指示器 k 均加 1 移向下一个位置。如此下去,直到 LA 或 LB 表中一个表处理完毕为止。
3)复制循环:将未处理完的表中剩余元素通过循环逐一复制到 LC 表中。
【算法描述】
void merge(SeqList *LA, SeqList *LB, SeqList *LC)
{
int i,j,k;
i=0;j=0;k=0;
while(i<=LA->last&&j<=LB->last)
if(LA->elem[i]<=LB->elem[j])
{
LC->elem[k]= LA->elem[i];
i++;
k++;
}
else
{
LC->elem[k]=LB->elem[j];
j++;
k++;
}
while(i<=LA->last) /*当表LA有剩余元素时,则将表LA余下的元素赋给表LC*/
{
LC->elem[k]= LA->elem[i];
i++;
k++;
}
while(j<=LB->last) /*当表LB有剩余元素时,则将表LB余下的元素赋给表LC*/
{
LC->elem[k]= LB->elem[j];
j++;
k++;
}
LC->last=LA->last+LB->last+1;
}
【算法分析】
由于两个待归并的表 LA、LB 本身就是有序表,且表 LC 的建立采用的是尾插法建表,插入时不需要移动元素,所以算法的时间复杂度 O(LA->last+LB->last)
有序表合并算法包括了对线性表的定位、比较、移动等线性表基本运算的组合。
只要掌握了基本操作就可以应对实际问题的变化需求。
【顺序存储结构的优缺点分析】
线性表顺序表示的优点是:
(1)无需为表示结点间的逻辑关系而增加额外的存储空间。
(2)可方便的随机存取表中的任一元素。
缺点:
(1)插入或删除运算需要移动大量的结点,效率低下。
(2)由于顺序表存储分配静态分配。当表长变化较大难以确定合适的存储规模。
【本节小结】
1、顺序存储结构——一组连续单元(C 语言的数组)依次存放线性表中的元素
2、基本运算:查找、插入、删除
3、综合应用——顺序表合并
4、顺序存储结构的优缺点分析
通过学习线性表的 ADT 定义,要求掌握抽象数据类型定义表示方法;
通过有序表合并的算法,理解基本操作集的组合和可实现多种问题的要求,并学习掌握算法分析的基本技术。
作业
对一个长度为n的顺序表,假设在任何位置上插入一个元素的概率是相等的,那么插入一个元素时要移动表中的( )个元素。
-
A.n
-
B.n+1
-
C.
-
D.
线性表的顺序存储是指将表中元素按照从大到小或从小到大存储。
-
A.✓
-
B.×
第4讲 线性表的链式存储---内容简介
本讲要点:
回顾线性表的顺序存储结构:逻辑上相邻的元素在物理存储位置上也相邻。
链式存储结构中,逻辑上相邻的元素在物理存储上不一定相邻。
因此设计出结点来对应线性表的元素及元素之间的关系。
结点包括两部分 :结点本身数据信息,元素之间的关联关系。
线性表采用链式方式将结点链接起来的存储结构称为链表。
1、单链表结构
2、单链表类型定义
3、单链表的基本使用方式
第 4 讲 线性表的链式存储
线性表的顺序存储结构,一组连续单元依次存放表中各个元素,逻辑顺序隐含在物理位置之中。便于随机存取,当插入、删除引起表长度的动态变化就要有大量元素移动,效率较低。
链式存储结构中结点包括两部分 不仅包括结点本身信息,还要包括关联关系部分。
线性表采用链式方式将结点链接起来的存储结构称为链表。
链式存储结构分为单链表、循环单链表、双向链表和静态链表。
l 从链接方式看,可分为单链表、循环链表和双向链表。
l 从实现角度看,可分为动态链表和静态链表。
1.单链表结构
链表中的结点包括数据域和指针域两个域:
- 数据域 data 用来存储结点的值;
- 指针域 next 用来存储结点本身的信息,邻接关系由后继指针指示其位置。
线性链表(单链表):用一组任意的存储单元存放线性表的结点,每个结点的唯一后继依靠一个结点指针维持。
(这组存储单元可以是连续的也可以是不连续的、甚至是零散的存储在内存的任何位置。即链表结点的逻辑顺序和物理顺序不一定相同。)
l 头指针 H 指向第一个结点。
l 最后一个结点的指针域为“空”(NULL)。
附加头结点:为方便统一表的运算处理,在单链表的第一个结点之前附设一个头结点。头结点的数据域没有必需要求,可存储线性表的长度附加信息,也可以什么都不存。
l 头结点的指针域存储指向第一个结点的存储位置。
l 如果线性表为空表,则头结点指针域为“空”,如图 2.8 所示。
问题:链表中头指针、头结点、首结点的关系。
带头结点的链表中,头指针指向头结点,头结点指向首结点。
无头结点的链表中,头指针指向首结点。
【单链表存储示例】
如图所示为线性表(A,B,C,D,E,F,G,H)的单链表存储结构,整个链表的存取需从头指针开始进行,依次顺着每个结点的指针域找到线性表的各个元素
单链表的示例图:则链表中元素及关系为: (A,B,C,D,E,F,G,H)
一般情况下,我们使用链表,只关心链表中结点间的逻辑顺序,并不关心每个结点的实际存储位置,因此我们通常是用箭头来表示链域中的指针,于是链表就可以更直观地画成用箭头链接起来的结点序
【单链表的存储结构】
单链表类型的 C 语言定义 描述如下:
void init_linklist(LinkList *l)/*对单链表进行初始化*/
{
*l=(LinkList)malloc(sizeof(Node)); /*申请结点空间*/
(*l)->next=NULL; /*置为空表*/
}
LinkList 与 Node*同为结构指针类型,这两种类型是等价的。通常我们习惯上用 LinkList 说明指针变量,强调它是某个单链表的头指针变量,用 Node*来定义指向单链表中节点的指针。
单链表的基本使用方式
单链表的操作必须从头指针开始依次访问表中结点,访问带头结点单链表 L 的首结点 p,其语句为 : p=L->next
【问题】请描述单链表 L 中首结点的 data 部分。答案: (L->next)->data
本节要点:
1、单链表结构
2、单链表类型定义
3、单链表的基本使用方式
作业
通过表达式 可以获取带头结点的单链表L中首元素结点的数据值。
-
A.L->next
-
B.(L->next)->data
-
C.L->data
-
D.L->next
单链表中必须设有头结点。()
-
A.✓
-
B.×
第5讲单链表的基本运算—内容简介
本讲要点:
单链表的基本运算的实现:
- l求单链表的长度
- l建立单链表
- l单链表查找
- l单链表插入操作
- l单链表删除
通过基本运算的实现理解线性表链式存储的特点。
第 5 讲单链表的基本运算
本讲要点:
1、单链表的基本运算:
2、算法应用举例:求两个集合的差
1、求单链表长度操作
在顺序表中,线性表的长度是它的属性,数组定义时就已确定。
在单链表中,整个链表由“头指针”来表示,单链表的长度在从头到尾遍历的过程中统计计数得到。
【算法思路】采用“数”结点的方法求出单链表的长度。即从“头”开始 “数”
(p=L->next),用指针 p 依次指向各个结点,一直“数”到最后一个结点(p->next==NULL),从而得到单链表的长度。
l 顺链头开始,计数器 j 初值为 0
l 当前指针 p 指向链表 L 的首元结点 p=L->next
l p 依次往后(计数 j++)直到表尾(p==NULL )
【算法描述】
void CreateFromHead(LinkList L)
{
Node *s;
char c;
int flag=1;
while(flag) /* flag初值为1,当输入"$"时,置flag为0,建表结束*/
{
c=getchar();
if(c!='$')
{
s=(Node*)malloc(sizeof(Node)); /*建立新结点s*/
s->data=c;
s->next=L->next;/*将s结点插入表头*/
L->next=s;
}
else
flag=0;
}
}
【算法分析】若单链表 L 为空表,p 的初值为“NULL”,算法中 while 循环未执行,则返回链表长度 j 为 0。
若单链表 L 为非空表,算法中 while 循环执行次数为表长度 n,故算法的时间复杂度为 O(n)。
2、建立空单链表
建立由头指针 L 指向的头结点,头结点的指针域置为空,完成空的单链表 L的初始化。
InitList(LinkList *L)
{
*L = (LinkList)malloc(sizeof(Node)); /*建立头结点*/
(*L)->next = NULL; /*建立空的单链表 L*/
}
3、建立单链表
建立单链表的方法主要由头插法和尾插法。
1)头插法建表
【算法思想】已知空链表 L,依次读入结点数据头插入,直到读入结束标志为止。
每插入一个结点到表头的步骤:
1.生成新结点 s 并赋值;
2.将结点 s 插入到首元结点之前,即表头结点之后;
3.重复以上过程,生成并插入第 i 个结点,直到读取到结束字符为$时,
结束读取数据,建表完毕。
【算法描述】
void CreateFromHead(LinkList L)
{
Node *s;
char c;
int flag=1;
while(flag) /* flag初值为1,当输入"$"时,置flag为0,建表结束*/
{
c=getchar();
if(c!='$')
{
s=(Node*)malloc(sizeof(Node)); /*建立新结点s*/
s->data=c;
s->next=L->next;/*将s结点插入表头*/
L->next=s;
}
else
flag=0;
}
}
注:头插法得到的单链表的逻辑顺序与输入的顺序相反,所以也称为逆序建表法。
2)尾插法建表
头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。
该方法是将新节点插到当前单链表的表尾上。为此需增加一个尾指针 r,始终指向当前链表的表尾。
【算法思想】
已知空链表 L,设置尾指针 r 指向当前表尾 r=L
依次读入结点数据尾插入,直到读入结束标志时将表尾结点链域置空
每插入一个结点到表尾的步骤:
1.生成新结点 s;
2.将结点 s 插入到表尾 r 结点之后;该结点作为当前表尾结点
【算法描述】
void CreateFromTail(LinkList L)
{
Node *r, *s;
char c;
int flag =1; /*设置一个标志,初值为1,当输入"$"时,flag为0,建表结束*/
r=L; /*r指针动态指向链表的当前表尾,以便于做尾插入,其初值指向头结点*/
while(flag) /*循环输入表中元素值,将建立新结点s插入表尾*/
{
c=getchar();
if(c!='$')
{
s=(Node*)malloc(sizeof(Node));
s->data=c;
r->next=s;
r=s;
}
else
{
flag=0;
r->next=NULL; /*将最后一个结点的next链域置为空,表示链表的结束*/
}
}
}
4、单链表的查找
单链表查找可分为按序号查找和按值查找的两种方式。
1)按序号查找
l 在单链表 L 中查找第 i 个结点,
【基本思想】链表的头指针出发,顺着链域 next 逐个数结点,直至搜索到第 i 个结点为止。
【算法思想】
1.从头查找,当前指针 p 指向表头 L;
2. 顺链计数,表未查完且未找到,则计数器 j 加 1,p 指针后移;继续找下一个.
3. 结果判断 如找到第 i 个结点,则返回结点 p;
如表查完未找到,则返回空。
【算法描述】
Node * Get (LinkList L, int i)
/*在带头结点的单链表L中查找第i个结点,若找到(1≤i≤n),则返回该结点的存储位置; 否则返回NULL*/
{
int j;
Node *p;
p=L;
j=0; /*从头结点开始扫描*/
while ((p->next!=NULL)&&(j<i))
{
p=p->next; /* 扫描下一结点*/
j++; /* 已扫描结点计数器 */
}
if(i == j)
return p; /* 找到了第i个结点 */
else
return NULL; /* 找不到,i≤0或i>n */
}
【问题】:在单链表中可否直接找到第 i 个结点?
答案:不行。链表中结点只能顺链查找,不能直接随机存取。
因为第 i 个结点的位置信息是在第 i-1 个结点的链域中存放,无法直接随机存取,这是由链表结构的特征决定的,必须从链表的表头开始查找。
2)按值查找
【算法思想】
1. 从表头开始, p 指向链表 L 首元结点p=L->next;
2.当表未查完时,若当前结点值不为 key,则指针 p 后移比较当前结点值为 key,则跳出循环
3.结束返回 p 指针的位置。
【算法描述】
Node *Locate( LinkList L,ElemType key)
/*在带头结点的单链表L中查找其结点值等于key的结点,若找到则返回该结点的位置p,否则返回NULL*/
{
Node *p;
p=L->next; /*从表中第一个结点开始 */
while (p!=NULL)
{
if (p->data!=key)
p=p->next;
else
break; /*找到结点值=key时退出循环 */
}
return p;
}
【算法时间性能分析】按序号查找和按值查找由于都是需要从表头开始逐个搜索,最多走完整个表,因而这两个算法的平均时间复杂度是相同的即 O(n)。
5、单链表插入操作
要在带头结点的单链表 L 中第 i 个数据元素之前插入一个数据元素 e。
【算法思想】
1.确定第 i-1 个结点的位置 (同按序号查找算法)
2.申请新结点 s
3. 插入挂链:新结点 s 插至第 i-1 个结点之后
S 结点的后继指向第 i 个结点 s->next=pre->next
第 i-1 个结点的指针指向 s pre->next=s
【算法描述】
int InsList(LinkList L,int i,ElemType e)
/*在带头结点的单链表L中第i个位置插入值为e的新结点s*/
{
Node *pre,*s;
int k;
pre=L;
k=0; /*从"头"开始,查找第i-1个结点*/
while(pre!=NULL&&k<i-1) /*表未查完且未查到第i-1个时重复,找到pre指向第i-1个*/
{
pre=pre->next;
k=k+1;
} /*查找第i-1结点*/
if(!pre) /*如当前位置pre为空表已找完还未数到第i个,说明插入位置不合理*/
{
printf("插入位置不合理!");
return ERROR;
}
s=(Node*)malloc(sizeof(Node)); /*申请一个新的结点S */
s->data=e; /*值e置入s的数据域*/
s->next=pre->next; /*修改指针,完成插入操作*/
pre->next=s;
return OK;
}
6、单链表删除
【算法思想】
1.确定第 i-1 个结点的位置 p(同按序号查找算法)
2.删除并释放第 i 个结点
指针 r 指向被删结点 r=p->next
删除第 i 个结点 r p->next=r ->next
释放 r 结点 free(r)
【算法描述】
int DelList(LinkList L,int i,ElemType *e)
/*在带头结点的单链表L中删除第i个元素,并将删除的元素保存到变量*e中*/
{
Node *pre,*r;
int k;
pre=L;
k=0;
while(pre->next!=NULL && k<i-1) /*寻找被删除结点i的前驱结点i-1使p指向它*/
{
pre=pre->next;
k=k+1;
} /*查找第i-1个结点*/
if(!(pre->next)) /* 即while循环是因为p->next=NULL或i<1而跳出的,而是因为没有找到合法的前驱位置,说明删除位置i不合法。*/
{
printf("删除结点的位置i不合理!");
return ERROR;
}
r=pre->next;
pre->next=pre->next->next; /*修改指针,删除结点r*/
*e = r->data;
free(r); /*释放被删除的结点所占的内存空间*/
printf("成功删除结点!");
return OK;
}
说明:删除算法中的循环条件(pre->next!=NULL && k<i-1)与前插算法中的循环条件(pre!=NULL && k<i-1)不同,
因为前插时的插入位置有 m+1 个(m 为当前单链表中数据元素的个数)。
i=m+1 是指在第 m+1 个位置前插入,即在单链表的末尾插入。
而删除操作中删除的合法位置只有 m 个,若使用与前插操作相同的循环条件,则会出现指针指空的情况,使删除操作失败。
作业
下列选项中, 项是链表不具有的特点。
-
A.插入和删除运算不需要移动元素
-
B.所需要的存储空间与线性表的长度成正比
-
C.不必事先估计存储空间大小
-
D.可以随机访问表中的任意元素
有一个带头结点的单链表HEAD,则判断其是否为空链表的表达式是
-
A.HEAD= =NULL
-
B.HEAD-〉NEXT= =NULL
-
C.HEAD-〉NEXT= =HEAD
-
D.HEAD!=NULL
在一个单链表中P所指结点后插入一个S所指结点时,
应执行语句: 。
-
A.P->next=S;S->next=P->next;
-
B.S->next=P->next;P->next=S;
-
C.S->next=P->next;P=S;
-
D.S->next=P;P->next=S;
第6讲单链表的应用实例及小结---内容简介
本讲主要内容:
-
通过实例:求两个集合的差来进一步对单链表的存储特点深刻理解并对基本算法综合运用。
-
单链表存储结构小结。
第 6 讲 单链表运算的应用示例及小结
【单链表运算应用示例】求两个集合的差
【问题描述】单链表表示集合,假设集合 A 用单链表 LA 表示,集合 B 用单链表 LB 表示,设计算法求两个集合的差 A-B。
【算法思想】由集合运算的规则可知,集合的差 A-B 中包含所有属于集合 A 而不属于集合 B 的元素。
逐个读取集合 A 的链表 LA 中每个元素,在集合 B 的链表 LB 中进行查找,若存在相同的元素,则从链表 LA 中将其删除。
【算法描述】
1、指针 pre 指向单链表 LA, 指针 p 指向单链表 LA 的首元结点;
2、指针 q 指向单链表 LB 的首元结点;随后逐一访问单链表 LB 中结点,将其与单链表 LA 中 p 指针指向的结点进行比较,如果两者相同,则删除 p 所指向的结点,直到读完表 LB 为止。
【本节要点】
1、顺链操作技术
访问单链表 L 中结点 i(p 指向该结点)时,必须从“头”开始。
由于第 i 个结点的地址在第 i-1 个结点(pre 指向该结点,为 p 的前驱)的指针域中存放,查找必须从单链表的“首结点”开始(p=L);
通过 p=p->next 并辅助计数器来实现;
2、表尾控制条件 p==NULL
3、指针保留技术:
在处理过程中始终需要维持当前指针 p 与其前驱指针 pre 的关系。如对第 i 个结点进行插入、删除等操作时,
需要对第 i-1 个结点的指针域进行链址操作(pre->next),因此在处理过程中始终需要维持当前指针 p 与其前驱指针 pre 的关系,这种技术简称为“指针保留技术”。
作业
设指针变量p指向单链表中结点A的直接前驱,若删除单链表中结点A,则需要修改指针的操作序列为( )。
-
A.q=p->next;p->next=q->next;free(q);
-
B.q=p->next; p->next=q->next;
-
C.p->next=p-> next->next;
-
D.q=p->next;p->data=q->data;free(q);
对链表进行插入和删除操作时不必移动链表中结点。( )
-
A.✓
-
B.×
在单链表中,可以从头结点出发,查找到表中所有结点。( )
-
A.✓
-
B.×
补充资源1-顺序表完整程序示例
/*包含文件*/
#include "stdio.h"
/*宏定义*/
#define MAXSIZE 100
#define OK 1
#define ERROR 0
typedef int ElemType; /*顺序表中存放整型元素*/
typedef struct {
ElemType elem[MAXSIZE]; //ElemType目前为整型;其可以为任意类型
int last;
}SeqList;
/*函数声明*/
void initList(SeqList* L);
void printList(SeqList L);
void inputData(SeqList* L);
void merge(SeqList* LA, SeqList* LB, SeqList* LC);
/*主函数*/
int main()
{
SeqList La, Lb, Lc;
/* 初始化三个顺序表*/
initList(&La);
initList(&Lb);
initList(&Lc);
/*给顺序表La和Lb赋初值*/
printf("请给顺序表La输入元素(按非递减顺序),输入-100表示结束\n");
inputData(&La);
printf("请给顺序表Lb输入元素(按非递减顺序),输入-100表示结束\n");
inputData(&Lb);
merge(&La, &Lb, &Lc);
/*输出三个顺序表*/
printf("\nLa="); printList(La);
printf("\nLb="); printList(Lb);
printf("\nLc(La+Lb)="); printList(Lc);
getchar(); //让输出界面不闪退,观察结果
return 1;
}
/*函数定义*/
void initList(SeqList* L)
{
L->last = -1;
}
void inputData(SeqList* L)
{
int i = 0, num;
scanf("%d", &num);
while (num != -100)
{
L->elem[i] = num;
i++;
scanf("%d", &num);
}
L->last = i - 1;
}
void printList(SeqList L)
{
int i;
printf("(");
for (i = 0; i <= L.last; i++)
printf("%d ", L.elem[i]);
printf(")");
}
//以下函数是改程序的核心部分
void merge(SeqList* LA, SeqList* LB, SeqList* LC)
{
int i, j, k;
i = 0; j = 0; k = 0;
while (i <= LA->last && j <= LB->last)
if (LA->elem[i] <= LB->elem[j])
{
LC->elem[k] = LA->elem[i];
i++;
k++;
}
else
{
LC->elem[k] = LB->elem[j];
j++;
k++;
}
while (i <= LA->last) /*当表LA有剩余元素时,则将表LA余下的元素赋给表LC*/
{
LC->elem[k] = LA->elem[i];
i++;
k++;
}
while (j <= LB->last) /*当表LB有剩余元素时,则将表LB余下的元素赋给表LC*/
{
LC->elem[k] = LB->elem[j];
j++;
k++;
}
LC->last = LA->last + LB->last + 1;
}
补充资源2-链表完整程序示例
#include "stdio.h"
#include "stdlib.h"
typedef int ElemType;
typedef struct node
{
ElemType data;
struct node *next;
}Node,*LinkList;
LinkList createFromTail();
void printLinkList(LinkList L);
void reverseLinkList(LinkList L);
int main()
{
LinkList L;
L=createFromTail();
printf("\nAfter create,the list is:\n");
printLinkList(L);
reverseLinkList(L);
printf("\nAfter Reverse,the list is:\n");
printLinkList(L);
return 1;
}
//用尾插法建立一个带头结点的单链表
LinkList createFromTail()
{
LinkList L;
Node *r, *s;
int flag =1;
int d;
L=(Node * )malloc(sizeof(Node)); //该结点为头结点
L->next=NULL;
r=L;
printf("Input int one by one,-1 to end:\n");
while(flag)
{
scanf("%d",&d);
if(c!=-1)
{
s=(Node*)malloc(sizeof(Node));
s->data=c;
r->next=s;
r=s;
}
else
{
flag=0;
r->next=NULL;
}
}
return L;
}
void printLinkList(LinkList L)
{
Node *p;
p=L->next;
while(p!=NULL)
{
printf("%d ",p->data);
p=p->next;
}
}
void reverseLinkList(LinkList L)
{
Node *p=L->next,*q; /* P为原链表的当前处理结点*/
L->next=NULL;
while(p!=NULL)
{ q=p->next;
p->next=L->next;
L->next=p;
p=q;
}
}
补充资源3-单链表初始化解析
单链表初始化函数解析
void InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node));
(*L)->next=NULL;
}
学C语言时,我们都清楚,指针很“厉害”了,那为什么这里还要用指针的指针?
有必要这么复杂吗?
答案:因为指针在此无能为力了,所以必须用指针的指针。
关键为什么指针在此就无能为力了?
一起分析,为什么必须用指针的指针?
#include "malloc.h"
typedef ElemType char; //链表中的元素为char类型
typedef struct Node
{ ElemType data;
struct Node * next;
}Node, *LinkList;
void InitList(LinkList* L)
{ *L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
}
void main()
{
LinkList H = NULL;
InitList(&H);
}
其实还可以这样初始化单链表:
不需要传参数,在InitList函数中申请一个空间,由L暂时指向;最后将L的值返回。
L中存放的就是申请结点的内存地址,返回后赋值给H。同样可完成初始化操作。
LinkList InitList()
{
LinkList L;
L = (LinkList)malloc(sizeof(Node));
L->next = NULL;
return L;
}
void main()
{
LinkList H = NULL;
H = InitList();
}
第7讲循环链表—内容简介
本讲内容:
单链形式的循环链表结构是首尾相接的单链表,即最后一个结点的指针指向链表的表头结点或第一个结点。
1. 循环链表结构的特点
2. 当前结点是循环链表表尾结点的控制条件
3. 循环链表从当前结点遍历所有结点的方法
4、两个循环单链表连接算法
第7 讲 循环单链表
单链形式的循环链表结构是首尾相接的单链表,即最后一个结点的指针指向链表的表头结点或第一个结点。
判别当前结点 p 是否为循环单链表 L 的表尾结点的判别条件:p->next!=L,即 p->next 是否指回到头,与一般单链表 p->next 是否为 NULL 不同。
在循环单链表中,也可只设置一个头结点,这样,空循环链表仅由一个自成循环的头结点表示。
循环链表:
定义: 即首尾相接的链表。
结构: 尾结点的指针域指向头结点或表的首元结点;
特点:表中所有结点都链接在一个环上。
循环链表示意图:
循环单链表的合并算法:
【算法思想】
l 遍历两链表找表尾;
l 将第一表尾链接第二表头,将第二表尾链接第一表头;
【算法步骤】
1.设置指针 p,q 从头遍历查找两链表的表尾;
2.修改第一表的尾指针,使其指向第二表的头结点;
3.修改第二表的尾指针,使其指向第一表的头结点;
循环单链表的合并算法举例:
有两个带头结点的循环单链表 LA、LB,设计算法,将两个循环单链表合并为一
个循环单链表,其头指针为 LA。
【算法描述】
LinkList merge_1(LinkList LA,LinkList LB)
{ /*此算法将两个采用头指针的循环单链表的首尾连接起来*/
Node *p, *q;
p=LA;
q=LB;
while (p->next!=LA) p=p->next; /*找到表LA的表尾,用p指向它*/
while (q->next!=LB) q=q->next; /*找到表LB的表尾,用q指向它*/
q->next=LA; /*修改表LB 的尾指针,使之指向表LA 的头结点*/
p->next=LB->next; /*修改表LA的尾指针,使之指向表LB 中的第一个结点*/
free(LB);
return(LA);
}
采用上面的方法,需要遍历链表,找到表尾,其执行时间是O(n)。
若单循环链表设置尾指针表示,在实现上述合并时﹐无需循环遍历找尾结点,只需要直接修改尾结点的指针域,其执行时间是O(1)。
【算法描述】
算法2.15 循环单链表的合并算法(2)
LinkList merge_2(LinkList RA,LinkList RB)
{ /*此算法将两个采用尾指针的循环链表首尾连接起来*/
Node *p;
p=RA->next; /*保存链表RA的头结点地址*/
RA->next=RB->next->next;/*链表RB的开始结点链到链表RA的终端结点之后*/
free(RB->next);/*释放链表RB的头结点*/
RB->next=p;/*链表RA的头结点链到链表RB的终端结点之后*/
return RB;/*返回新循环链表的尾指针*/
}
【问题】判断循环空链表的条件是什么?
答案:头结点的指针域指向自己,即 head->next = =head。
本节要点:
1. 循环链表结构的特点
2. 当前结点是循环链表表尾结点的控制条件
3. 循环链表从当前结点遍历所有结点的方法
作业
有一个带头结点的循环单链表HEAD,则判断其是否为空链表的条件是 。
-
A.HEAD==NULL
-
B.HEAD-〉NEXT==NULL
-
C.HEAD-〉NEXT==HEAD
-
D.HEAD!=NULL
在单向循环链表中,从表中任意结点出发都可以顺着next域访问到表中所有元素()
-
A.✓
-
B.×
第8讲双向链表—内容简介
在单链表的每个结点里再增加一个指向其前趋的指针域prior,这样形成的链表中就有两条方向不同的链,我们称之为双向链表。
本讲主要内容:
l 双向链表下的插入和删除操作
l 双向链表应用举例
第 8 讲 双向链表
循环单链表的出现,虽然能够实现从任一结点出发沿着链能找到其前趋结点,但时间耗费是 O (n) 。
如果希望从表中快速确定某一个结点的前趋,另一个解决方法就是在单链表的每个结点里再增加一个指向其前趋的指针域 prior。这样形成的链表中就有两条方向不同的链,我们称之为双向链表。
双向链表的结构定义如下:
typedef struct DNode
{
ElemType data;
struct DNode* prior, * next;
}DNode, * DoubleList;
双向链表的结点结构如图所示。
注:
- 双向链表也是由头指针唯一确定的,增加头结点能使双链表的某些运算变得方便
- 由于在双向链表中既有前向链又有后向链,寻找任一个结点的直接前驱结点与直接后继结点变得非常方便。
- 设指针 p 指向双链表中某一结点,则有下式成立:p->prior->next = p = p->next->prior
- 在双向链表中,那些只涉及后继指针的算法,如求表长度、取元素、元素定位等,与单链表中相应的算法相同,但对于前插和删除操作则涉及到前驱和后继两个方向的指针变化,因此与单链表中的算法不同。
1、 双向链表的前插操作
【算法思想】欲在双向链表第 i 个结点之前插入一个的新的结点,则指针的变化情况如图所示:
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
【算法描述】
int DlinkIns(DoubleList L, int i, ElemType e)
{
DNode* s, * p;
int k;
p = L;
k = 0; /*从"头"开始,查找第i-1个结点*/
while (p->next != L && k < i) /*表未查完且未查到第i-1个时重复,找到p指向第i个*/
{
p = p->next;
k = k + 1;
} /*查找第i-1结点*/
if (p->next == L) /*如当前位置p为空表已找完还未数到第i个,说明插入位置不合理*/
{
printf("插入位置不合理!");
return ERROR;
}
s = (DNode*)malloc(sizeof(DNode));
if (s)
{
s->data = e;
s->prior = p->prior;
p->prior->next = s;
s->next = p;
p->prior = s;
return OK;
}
else
return ERROR;
}
2、 双向链表的删除操作
【算法思想】欲删除双向链表中的第 i 个结点,则指针的变化情况如图所示:
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
【算法描述】
int DlinkDel(DoubleList L,int i,ElemType *e)
{
DNode *p;
int k;
p=L;
k=0; /*从"头"开始,查找第i个结点*/
while(p->next!=L && k<i) /*表未查完且未查到第i个时重复,找到p指向第i个*/
{
p=p->next;
k=k+1;
}
if(p->next == L)
{
return ERROR;
}
else
{
*e=p->data;
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
return OK;
}
}
3、 双向循环链表
双向链表可以有循环表,称为双向循环链表。
4、 应用举例
编写算法:将一个循环双链表 L=(a,b,c,d)转换为 L=(b,a,c,d)。
算法思想:实际上是改变表中两个元素的链接。在修改有关链域之前,注意做必要的备份。
作业
本节要点:
单链表只能通过后继域从一个方向访问结点,双向链表可从两个方向访问任一结点,快速,方便。与单链表相比,双向链表的优点之一是 。
-
A.插入删除操作更加方便
-
B.可以进行随机访问
-
C.可以省略表头指针和表尾指针
-
D.访问前后相邻结点更方便。
在双向链表L中,可以从任一结点p出发沿同一方向的指针域查找到表中所有元素。()
-
A.✓
-
B.×
第9讲静态链表—内容简介
链表通常借助于高级语言中的指针数据类型来实现,但在实际应用中,有些语言中并未提供“指针”类型,故采用数组模拟实现链表的方式,用静态模拟动态。
本讲要点:
l 静态链表的基本特征
l 静态链表的基本操作
第 9 讲 静态链表
由前面讨论可知,链表的形式多样,组织方式灵活,便于元素的插入和删除操作,通常借助于高级语言中的指针数据类型来实现,
但在实际应用中,有些语言中并未提供“指针”类型,故采用数组模拟实现链表的方式---静态链表。
一.静态链表的基本特征:
1.存储池:定义一个较大的结构数组作为备用结点空间
2.游标机制:每个结点应含有两个域:data 域和 next(或 cursor)域。
●data 域用来存放结点的数据信息
●cursor 域为游标指示器,指示后继结点在结构数组中的相对位置(即数组下标值)。
数组的第 0 个分量可以设计成表的头结点,头结点的 cursor 域指示了表中第一个结点的位置,cursor 值为 0 表示静态单链表的结束。
3.结点结构:静态单链表可以借助结构体数组来描述:
#define Maxsize 100 //链表可能达到的最大长度
typedef struct
{
ElemType data;
int cursor;
}Component, StaticList[Maxsize];
例:通过变量定义语句 StaticList S;定义的静态单链表 S 中存储着线性表( a,b,c,d,f,g,h,i),Maxsize=11,如图 2.18 所示。要在第 4 个元素后插入元素 e。
静态链表的插入和删除操作示例
方法:先申请一个空闲空间并置入元素 e,即:使 S[9].data=e,
然后修改第四个 元 素 的 游 标 域 , 将 e 插 入 到 链 表 中 。 即 : S[9].cursor= S[4].cursor ,S[4].cursor=9。
若要删除第 8 个元素 h,则先顺着游标链通过记数找到第 7 个元素存储位置 6,删除的具体做法是令 S[6].cursor=S[7].cursor。
缺点 :上述例子中未考虑对已释放空间的回收,这样在经过多次插入和删除后,
会造成静态链表的“假满”。即表中有很多的空闲空间,但却无法再插入元素。造成这种现象的原因是未对已删除的元素所占的空间进行回收。
解决这个问题的方法:是将所有未被分配的结点空间以及因删除操作而回收的结点空间用游标链成一个备用静态链表。
当进行插入操作时,先从备用链表上取一个分量来存放待插入的元素,然后将其插入到已用链表的相应位置。
当进行删除操作时,则将被删除的结点空间链接到备用链表上以备后用。
这种方法是指在已申请的大的存储空间中有一个已用的静态单链表,还有一个备用单链表。
已用静态单链表的头指针为 0,备用静态单链表的头指针需另设一个变量 av 来表示。
二.静态单链表的基本操作算法:
1.初始化
算法描述:初始化为一个备用静态单链表。
算法:
void initial(StaticList space, int *av)
{
int k;
space[0].cursor=0; /*设置已用静态单链表的头指针指向位置0*/
for(k=0;k<Maxsize-1;k++)
space[k].cursor=k+1; /*连链*/
space[Maxsize-1].cursor=0; /*标记链尾*/
*av=1; /*设置备用链表头指针初值*/
}
2.分配结点
【算法描述】
int getnode(StaticList space, int *av)
/*从备用链表摘下一个结点空间,分配给待插入静态链表中的元素*/
{
int i;
i=*av;
*av=space[*av].cursor;
return i;
}
●对系统而言,在备用链表中分配结点相当于备用链表中减少一个结点 i,对使用者而言,相当于申请得到了一个可用的新结点 i。
3.结点回收
【算法描述】
void freenode(StaticList space, int *av, int k)
/*将下标为 k的空闲结点插入到备用链表*/
{
space[k].cursor=*av;
*av=k;
}
●对系统而言,备用链表回收空闲结点相当于备用链表中增加一个结点 k,对使用者而言,相当于释放了一个不用的结点 k。
作业
静态链表中与动态链表的插入和删除运算类似,不需要做元素的移动。()
-
A.✓
-
B.×
静态链表既有顺序存储结构的优点,又有动态链表的优点。所以,它存取表中第i个元素的时间与位置序号i无关,可以实现随机存取。()
-
A.✓
-
B.×
第10讲链式结构小结—内容简介
本讲内容:
对线性表的链式存储:单链表、单向循环链表、双向链表、双向循环链表、静态链表的存储特点进行小结。通过对比掌握线性表链式存储的特点。
作业
已知单链表的头指针为head且该链表不带头结点,则该单链表为空的条件是 。
-
A.head== NULL
-
B.head->next==NULL
-
C.head->next==head
-
D.head!=NULL
设指针变量p指向单链表中某结点的直接前驱,若删除单链表中该结点,需要修改指针的操作序列为 。
-
A. q=p->next; p->next=q->next;free(q);
-
B. q=p->next; free(q);
-
C. p->next=p->next->next;free(p->next);
-
D.q=p->next; free(q);
设带有头结点的单向循环链表的头指针变量为head,则其判空条件是 。
-
A.head==NULL
-
B.head->next==NULL
-
C.head->next==head
-
D.head!=NULL
在双向循环链表中,可以从任一结点p出发沿同一方向的指针域查找到表中所有元素。()
-
A.✓
-
B.×
第11讲线性表应用——一元多项式的表示与相加—内容简介
本讲通过一元多项式的表示及相加的问题,作为线性表应用的典型,总结了本章学习的线性表的两种存储方式、运算实现技术等主要内容。
本讲内容:
1. 一元多项式的表示
2. 一元多项式的存储:顺序存储和链式存储
3.一元多项式的相加
第 11 讲线性表应用—一元多项式的表示及相加
前面学习了线性表的概念、顺序存储方式和链式存储方式,本节课我们来学习线性表应用。
本节通过一元多项式的表示及相加的问题,作为线性表应用的典型,总结了本章学习的线性表的两种存储方式、运算实现技术等主要内容。
1、一元多项式的表示
一元多项式可按升幂的形式写成:
其中,ei 为第 i 项的指数,pi 是指数 ei 的项的系数,(且 1≤e1≤e2≤…≤en)
在计算机内,P n (x)可以用一个线性表 P 来表示:
设有两个一元多项式 Pn(x) 和 Qm(x),假设 m<n,则两个多项式相加的结果Rn(x)= Pn(x) + Qm(x),也可以用线性表 R 来表示:
2、一元多项式的存储
一元多项式的操作可以利用线性表来处理。因此,一元多项式也有顺序存储和链式存储两种方法。
(1)一元多项式的顺序存储表示
对于一元多项式:
有两种顺序存储方式:
第一种:只存储各项的系数,存储位置下标对应其指数项,适用于存储非零系数多的一元多项式
第二种:系数及指数均存入顺序表,适用于存储非零项少且指数高的一元多项式,此时只存储非零项的系数和指数即可。
例如:
(2)一元多项式的链式存储表示
在链式存储中,对一元多项式只存储非零项的指数项和系数项
用单链表存储表示的结点结构为:
struct Polynode
{
int coef;
int exp;
Polynode* next;
} Polynode, * Polylist;
例:建立一元多项式链式存储算法
【算法思想】通过键盘输入一组多项式的系数和指数,用尾插法建立一元多项式的链表。以输入系数 0 为结束标志,并约定建立多项式链表时,总是按指数从小到大的顺序排列。
【算法描述】
void polycreate(Polylist head)
{
Polynode* rear, * s;
int c, e;
rear = head; /* rear 始终指向单链表的尾,便于尾插法建表*/
scanf("%d,%d", &c, &e); /*键入多项式的系数和指数项*/
while (c != 0) /*若c=0,则代表多项式的输入结束*/
{
s = (Polynode*)malloc(sizeof(Polynode)); /*申请新的结点*/
s->coef = c;
s->exp = e;
rear->next = s; /*在当前表尾做插入*/
rear = s;
scanf("%d,%d", &c, &e);
}
rear->next = NULL; /*将表的最后一个结点的next置NULL,以示表结束*/
}
3、一元多项式的相加运算
(1)用单链表表示的两个一元多项式
(2)多项式相加的运算规则
为了保证“和多项式”中各项仍按升幂排列,在两个多项式中:
①指数相同项的对应系数相加,若和不为零,则构成“和多项式”中的一项;
②指数不相同的项仍按升幂顺序复抄到“和多项式”中。
【算法思想】以单链表 polya 和 polyb 分别表示两个一元多项式 A 和 B,
A+B 的求和运算,就等同于单链表的插入问题(将单链表 polyb 中的结点插入到单链表 polya 中),因此 “和多项式“中的结点无需另生成。
为实现处理,设 p、q 分别指向单链表 polya 和 polyb 的当前项,比较 p、q结点的指数项,由此得到下列运算规则:
①若 p->exp< q->exp,则结点 p 所指的结点应是“和多项式”中的一项,令指针 p 后移;
②若 p->exp=q->exp,则将两个结点中的系数相加,当和不为零时修改结点 p 的系数域,释放 q 结点;若和为零,则和多项式中无此项,从 A 中删去 p 结点,同时释放 p 和 q 结点。
③若 p->exp>q->exp,则结点 q 所指的结点应是“和多项式”中的一项,将结点 q 插入在结点 p 之前,且令指针 q 在原来的链表上后移;
【算法描述】
void polyadd(Polylist polya, Polylist polyb)
/*此函数用于将两个多项式相加,然后将和多项式存放在多项式polya中,并将多项式ployb删除*/
{
Polynode* p, * q, * pre, * temp;
int sum;
p = polya->next; /*令 p和q分别指向polya和polyb多项式链表中的第一个结点*/
q = polyb->next;
pre = polya; /* r指向和多项式的尾结点*/
while (p != NULL && q != NULL) /*当两个多项式均未扫描结束时*/
{
if (p->exp < q->exp)
/*如果p指向的多项式项的指数小于q的指数,将p结点加入到和多项式中*/
{
pre->next = p;
pre = p;
p = p->next;
}
else
if (p->exp == q->exp) /*若指数相等,则相应的系数相加*/
{
sum = p->coef + q->coef;
if (sum != 0)
{
p->coef = sum;
pre->next = p;
pre = p;
p = p->next;
temp = q;
q = q->next;
free(temp);
}
else
{
temp = p;
p = p->next;
free(temp);
/*若系数和为零,则删除结点p与q,并将指针指向下一个结点*/
temp = q;
q = q->next;
free(temp);
}
}
else
{
pre->next = q;
pre = q; /*将q结点加入到和多项式中*/
q = q->next;
}
}
if (p != NULL) /*多项式A中还有剩余,则将剩余的结点加入到和多项式中*/
pre->next = p;
else /*否则,将B中的结点加入到和多项式中*/
pre->next = q;
}
假设 A 多项式有 M 项,B 多项式有 N 项,则上述算法的时间复杂度为O(M+N)
推广:
通过对多项式加法的介绍,可以将其推广到实现两个多项式的相乘,因为乘法可以分解为一系列的加法运算。
本节要点:
1. 一元多项式的表示
2. 一元多项式的存储:顺序存储和链式存储
3.一元多项式的相加
第12讲顺序表与链表综合比较—内容简介
本讲内容:
顺序表和链表这两种存储表示方法各有优缺点。
l 顺序表和链表的比较,从以下三个方面进行对比:
1.基于空间的考虑
2.基于时间的考虑
3. 基于语言的考虑
l 线性表各种链式存储方式的比较
第 12 讲 顺序与链表综合比较
顺序表和链表这两种存储表示方法各有优缺点。在实际应用中究竟选用哪一种存储结构呢?这要根据具体的要求和性质决定。
顺序表和链表的比较
1.基于空间的考虑
顺序表的存储空间是静态分配的,在程序执行之前必须明确规定它的存储规模。
若线性表的长度 n 变化较大,则存储规模难于预先确定。估计过大将造成空间浪费,估计太小又将使空间溢出的机会增多。
在静态链表中,初始存储池虽然也是静态分配的,但若同时存在若干个结点类型相同的链表,则它们可以共享空间,使各链表之间能够相互调节余缺,减少溢出机会。
动态链表的存储空间是动态分配的,只要内存空间尚有空闲,就不会产生溢出。
因此,当线性表的长度变化较大,难以估计其存储规模时,采用动态链表作为存储结构较好。
存储密度(Storage Density)是指结点数据本身所占的存储量和整个结点结构所占的存储量之比,即:存储密度=结点数据本身所占的存储量/结点结构所占的存储总量
链表中的每个结点,除了数据域外,还要额外设置指针(或游标)域,从存储密度来讲,这是不经济的。
一般地,存储密度越大,存储空间的利用率就高。显然,顺序表的存储密度为 1,而链表的存储密度小于 1。
例如单链表的结点的数据均为整数,指针所占空间和整型量相同,则单链表的存储密度为 50%。
因此若不考虑顺序表中的备用结点空间,则顺序表的存储空间利用率为 100%,而单链表的存储空间利用率为50%。
由此可知,当线性表的长度变化不大,易于事先确定其大小时,为了节约存储空间,宜采用顺序表作为存储结构。
2.基于时间的考虑
顺序表是由向量实现的,它是一种随机存取结构,对表中任一结点都可以在O (1) 时间内直接地存取,而链表中的结点,需从头指针起顺着链找才能取得。
因此,若线性表的操作主要是进行查找,很少做插入和删除时,宜采用顺序表做存储结构。
在链表中的任何位置上进行插入和删除,都只需要修改指针。
而在顺序表中进行插入和删除,平均要移动表中近一半的结点,尤其是当每个结点的信息量较大时,移动结点的时间开销就相当可观。
因此,对于频繁进行插入和删除的线性表,宜采用链表做存储结构。
若表的插入和删除主要发生在表的首尾两端,则宜采用尾指针表示的单循环链表。
3.基于语言的考虑
在没有提供指针类型的高级语言环镜中,若要采用链表结构,则可以使用光标实现的静态链表。
虽然静态链表在存储分配上有不足之处,但它是和动态链表一样,具有插入和删除方便的特点。
值得指出的是,即使是对那些具有指针类型的语言,静态链表也有其用武之地。
特别是当线性表的长度不变,仅需改变结点之间的相对关系时,静态链表比动态链表可能更方便。
链表名称 |
找表头结点 |
找表尾结点 |
找 P 结点前驱结点 |
带头结点单链表 L | L->next 时间耗费 O(1) | 一重循环 时间耗费 O(n) | 顺 P 结点的 next域无法找到 P 结点的前驱 |
带头结点循环单链表(头指针)L | L->next 时间耗费 O(1) |
一重循环 时间耗费 O(n) | 顺 P 结点的 next域可以找到 P 结点的前驱 时间耗费 O(n) |
带尾指针的循环单链表 R |
R->next O(1) |
R 时间耗费 O(1) | 顺 P 结点的 next域可以找到 P 结点的前驱 时间耗费 O(n) |
带头结点双向循环链表 L | L->next O(1) | L->prior 时间耗费 O(1) | P->prior 时间耗费 O(1) |
作业
下列选项中, 项是链表不具有的特点。
-
A.插入和删除运算不需要移动元素
-
B.所需要的存储空间与线性表的长度成正比
-
C.不必事先估计存储空间大小
-
D.可以随机访问表中的任意元素
在线性表中最常用的操作是存取第i个元素及其前趋的值,可采用 存储方式最省时间?
-
A.顺序表
-
B.带头指针的双向循环链表
-
C.带头指针的单向循环链表
-
D.带头指针的单向链表
下面关于线性表的叙述错误的是( )。
-
A. 线性表采用顺序存储必须占用一片连续的存储空间
-
B.线性表采用链式存储不必占用一片连续的存储空间
-
C.线性表采用链式存储便于插入和删除操作的实现
-
D.线性表采用顺序存储便于插入和删除操作的实现
第二章线性表总结与提高---内容简介
本讲内容:
1、 本章重要知识点
l 线性表的特征:
l 线性表存储方式与基本操作
2、 典型题例:通过3个具有代表性的典型题例突出线性表的存储结构的特点及基本操作。
第二章 线性表总结与提高
本章介绍的线性表是一种最简单最常用的数据结构,理解线性表定义、存储表示,熟练掌握线性表的每一种存储表示方法的特点,灵活应用适当的存储表示法解决具体问题。
【主要知识点】
线性表的特征:线性表中每个数据元素有且仅有一个直接前驱和一个直接后继,第一个结点无前驱,最后一个结点无后继。
线性表存储方式:实现线性表在计算机中的存放有顺序存储与链式存储两种方式。
线性表顺序存储(顺序表):采用静态分配方式,借助于 C 语言的数组类型,申请一组连续的地址空间,依次存放表中元素,其逻辑次序隐含在存储顺序之中。
因为线性表中元素类型相同(即占用空间相等),如果给定起始地址(即数组名)和元素下标,那么就可以方便地实现表中元素的存取。
但当表长度变化时需引起元素的移动,因此较适合表中元素个数固定的情况。
线性表链式存储(链表):采用动态分配方式,借助于 C 语言的指针类型,动态申请与动态释放地址空间,故链表中的各结点的物理存储可以是不连续的。
当表长度变化时仅需适当变化指针的联接,适合于表中元素个数动态变化。
单链表的操作特点:要特别注意总结体会指针的用法。
⑴顺链操作技术:从“头”开始,访问单链表 L 中结点 i(p 指向该结点)时,
由于第i 个结点的地址在第 i-1 个结点(pre 指向该结点,为 p 的前驱)的指针域中存放,查找必须从单链表的“首结点”开始(p=L);通过 p=p->next 并辅助计数器来实现;
⑵指针保留技术:通过对第 i 个结点进行插入、删除等操作时,需要对第 i-1 个结点的指针域进行链址操作(pre->next),
因此在处理过程中始终需要维持当前指针 p 与其前驱指针 pre 的关系,将这种技术简称为“指针保留技术”。
链表处理中的相关技术:
单链表与多重链表的差别在于指针域的个数;
一般链表与循环链表的差别在于是否首尾相接,将非空表、空表等多种情况统一处理,以方便运算。
判断当前结点 p 是否为表尾:一般链表中,p 结点是表尾的条件是:该结点的后继指针值为空指针即:p->next= =NULL,
循环链表中,p 结点是表尾结点的条件是:该结点的后继指针值为头指针值即:p->next= = head 。
链表的表长度 n 值并未显式保存:由于链表是动态生成的结构,其长度要通过顺链查找到表尾得到。
因此在处理链表时,往往是以当前处理位置结点 p 是否为表尾作为控制条件,而不是以表长度 n 作为控制条件。
【典型题例】
【例1】
已知顺序表 L 中的数据元素类型为 int。设计算法将其调整为左右两部分,左边的元素(即排在前面的)均为奇数,右边所有元素(即排在后面的)均为偶数,
并要求算法的时间复杂度为 O(n),空间复杂度为 O(1)。
【问题分析】初见此题,可能会想到额外申请 1 个顺序表空间,之后依次从顺序表 L 中选择奇数放入新表前部分,选择偶数放在新表的后半部分。
但是题目要求空间复杂度为 O(1),很显然上述方法是不可行的。既然要求空间复杂度为 O(1),说明只能借助 1 个辅助空间。
分析题目要求,其实只需要将位于表左半部分的偶数与位于表右半部分的奇数通过一个辅助变量进行交换即可,
为此可以设置两个位置指示器 i 和 j,i 初值为 0,j 初值为 L->last,当L->elem[i]为偶数,L->elem[j]为奇数时,则将 L->elem[i] 与 L->elem[j]交换;
否则,L->elem[i]为奇数,i++, L->elem[j]为偶数,j++。
这样既可以保证算法的时间复杂度为 O(n),亦可保证空间复杂度为 O(1)。
【算法描述】
void AdjustSqlist(SeqList *L)
{
int i=0,j=L->last;
int temp;
while(i<j)
{
while(L->elem[i]%2 != 0)
i++; /*从表的左半部分开始检测,若为奇数,则i加1,直到找到偶数为止*/
while(L->elem[j]%2 == 0)
j--; /* 从表的右半部分开始检测,若为偶数,则j减1,直到找到奇数为止*/
if(i<j)
{
temp = L->elem[i];
L->elem[i]= L->elem[j];
L->elem[j]=temp;
}
}
}
【例2】
算法实现带头结点单链表的就地逆置问题。
【问题分析】逆置就是使得表中内容由原来的(a1,a2,…,ai-1,ai,ai+1, …,an)变为(an,an-1,…,ai+1,ai,ai-1, …,a1)。就地逆置就是不需要额外申请结点空间,只需要
利用原有的表中的节点空间。若对顺序表中的元素进行逆置,可以借助于“交换”前后相应元素;对单链表中的元素进行逆置,则不能按“交换”思路,
因为对于链表中第 i 个结点需要顺链查找第 n-i+1(链表长度为 n)个结点,逆置链表的时间复杂度将达 O(n2)。
算法思路:逆置后的单链表初始为空,表中的结点不是新生成的,而是从原链表中依次“删除”,再逐个头插入到逆置表中(类同算法 2.5 头查法创建链表)。
设逆置链表的初态为空表,“删除”已知链表中的第一个结点,然后将它“插入”到逆置链表的“表头”,即使它成为逆置链表中“新”的第一个结点,如此循环,直至原链表为空表止。
1) 根据单链表的特点,通过头指针 L 可以顺着每个结点的 next 域,依次访问到 a1,a2,a3…an-1,an;
2)可以借鉴前面讲到过的头插入法建链表的方法,因为头插入法建链表又称为逆序建表法
3)唯一不同的是,不需要重新申请结点空间,而只需要从原有单链表上依次“摘下”结点,之后插入到单链表头结点和表中第一个结点之间即可。如图所示
【算法描述】
void ReverseList(LinkList L)
{
Node *p,*q;
p=L->next;
L->next=NULL;
while(p!=NULL)
{
q=p->next; /*q指针保留p->next得值*/
p->next=L->next;
L->next=p; /*将p结点头插入到单链表L中*/
p=q; /*p指向下一个要插入的结点*/
}
}
【例3】 建立一个带头结点的线性链表,用以存放输入的二进制数,链表中每个结点的data 域存放一个二进制位。并在此链表上实现对二进制数加 1 的运算 。
【问题分析】
①建链表:带二进制数可用带头结点的单链表存储,第一个结点存储二进制数的最高位,依次存储,最后一个结点存储二进制数的最低位。
②二进制加法规则:实现二进制数加 1 运算,方向从低位往高位找到第一个值为 0 的位,从该位开始,对后面所有低位进行求反运算。
③链表实现二进制加 1 时,从高位往低位与运算方向正好相反,从第一个结点开始找,找出最后一个值域为 0 的结点,把该结点值域赋为 1,其后所有结点的值域赋为 0。
④若在链表中未找到值域为 0 的结点,则表示该二进制数各位均为 1,此时,申请一新结点,值域为 1,插入到头结点与原链表的第一个结点之间,成为新链表的第一个结点,其后所有结点的值域赋为 0。
【算法描述】
void BinAdd(LinkList l)
/*单链表实现二进制数加1运算*/
{
Node *q,*r,*temp,*s;
q=l->next;
r=l;
while(q!=NULL) /*查找最后一个值域为0的结点*/
{
if(q->data == 0)
r = q;
q = q->next;
}
if(r != l)
{
r->data = 1; /*将最后一个值域为0的结点的值域赋为1*/
}
else /*未找到值域为0的结点*/
{
temp = r->next;
s=(Node*)malloc(sizeof(Node)); /*申请新结点*/
s->data=1; /*值域赋为1*/
s->next=temp;
r->next = s; /*插入到头结点之后*/
r = s;
}
r = r->next;
while(r!=NULL) /*将后面的所有结点的值域赋为0*/
{
r->data = 0;
r = r->next;
}
}
作业
某线性表中最常用的操作是存取序号为i的元素和在最后进行插入和删除运算,则采用 存储方式时间性能最好。
-
A.双向链表
-
B.双向循环链表
-
C.单向循环链表
-
D.顺序表
已知一个带头结点的非空循环单链表,其尾指针是R,则其首元素结点的地址为:
-
A. R->next
-
B. *( R->next->next )
-
C.&( R->next->next )
-
D. R->next->next
线性表的顺序存储优于链式存储结构。()
-
A.✓
-
B.×
在带头结点的非空单链表中,头结点的存储位置由__头指针____指示
第二章 线性表(一)单元测试(1)
第二章 单元测试(1)
1单选(5分)
在长度为n的顺序表中的第i( 1 <= i <= n+1 )个位置上插入一个元素,其算法时间复杂度为( )。
-
A.O(logn)(以2为底)
-
B.O(n)
-
C.O(1)
-
D.O(n*n)
2单选(5分)
在长度为n的顺序表中的第i( 1 =< i <= n+1 )个位置上插入一个元素,需要移动的元素个数为( )。
-
A.n-i
-
B.i
-
C.n-i-1
-
D.n-i+1
3单选(5分)
链表不具有的特点是( )。
-
A.所需存储空间与线性表程度成正比
-
B.不必事先估计存储空间
-
C.插入、删除不需要移动元素
-
D.可随机访问任一元素
4单选(5分)
在一单链表中,删除指针p所指的后继结点,以下语句正确的是( )。
-
A.free(p->next);p->next=p->next->next;
-
B.s=p->next;p->next=s->next;free(s);
-
C.p->next=p->next->next; free(p->next);
- D. p=p->next;
5单选(5分)
假设删除长度为n的顺序表中的每个元素的概率相同,则删除一个元素平均要移动的元素个数是( )。
-
A.(n-1)/2
-
B.n/2
-
C.n
-
D.(n+1)/2
6单选(5分)
设某顺序表中第一个元素的地址是Base,每个结点占m个单元,则第i个结点的地址为( )。
-
A.Base-i×m
-
B.Base+(i+1)×m
-
C.Base+i×m
-
D.Base+(i-1)×m
7单选(5分)
长度为n的非空线性表采用顺序存储结构,在表的第i个位置插入一个数据元素,i的合法值应该是( )。
-
A.1≤i≤n-1
-
B.i>0
-
C.0≤i≤n+1
-
D.1≤i≤n+1
8单选(5分)
非空单链表结点结构为【data,next】,若指针p所指结点是尾结点,则( )表达式为真。
-
A.p->next!=NULL
-
B.p->next==P
-
C.p==NULL
-
D.p->next==NULL
9单选(5分)
某顺序表的第一个元素的存储地址是500,每个元素占4个单元,则第8个元素的起始地址是( )。
-
A.504
-
B.528
-
C.508
-
D.516
10单选(5分)
在长度为n的顺序表中删除第i(1<=i<=n)个位置上的元素,需要移动的元素个数为( )。
-
A.n-i-1
-
B.i
-
C.n-i+1
-
D.n-i
11判断(5分)
单链表中增加头结点的目的是存储链表的长度。
-
A.✓
-
B.×
12判断(5分)
线性表在链式存储时,查找第i个元素的时间同i的值无关。
-
A.✓
-
B.×
13判断(5分)
线性表在顺序存储时,查找第i个元素的时间同i 的值成正比。
-
A.✓
-
B.×
14判断(5分)
线性表的特点是每个元素都有一个前驱和一个后继。
-
A.✓
-
B.×
15判断(5分)
线性表的链式存储结构优于顺序存储。
-
A.✓
-
B.×
16判断(5分)
顺序表的每个结点只能是一个基本类型,而链表的每个结点可以是一个构造类型。
-
A.✓
-
B.×
17判断(5分)
在顺序表中,逻辑上相邻的两个元素物理存储上也一定也相邻。
-
A.✓
-
B.×
18判断(5分)
在线性表的链式存储结构中,逻辑上相邻的两个元素在物理存储上并不一定紧邻。
-
A.✓
-
B.×
19判断(5分)
线性表采用顺序存储,必须占用一段地址连续的存储单元。
-
A.✓
-
B.×
20判断(5分)
顺序表结构适宜进行随机访问,而链表适宜进行插入、删除。
-
A.✓
-
B.×
第二章 单元测试(2)
第二章 单元测试(2)
1单选(5分)
非空循环单链表L中,p指针指向尾结点,则以下表达式成立的是( )。
-
A.p==NULL
-
B.p->next==NULL
-
C.p==L
-
D.p->next==L
2单选(5分)
若某线性表最常用的操作是存取任一指定序号的元素和在最后进行插入和删除运算,则利用( )存储方式最节省时间。
-
A.双向链表
-
B.带头结点的双循环链表
-
C.顺序表
-
D.单循环链表
3单选(5分)
某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用( )存储方式最节省运算时间。
-
A.单链表
-
B.仅有头指针的单循环链表
-
C.双链表
-
D.带尾指针的单循环链表
4单选(5分)
对于双向循环链表,在两个结点之间插入一个新结点需修改的指针共( )个。
-
A.4
-
B.3
-
C.2
-
D.5
5单选(5分)
将带头指针的长度为m的单链表,链接到同样带头指针的长度为n的单链表末尾。该算法的时间复杂度为( )。
-
A.O(m+n)
-
B.O(m*n)
-
C.O(n)
-
D.O(m)
6单选(5分)
在某双向链表中删除一个结点,需要改动( )个指针域。
-
A.4
-
B.3
-
C.1
-
D.2
7单选(5分)
某双向链表中,结点结构为【prior,data,next】。那么删除p指针所指结点时,需要执行语句:p->next->prior=p->prior; ( ); free(p);
-
A.p->next = p->prior
-
B.p->prior->next = p
-
C.p->prior = p->next
-
D.p->prior->next = p->next
8判断(5分)
静态链表既有顺序存储的优点,又有动态链表的优点。所以,它存取表中第i个元素的时间与i无关。
-
A.✓
-
B.×
9判断(5分)
循环单链表中,每个结点都有一个前驱和后继,因此循环单链表不是线性结构。
-
A.✓
-
B.×
10判断(5分)
静态链表中能容纳的元素个数在链表定义时就确定了,以后不能增加。
-
A.✓
-
B.×
11判断(5分)
静态链表与动态链表在元素的插入、删除上类似,不需做元素的移动。
-
A.✓
-
B.×
12判断(5分)
线性表在顺序存储时,查找第i个元素的时间同i的值无关。
-
A.✓
-
B.×
13判断(5分)
线性表在顺序存储时,删除第i个元素的时间同i的值无关。
-
A.✓
-
B.×
14判断(5分)
静态链表因为采用的是一段连续的空间来存储元素,因此查找第i个元素的时间和i无关。
-
A.✓
-
B.×