第二章 线性表

第二章 线性表

2.1 线性表的定义与特点

同一线性表中的元素必定有相同的特性,即属于同一数据对象,相邻元素之间存在着序偶关系。

诸如此类由n (n>=0)个数据特性相同的元素构成的有限序列称为线性表。
线性表中元素的个数n (n>=0)定义为线性表的长度,n = 0时称为空表。

对于非空的线性表或线性结构, 其特点是:

(1)存在唯一的一个被称作"第一个"的数据元素;
(2)存在唯一的一个被称作“ 最后一个“的数据元素;
(3)除第一个之外,结构中的每个数据元素均只有一个前驱;
(4)除最后一个之外,结构中的每个数据元素均只有一个后继。

2.2 案例引入

一个多项式的运算

稀疏多项式的运算

图书管理系统

2.3 线性表的类型定义

线性表是一个相当灵活的数据结构,其长度可根据需要增长或缩短,即对线性表的数据元素
不仅可以进行访问,而且可以进行插入和删除等操作。下面给出线性表的抽象数据类型定义:

ADT List{
    数据对象:D={ai|ai∈ElemSet,i=1,2,,n,n>=0}
    数据关系:R={<ai-1,ai>|ai-1,aiD,i=1,2,,n}
    基本操作: 
    InitList(&L)
        操作结果:构造一个空的线性表L。
    DestroyList(&L)
        初始条件:线性表L已存在。
        操作结果:销毁线性表L。
    ClearList(&L)
        初始条件:线性表L已存在。
        操作结果:将L重置为空表。
    ListEmpty(L)
        初始条件:线性表L已存在。
        操作结果:若L为空表,则返回true,否则返回false。
    ListLength(L)
        初始条件:线性表L已存在。
        操作结果:返回数据元素个数。
    GetElem(L,i,&e)
        初始条件:线性表L已存在,且1<=i<=ListLength(L)。
        操作结果:用e返回L中第i个数据元素的值。
    LocateElem(L,e)
        初始条件:线性表L已存在。
        操作条件:返回L中第1个值与e相同的元素在L中的位置。若这样的数据元素不存在,则返回值为0.
    PriorElem(L,cur_e,&pre_e)
        初始条件:线性表L已存在。
        操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义。
    NextElem(L,cur_e,&next_e)
        初始条件:线性表L已存在。
        操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回其前驱,否则操作失败,next_e无定义。
    ListInsert(&L,i,e)
        初始条件:线性表L已存在,且1<=i<=ListLength(L)+1。
        操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1。 
    ListDelete(&L,i)
        初始条件:线性表L已存在且非空,且1<=i<=ListLength(L)。
        操作结果:删除L的第i个数据元素,L的长度减1。
    TraverseList(L)
        初始条件:线性表L已存在。
        操作结果:对线性表L进行遍历,在遍历过程中对L的每个结点访问一次。
}ADT List

2.4 线性表的顺序表示和实现

2.4.1 线性表的顺序存储表示

线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素, 这种表示也称作线性表的顺序存储结构或顺序映像。通常, 称这种存储结构的线性表为顺序表。其特点是,逻辑上相邻的数据元素, 其物理次序也是相邻的。

假设线性表的每个元素需占用l个存储单元, 并以所占的第一个单元的存储地址作为数据元素的存储起始位置。则线性表中第i+ 1个数据元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间满足下列关系:
LOC(ai+1) = LOC(ai) + I
一般来说, 线性表的第l个数据元素ai的存储位置为:
LOC(ai) = LOC(a1) + ( i - I) x I

式中,LOC(a1)是线性表的第一个数据元素a1的存储位置,通常称作线性表的起始位置或基地址。

只要确定了存储线性表的起始位置,线性表中任一数据元素都可随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。

通常都用数组来描述数据结构中的顺序存储结构。在此, 由于线性表的长度可变, 且所需最大存储空间随问题不同而不同,则在C语言中可用动态分配的一维数组表示线性表, 描述如下:

//------顺序表的存储结构----
#define MAXSIZE 100            //顺序表可能达到的最大长度
typedef struct
{
    ElemType *elem;            //存储空间的基地址
    int length;                //当前长度
}SqList;                       //顺序表的结构类型为SqList

用顺序表存储案例2.3的图书数据时,其顺序存储结构的类型定义如下:

#define MAXSIZE 10000       //图书表可能达到的最大程度
typedef struct              //图书信息定义
{
    char no[20];            //图书ISBN
    char name[50];          //图书名字
    float price;            //图书价格
}Book;
typedef struct 
{
    Book *elem;             //存储空间的基地址
    int length;             //图书表中当前图书个数
}SqList;                    //图书表的顺序存储结构类型为SqList   
//在上述定义后,可以通过变量定义语句
SqList L;

将L定义为SqList类型的变量,便可以利用L.elem[i-1]访问表中位置序号为i的图书记录。

2.4.2 顺序表中基本操作的实现

1.初始化

顺序表的初始化操作就是构造一个空的顺序表。

算法2.1 顺序表的初始化

【算法步骤】
1.为顺序表L动态分配 一 个预定义大小的数组空间,使elem指向这段空间的基地址。
2.将表的当前长度设为0。
【算法描述】

Status InitList(SqList &L)
{                                       //构造一个空的顺序表L
    L.elem=new ElemType[MAXSIZE];       //为顺序表分配一个大小为MAXSIZE的数组空间
    if(!L.elem)exit(OVERFLOW);          //存储分配失败退出
    L.length=0;                         //空表长度为0
    return Ok;
}

动态分配线性表的存储区域可以更有效地利用系统的资源 , 当不需要该线性表时 , 可以使用
销毁操作及时释放占用的存储空间。

2.取值

取值操作是根据指定的位置序号i, 获取顺序表中第i个数据元素的值。
由千顺序存储结构具有随机存取的特点 , 可以直接通过数组下标定位得到,elem[ i-1]单元存储第i个数据元素。

算法2.2 顺序表的取值
【算法步骤】
1.判断指定的位置序号 i 值是否合理 (I<=i<=L.length), 若不合理,则返回ERROR。
2.若 i 值合理,则将第 i 个数据元素 L.elem[i-1] 赋给参数 e, 通过 e 返回第 1 个数据元素的传值。
【算法描述】

Status GetElem(SqList L,int i,ElemType &e)
{
    if {i<ll li>L.length) return ERROR;       //判断i值是否合理,若不合理, 返回 ERROR
    e = L.elem[i 一 1];                       //elem[i-1] 单元存储第 i 个数据元素
    return OK;
}

3.查找

查找操作是根据指定的元素值e, 查找顺序表中第1个与e相等的元素。若查找成功,则返
回该元素在表中的位置序号;若查找失败,则返回0。

算法2.3 顺序表的查找
【算法步骤】
1.从第一个元素起,依次和 e 相比较,若找到与 e 相等的元素 L.elem[i], 则查找成功,返回
该元素的序号 i+l 。
2.若查遍整个顺序表都没有找到,则查找失败,返回0。

【算法描述】

int LocateELem(SqList L,ElemType e)
{                                     //在顺序表1中查找值为e的数据元素, 返回其序号
    for(i=O;i< L.length;i++)
        if(L.elem[i)==e) return i+l;
    return O;
}

顺序查找按值查找算法的平均时间复杂度为O(n)=(n+1)/2

4.插入

算法2.4 顺序表的插入
[算法步骤】
1.判断插入位置i是否合法(i 值的合法范围是l<=i<=n+ I), 若不合法 则返回 ERROR 。
2.判断顺序表的存储空间是否已满,若满见肤返回 ERROR 。
3.将第n个至第i个位置的元素依次向后移动一个位置,空出第i个位置(i= n+l 时无需移动。
4.将要插入的新元素e放入第i个位置。
5.表长加l。
【算法描述】

Status Listinsert(SqList &L,int i ,ElemType e)
{          //在顺序表 L 中第i个位置之前插入新的元素 e, i 值的合法范围是 1<=i<=L.length+1
    if((i<l)||(i>L.length+1)) return ERROR;
    if(L.length==MAXSIZE) return ERROR;
    for(j=L.length-1; j>=i-1; j--)
       L.elem[j+l]=L.elem[j];
    L.elem[i-l)=e;
    ++L.length;
    return OK;
}

平均时间复杂度O(n)=n/2

5.删除

一 般情况下,删除第 i(1 ::Si::Sn) 个元素时需将第 i+1个至第 n 个元素(共 n-i 个元素) 依次向前移动一个位置(i = n 时无需移动。
算法2.5 顺序表的删除
【算法步骤】
1.判断删除位置 i 是否合法(合法值为1<=i<=n), 若不合法则返回 ERROR。
2.将第i+1个至第n个的元素依次向前移动一个位置 (i = n时无需移动。
3.表长减 1。

【算法描述】

Status ListDelete(SqList &L,int i)
{                  //在顺序表L中删除第i个元素,i值的合法范围是1<=i<=L.length
    if((i<l)||(i>L.length)) return ERROR;
    for (j=i; j <=L.length-1; j ++)
         L.elem[j-1)=L.elem[j];
    --L.length;
    return OK;
}

平均时间复杂度O(n)=(n-1)/2

2.5 线性表的链式表示和实现

2.5.1 单链表的定义和表示

线性表链式存储结构的特点:用 一 组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

结点:数据元素a, 的存储映像,包括以下两个域;

数据域:存储数据元素信息的域;

指针域:存储直接后继存储位置的域;

指针或链:指针域中存储的信息。

n个结点(ai,(1<=i<=n) 的存储映像)链结成 一个链表,即为线性表(a1, a2,…, an)的链式存储结构。又由于此链表的每个结点中只包含 一 个指针域,故又称线性链表单链表。

(根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、
双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表用
千实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。)

将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。

//单链表的存储结构
typedef struct LNode
{ElemType data; //结点的数据域
struct LNode *next;//结点的指针域
}LNode,*LinkList;//LinkList 为指向结构体 LNode 的指针类型

说明:通常习惯上用LinkList 定义单链表,强调定义的是某个单链表的头指针;用 LNode定义指向单链表中任意结点的指针变量。例如,若定义LinkList L,则L为单链表的头指针,若定义LNode * p,则p为指向单链表中某个结点的指针, p代表该结点。当然也可以使用定义LinkListp, 这种定义形式完全等价于 LNode*p。

下面对首元结点头结点头指针三个容易混淆的概念加以说明。

(I) 首元结点是指链表中存储第一个数据元素a 1 的结点。如图 2.8 或图 2.9所示的结点"ZHAO" 。

(2) 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整数型时,头结点的数据域中可存放该线性表的长度。

(3) 头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

链表增加头结点的作用:(1)便于首元结点的处理
(2)便于空表和非空表的统一处理

单链表是非随机存取的存储结构,要取得第i个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存取结构。因此,其基本操作的实现不同于顺序表。

2.5.2 单链表基本操作的实现

1.初始化

算法 2.6 单链表的初始化(只有 一 个头结点的空链表)

【算法步骤】
(1)生成新结点作为头结点,用头指针L 指向头结点。
(2)头结点的指针域置空。
【算法描述】
Status InitList(LinkList &L)
{//构造一个空的单链表L
L=new LNode; //生成新结点作为头结点, 用头指针L指向头结点
1->next=NULL;//头结点的指针域置空
return OK;
}

2.取值

算法 2.7 单链表的取值

【算法步骤】
a.用指针p指向首元结点,用j作计数器初值赋为1。
b.从首元结点开始依次顺着链域 next 向下访问,只要指向当前结点的指针 p 不为空(NULL), 并且没有到达序号为i的结点,则循环执行以下操作:
• p指向下一个结点;
• 计数器j相应加1
c.退出循环时,如果指针p为空,或者计数器j大于i, 说明指定的序号i值不合法(i大于
表长n或i小于等于0), 取值失败返回ERROR; 否则取值成功, 此时j=i时,p所指的结点就
是要找的第l个结点,用参数e保存当前结点的数据域, 返回OK。
【算法描述】
Status GetElem(LinkList L,int i,ElemType &e)
{/ /在带头结点的单链表L中根据序号i.获取元素的值,用e返回L中第i个数据元素的值
p=L->next;j=l; //初始化,p指向首元结点,计数器j初值赋为1
while(p&&j<i) //顺链域向后扫描,直到p为空或p指向第l.个元素
p=p->next;
/ /p指向下一个结点
++j; //计数器j相应加1
if(!p!lj>i)return ERROR; //i值不合法 i>n或i<=0
e=p->data; //取第i个结点的数据域
return OK;
}
【算法分析】
假设每个位置上元素的取值概率相等, 即
pi= 1/n
ASL=1/n[0+1+...+(n-1)]=(n-1)/2   //平均查找长度
单链表取值算法的平均时间复杂度为O(n)。

3.查找

算法2.8 单链表的按值查找

【算法步骤】
a.用指针p指向首元结点 。
b.从首元结点开始依次顺着链域next向下查找, 只要指向当前结点的指针p不为空,并且p所指结点的数据域不等千给定值e, 则循环执行以下操作:p指向下 一 个结点。
c.返回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相关,其平均时间复杂度分析类似于算法 2.7, 也为 O(n)。

4.插入

算法2.9 单链表的插入

【算法步骤]
将值为e的新结点插人到表的第i个结点的位置上,即插入到结点ai-1与ai之间,具体插入5个步骤说明如下:
a.查找结点ai-1 并由指针p指向该结点。
b.生成一个新结点 *s 。
c.将新结点*s的数据域置为e 。
d.将新结点*s的指针域指向结点ai。
e.将结点*p 的指针域指向新结点*s。
【算法描述】
Status Listinsert(LinkList &L,int i,ElemType e)
{//在带头结点的单链表L中第i个位置插入值为e的新结点
p=L;j=O;
while (p && (j<i-1))
{p=p->next;++j;}//查找第i-1个结点,p指向该结点
if (!p I||j>i-1) return ERROR;//i>n+l或者i<1
s = new LNode;//生成新结点*s
s->data = e;//将结点*s的数据域置为e
s->next = p->next;//将结点 *s 的指针域指向结点 a
p->next=s;//将结点*p的指针域指向结点* s
return OK;
}
【算法分析】
平均时间复杂度仍为O(n)

5.删除

算法2.10 单链表的删除

【算法步骤】
删除单链表的具体4个步骤说明如下:
a.查找结点ai-1并由指针p指向该结点。
b.临时保存待删除结点ai的地址在q,以备释放。
c.将结点*p的指针域指向ai的直接后继结点。
d.释放结点ai的空间。
【算法描述】
Status ListDelete(LinkList &L,int i)
{//在带头结点的单链表L中,删除第i个元素
p=L;j=O;
while ((p->next) && (j<i-1))//查找第i-1个结点,p指向该结点
{p=p->next; ++j;}
if (! (p->next) 11 (j>i-1)) return ERROR; //当i>n或i<1时,删除位置不合理
q=p->next;//临时保存被删结点的地址以备释放
p->next=q->next;//改变删除结点前驱结点的指针域
delete q;//释放删除结点的空间
return OK;
}
【算法分析】
类似于插入算法,删除算法时间复杂度亦为O(n)。

6.创建单链表(包括若干个结点的链表)

从空表的初始状态起,依次建立各元素结点,并逐个插入链表。
根据结点插入位置的不同,链表的创建方法可分为前插法后插法

前插法

前插法是通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请 一 个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
算法2.11 前插法创建单链表

【算法步骤】
a.创建一个只有头结点的空链表。
b.根据待创建链表包括的元素个数n, 循环n次执行以下操作:
• 生成 一 个新结点*p;
• 输入元素值赋给新结点*p的数据域;
• 将新结点*p插入到头结点之后。
【算法描述】
void CreateList_H(LinkList &L,int n)
{//逆位序输入n个元素的值,建立带表头结点的单链表L
L=new LNode;
L->next=NULL;//先建立 一 个带头结点的空链表
for(i=O;i<n;++i)
{
p=new LNode;//生成新结点*p
cin>>p->data;//输入元素值赋给新结点*p的数据域
p->next=L->next;L->next=p;//将新结点*p插人到头结点之后
}
}
【算法分析】
算法2.11的时间复杂度为O(n)。
后插法

后插法是通过将新结点逐个插入到链表的尾部来创建链表。 同前插法 一 样, 每次申请 一 个新结点, 读入相应的数据元素值。 不同的是, 为了使新结点能够插入到表尾, 需要增加 一 个尾指 针r指向链表的尾结点。

算法 2.12 后插法创建单链表

【算法步骤】
a.创建 一 个只有头结点的空链表。
b.尾指针r初始化, 指向头结点。
c.根据创建链表包括的元素个数n, 循环n次执行以下操作:
• 生成一个新结点*p;
• 输入元素值赋给新结点*p 的数据域;
• 将新结点*p 插入到尾结点*r之后;
• 尾指针r指向新的尾结点*p。
【算法描述】
void CreateList_R(LinkList &L,int n)
{//正位序输人n个元素的值, 建立带表头结点的单链表L
L=new LNode;
L->next = NULL;//先建立 一 个带头结点的空链表
r = L;//尾指针r指向头结点
for(i = O;i<n;++i)
p=new LNode;//生成新结点
cin>>p->data;//输人元素值赋给新结点*p的数据域
p->next=NULL; r->next=p;//将新结点*p插人尾结点*r之后
r=p;//r指向新的尾结点*p
}
}
【算法分析】
算法2.12的时间复杂度亦为O(n)。

7.补充

1.判断链表是否为空

Status ListEmpty(LinkList L)                        //判断链表是否为空
{
	if (L->next)
		return 0;
	else
		return 1;
}

2.清空链表

Status ClearList(LinkList & L)                    //清空单链表
{
	LNode* p, * q;
	p = L->next;
	while (p)
	{
		q = p->next;
		delete p;
		p = q;
	}
	L->next = NULL;
	return 1;

3.销毁链表

Status DestroyList(LinkList& L)                      //销毁单链表
{
	LNode* p;
	while(L)
	{
		p = L;
		L = L->next;
		delete p;
	}
	return 0;
}

4.判断表长

Status ListLength(LinkList L)                   //求表长
{
	LinkList p;
	p = L->next;
	int i = 0;
	while (p)
	{
		i++;
		p = p->next;
	}
	return i;
}

2.5.3 循环链表

循环链表是另一种形式的链式存储结构。其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他结点。

在单链表中,判别条件为p!=NULL或p- >next!=NULL,而循环单链表的判别条件为p!=L或p- >next!=L 。

如何实现两个链表的连接:(存在两个循环链表,且均有尾指针A,B)

p = B->next->next;
B->next = A->next;
A->next = p;

2.5.4 双向链表

以上讨论的链式存储结构的结点中只有 一 个指示直接后继的指针域, 由此,从某个结点出发只能顺指针向后寻查其他结点。 若要寻查结点的直接前驱,则必须从表头指针出发。 换句话说,在单链表中,查找直接后继结点的执行时间为 0(1), 而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表 (Double Linked List) 。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱, 在 C 语言中可描述如下:

typedef struct DuLNode{
   ElemType data;           //数据域
   struct DuLNode *prior;   //直接前驱
   struct DuLNode *next;    //直接后驱
}DuLNode,*DuLinkList;

并且在双向链表中也有循环链表,并且有

p->next->prior=p=p->prior->next;

链表的插入

Status ListInsert(DuLinkList& L, int i, Elemtype e)        //插入
{
	if (!(p = GetElem(L, 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;
}

链表的删除

Status ListDelete(DuLinkList& L, int i, Elemtype& e)     //删除
{
	if (!(p = GetElem(L, i)))return ERROR;
	e = p->data;
	p->prior->next = p->next;
	p->next->prior = p->prior;
	free(p);
	return OK;
}

2.6 顺序表和链表的比较

1.空间性能

1.存储空间的分配

顺序表:预先分配,元素个数受限,易造成存储空间浪费或空间溢出现象。

链表:无需预先分配,元素个数不受限制。

2.存储密度的大小

存储密度=数据元素本身占用的存储量/结点结构占用的存储量

存储密度越大,存储空间的利用率越高。顺序表的存储密度为1,链表的存储密度小于1。

2.时间性能

1.存取元素的效率

顺序表:数组实现,随机存取结构,取值效率高。

链表:顺序存取结构,从表头开始依次向后遍历,效率低。

2.插入和删除操作的效率

顺序表:进行操作时,平均要移动表中近一半的结点,时间复杂度为O(n)。

链表:无需移动数据,只需修改指针,时间复杂度为O(1)。

2.7 线性表的应用

2.7.1 线性表的合并

问题:集合A和集合B,求它两的并集A=AUB。

例如:A=(2,3,4,5),B=(6,3)

​ 合并后为:A=(2,3,4,5,6)

用算法实现如下:(集体实现可用顺序形式和链表形式)

算法步骤:1.分别获取LA表长m,LB表长n。

​ 2.从LB中第一个数据元素开始,循环n次执行以下操作:

​ 从LB中查找第i(1<=i<=n)个数据元素赋给e;

​ 在LA中查找元素e,如果不存在,则将e插在表LA的最后。

算法描述
void MergeList(List &LA,List LB)
{                         //将所有在线性表LB中但不在LA中的数据元素插入到LA中
     m = ListLength(LA); n = ListLength(LB); //求线性表的长度
     for(i = l;i< = n;i++)
     { 
        GetElem(LB,i,e);
        if(! LocateElem (LA, e))
           Listinsert(LA,++m,e);
     }
}

以上算法的时间复杂度为O(m*n)

2.7.2 有序表的合并

有序表:线性表中的数据元素互相之间可以比较,并且数据元素在线性表中依值非递减或非递增有序排列的线性表。

问题:有序集合是指集合中的元素有序排列。有集合A和集合B,求并集C=AUB。

例如:A=(3,5,8,11),B=(2,6,8,9,11,15,20)

​ 合并后为:C=(2,3,5,6,8,8,9,11,11,15,20)

1.顺序有序表的合并

算法步骤:1.创建一个表长为m+n的空表LC;

​ 2.指针pc初始化,指向LC的第一个元素;

​ 3.指针pa和pb初始化,分别指向LA和LB的第一个元素;

​ 4.当指针pa和pb均未达到相应表尾时,则依次比较pa和pb所指向的元素值,从LA或LB中摘取元素较小的结点插到LC的最后;

​ 5.如果pa已到达LA的表尾,依次将LB的剩余元素插入到LC的最后。

算法描述
void MergeList_Sq(SqList LA,SqList LB,SqList &LC)
{                           // 已知顺序有序表LA和LB的元素按值非递减排列
                            //归并LA和LB得到新的顺序有序表LC, LC的元素也按值非递减排列
    LC.length=LA.length+LB.length;     //新表长度为待合并两表的长度之和
    LC.elem = new ElemType[LC.length]; //为合并后的新表分配一个数组空间
    pc=LC.elem;               //指针pc 指向新表的第一个元素
    pa=LA.elem; pb=LB.elem;   //指针pa 和pb 的初值分别指向两个表的第一个元素
    pa_last = LA.elem+LA.length-1;  //指针pa_last指向LA的最后一个元素
    pb_last = LB.elem+LB.length-1;  //指针pb_last指向LB的最后一个元素
    while ((pa<=pa_last) && (pb<=pb_last)) //LA和LB均未到达表尾
    {
      if(*pa<=*pb)*pc++=*pa++;         //依次摘取两表中较小的结点插入表LC的最后
      else *pc++=*pb++;
    }
    while (pa<=pa_last) *pc++=*pa++;   //LB已到达表尾,依次将LA的剩余元素插入LC的最后;
    while (pb<=pb_last) *pc++=*pb++;
}

该算法的空间复杂度和空间复杂度均为O(m+n)

2.链式有序表的合并

算法步骤:1.指针pa和pb初始化,分别指向LA和LB的第一个结点;

​ 2.LC的结点取值为LA的头结点;

​ 3.指针pc初始化,指向LC的头结点;

​ 4.当指针pa和pb均未达到相应表尾时,则依次比较pa和pb所指向的元素值,从LA或LB中摘取元素值较小的结点插入到LC的最后;

​ 5.将非空表的剩余段插入到pc所指向结点之后;

​ 6.释放LB的头结点。

算法描述
void MergeList_L(LInkList &La,LinkList &LB,LinkList &LC)
{  //已知单链表LA和LB得元素按值非递减排列
   //归并LA和LB得到新的单链表LC,LC的元素也是按值非递减排列
    pa=LA->next;pb=LB->next;        //pa和pb的初值分别指向两个表的第一个结点
    LC=LA;                          //用LA得头结点作为LC的头结点
    pc=LC;                          //pc的初值指向LC的头结点
    while(pa&&pb)
    {
        //LA和LB均未达到表尾
        if(pa->data<=pb->data)      //摘取pa所指结点
        {   
            pc->next=pa;            //将pa所指结点链接到pc所指结点之后
            pc=pa;                  //pc指向pa
            pa=pa->next;            //pa只需下一结点
        }
        else                        
        {
            pc->next=pb;            //
            pc=pb;                  //   
            pb=pb->next;            //
        }
    }
    pc->next=pa?pa:pb;              //将非空表的剩余段插入到pc所指结点之后
    delete LB;                      //释放LB的头结点
}

该算法时间复杂度为O(m+n),空间复杂度为O(1)

2.8案例分析与实现

1.一元多项式的运算

一元多项式可以抽象成一个线性表。在计算机中,我们可以采用数组来表示一元多项式的线性表。利用数组p表示:数组中每个分量p[i]表示多项式每项的系数p;, 数组分最的下标l即对应每项的指数。 数组中非零的分量个数即为多项式的项数。

2.稀疏多项式的运算

稀疏多项式也可以抽象成 一 个线性表。结合2.7节介绍的两个有序表的归并方法,可以看出, 稀疏多项式的相加过程和归并两个有序表的过程极其类似,不同之处仅在于后者在比较数据元素时只出现两种情况(小千等于、大于),而多项式的相加过程在比较两个多项式指数时要考虑三种情况(等于、小千、大千)。因此,多项式相加的过程可以根据算法2.16和算法2.17改进而成。和顺序存储结构相比,利用链式存储结构更加灵活,更适合表示 一 般的多项式,合并过程的空间复杂度为 0(1), 所以较为常用。本节将给出如何利用单链表的基本作来实现多项式的相加运算。

**如何实现用这种单链表表示的多项式的加法运算呢? **

根据多项式相加的运算规则:对于两个多项式中所有指数相同的项,对应系数相加,若其和不为零,则作为“和多项式”中的一项插入到“ 和多项式 "链表中去;对千两个多项式中指数不相同的项,则将指数值较小的项插入到“ 和多项式 "链表中去”和多项式链表中的结点无需生成,而应该从两个多项式的链表中摘取。图2.22所示的两个多项式相加的结果如图2.23所示,图中的长方框表示已被释放的结点。

用链表表示多项式时,每个链表结点存储多项式中的一个非零项,包括系数 ( coef ) 和指数( expn ),两个数据域以及一个指针域 ( next ) 。对应的数据结构定义为:
typedef struct PNode {                       //创建结构体
	float a;        //系数
	int e;          //指数
	struct PNode* next;
}PNode,*PNodenomial;
  1. 多项式的创建

    算法2.18 多项式的创建

    【算法步骤】

    (1)创建 一 个只有头结点的空链表。
    (2)根据多项式的项的个数n, 循环n次执行以下操作:
    • 生成 一 个新结点* s;
    • 输入多项式当前项的系数和指数赋给新结点*s的数据域;
    • 设置 一 前驱指针 pre, 用于指向待找到的第 一 个大于输入项指数的结点的前驱, pre 初值指向头结点;
    • 指针 q 初始化, 指向首元结点;

    •循链向下逐个比较链表中当前结点与输入项指数, 找到第 一 个大于输入项指数的结点 *q;
    • 将输入项结点 *s 插入到结点 *q 之前。

​ 【算法描述】

void CreatePolyn(Polynomial &P,int n)
{//输入m项的系数和指数,建立表示多项式的有序链表P
P=new PNode;
P->next=NULL;//先建立 一 个带头结点的单链表
for(i=l;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
}

【算法分析】时间复杂度为O(n2)。

  1. 多项式的相加

    算法 2.19 多项式的相加

    【算法步骤]
    (1)指针p1和p2初始化, 分别指向Pa和Pb的首元结点。
    (2)p3 指向和多项式的当前结点, 初值为Pa的头结点。
    (3)当指针 p1和 p2均未到达相应表尾时, 则循环比较p1和 p2 所指结点对应的指数值
    (pl->expn与p2->expn), 有下列3种情况:
    • 当pl->expn等于p2->expn时,则将两个结点中的系数相加: 若和不为零,则修改p1所指结点的系数值 , 同时删除p2所指结点

    ​ 若和为零,则删除pl和p2所指结点;
    • 当pl->expn小于p2->expn时,则应摘取pl所指结点插入到“ 和多项式 ”链表中去;
    • 当pl->expn大于p2->expn时,则应摘取p2所指结点插入到“ 和多项式 "链表中去。
    (4)将非空多项式的剩余段插入到 p3 所指结点之后。
    (5)释放Pb的头结点。

    【算法描述】

void AddPolyn{Polynomial &Pa,Polynomial &Pb)
{//多项式加法: Pa=Pa+Pb, 利用两个多项式的结点构成“ 和多项式 ”
pl=Pa->next; p2=Pb->next; //pl 和 p2 初值分别指向 Pa 和 Pb 的首元结点
p3=Pa; //p3 指向和多项式的当前结点, 初值为 Pa
while (p1&&p2) //p1和 p2 均非空
{
if(pl->expn == p2->expn) // 指数相等
{
sum=pl->coef+p2->coef;//sum 保存两项的系数和
if(sum!=O) //系数和不为 0
{
pl->coef = sum; // 修改 Pa 当前结点的系数值为两项系数的和
p3->next=pl; p3=pl; // 将修改后的 Pa 当前结点链在 p3 之后,p3 指向 p1
p1 = p1->next;//p1指向后一项
r = p2; p2 = p2->next; delete r; // 删除 Pb 当前结点, p2 指向后一项
}
else//系数和为 0
{
r = p1; p1=p1->next; delete r; //删除Pa当前结点,p1指向后一项
r = p2; p2 = p2->next; delete r; //删除Pb当前结点,p2指向后一项
}}
else if (p1->expn<p2->expn) //Pa当前结点的指数值小
{
p3->next = p1; //将p1链在p3之后
p3 = p1;//p3指向p1
p1 = p1->next; //p1指向后一项
}
else //Pb当前结点的指数值小
{
p3->next=p2; //将p2链在p3之后
p3 = p2; //p3指向p2
p2=p2->next; //p2指向后一项
}}//while
p3->next = pl?pl:p2;//插入非空多项式的剩余段
delete Pb;//释放Pb的头结点
}

【算法分析】

时间复杂度为O(m + n),空间复杂度为 0(1) 。

3.图书信息管理系统

把图书表抽象成一个线性表,每本图书(包括ISBN、书名、定价)作为线性表中的 一 个元素。在图书信息管理系统中要求实现查找、插入、删除、修改、排序和计数总计6个功能,具体分析如下。
(1) 对千查找、插入、删除这 3 个功能的算法,本章已分别给出了线性表利用顺序存储结构
和链式存储结构表示时相应的算法描述。
(2) 对千修改功能,可以通过调用查找算法,找到满足条件的图书进行修改即可。
(3) 对于排序功能,如果在没有时间复杂度限制的情况下,可以采用读者熟悉的冒泡排序来完成;如果图书数目较多,对排序算法的时间效率要求较高,在学完第8章的内部排序算法后,可以选取 一 种较高效的排序算法来实现,例如,快速排序。
(4)对千计数功能,如果采取顺序存储结构,线性表的长度是它的属性,可以直接通过返回length的值实现图书个数的统计功能,时间复杂度是0(1); 如果采取链式存储结构,则要通过从首元结点开始,附设 一 个计数器进行计数, 一 直“ 数 ”到最后 一 个结点,时间复杂度是O(n) 。

在实现图书信息管理系统时,具体采取哪种存储结构,可以根据实际情况而定。 如果图书数据较多,需要频繁地进行插入和删除操作,则宜采取链表表示;反之,如果图书数据个数变化不大,很少进行插入和删除操作,则宜采取顺序表表示。此案例中所涉及的算法比较基础,但非常重要,读者可以分别采用顺序表和链表实现此案例的相应功能,作为本章内容的实验题目来完成。

posted @ 2022-01-30 00:43  zou-lin1234  阅读(229)  评论(0)    收藏  举报