07习题2.4 递增的整数序列链表的插入(函数题)【PTA浙大版《数据结构(第2版)》题目集】
1.原题链接
习题2.4 递增的整数序列链表的插入 (pintia.cn)
2.题目描述
本题要求实现一个函数,在递增的整数序列链表(带头结点)中插入一个新整数,并保持该序列的有序性。
函数接口定义:
List Insert( List L, ElementType X );
其中List
结构定义如下:
typedef struct Node *PtrToNode;
struct Node {
ElementType Data; /* 存储结点数据 */
PtrToNode Next; /* 指向下一个结点的指针 */
};
typedef PtrToNode List; /* 定义单链表类型 */
L
是给定的带头结点的单链表,其结点存储的数据是递增有序的;函数Insert
要将X
插入L
,并保持该序列的有序性,返回插入后的链表头指针。
裁判测试程序样例:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
typedef struct Node *PtrToNode;
struct Node {
ElementType Data;
PtrToNode Next;
};
typedef PtrToNode List;
List Read(); /* 细节在此不表 */
void Print( List L ); /* 细节在此不表 */
List Insert( List L, ElementType X );
int main()
{
List L;
ElementType X;
L = Read();
scanf("%d", &X);
L = Insert(L, X);
Print(L);
return 0;
}
/* 你的代码将被嵌在这里 */
输入样例:
5
1 2 4 5 6
3
输出样例:
1 2 3 4 5 6
3.参考答案
List Insert( List L, ElementType X ){
List emptyL = (List)malloc(sizeof(struct Node));
List NewL = L;
emptyL->Data = X;
while (NewL->Next) {
if (X<NewL->Next->Data) break;
else NewL = NewL->Next;
}
emptyL->Next = NewL->Next;
NewL->Next = emptyL;
return L;
}
4.解题思路
- 定义一个链表结点用来作为待插入链表的结点,初始化该结点。
- 遍历链表结点,找到Data刚好比X大的结点,将待插入的结点插入到这个结点之前。
5.答案详解
List Insert(List L, ElementType X ){
//定义一个链表结点emptyL并申请内存,用来作为插入链表的结点
List emptyL = (List)malloc(sizeof(struct Node));
//定义一个链表结点NewL,并把L赋给NewL,这是为了遍历链表结点但不改变原链表的头结点
List NewL = L;
//X的值赋给emptyL->Data
emptyL->Data = X;
//遍历链表结点,找到Data刚好比X大的结点NewL->Next
while (NewL->Next) {
if (X<NewL->Next->Data) break;
else NewL = NewL->Next;
}
//插入结点
emptyL->Next = NewL->Next;
NewL->Next = emptyL;
return L;
}
6.知识拓展
本题主要复习C语言中已经学习过的链表,链表也是数据结构线性表不连续存储的具体实现方式。
6.1线性表链式存储结构的特点
- 用一组任意的存储单元存储线性表的数据元素(存储单元可以连续也可以不连续)。
- 为了表示每个数据元素 a i a_i ai 与其直接后继数据元素 a i + 1 a_{i+1} ai+1之间的逻辑关系,对数据元素 a i a_i ai 来说,除了存储其本身的信息之外,还需存储一个指示其前一个元素或后一个元素的信息(即直接前驱或后继的存储位置)。这两部分信息组成的数据单元称为结点(
node
)。 - 结点包括两个域:其中存储数据元素信息的域称为数据域;存储直接前驱或后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成一个链表,即线性表的链式存储结构。
- 如果链表的每个结点中只包含一个指针域,又称线性链表或单链表。
- 根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。
- 单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
- 用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的,逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻。
6.2单链表示意图
通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针,如下图所示。这是因为在使用链表时,关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。
6.3首元结点、头结点、头指针
一般情况下,为了处理方便,在单链表的第一个结点之前附设一个结点,称之为头结点。
首元结点、头结点、头指针三个容易混淆的概念:
- 首元结点是指链表中存储第一个数据元素 a 1 a_1 a1 的结点。
- 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。例如,当数据元素为整数型时,头结点的数据域中可存放该线性表的长度。
- 头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。
6.4头结点的作用
链表增加头结点的作用:
-
便于首元结点的处理
增加了头结点后,首元结点的地址保存在头结点(即其「前驱」结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。
-
便于空表和非空表的统一处理
当链表不设头结点时,假设 L 为单链表的头指针,它应该指向首元结点,则当单链表为长度
n
为 0 的空表时,L
指针为空(判定空表的条件可记为:L==NULL
)。增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如下图(a)所示的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为:
L−>next==NULL
),如下图所示。
顺序表与链表对比
- 在顺序表中,由于逻辑上相邻的两个元素在物理位置上紧邻,则每个元素的存储位置都可从线性表的起始位置计算得到。
- 在单链表中,各个元素的存储位置都是随意的。然而,每个元素的存储位置都包含在其直接前驱结点的信息之中。由此,单链表是非随机存取的存储结构,要取得第i个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存取结构。因此,其基本操作的实现不同于顺序表。
6.5单链表的存储结构
单链表可由头指针唯一确定,在 C 语言中可用「结构指针」来描述:
//单链表的存储结构
typedef struct LNode{
ElemType data; //结点的数据域
struct LNode *next;//结点的指针域
}LNode,*LinkList; //LinkList 为指向结构体 LNode 的指针类型变量
- 这里定义的是单链表中每个结点的存储结构,它包括两部分:存储结点的数据域
data
,其类型用通用类型标识符ElemType
表示;存储后继结点位置的指针域 next,其类型为指向结点的指针类型LNode *
。 - 为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,
LinkList
与LNode *
,两者本质上是等价的。通常习惯上用LinkList
定义单链表,强调定义的是某个单链表的头指针;用LNode *
定义指向单链表中任意结点的指针变量。例如,若定义LinkList L
,则L
为单链表的头指针,若定义LNode *p
,则p
为指向单链表中某个结点的指针,用*p
代表该结点。当然也可以使用定义LinkList p
,这种定义形式完全等价于LNode *p
。 - 单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。若头指针名是 L,则简称该链表为表 L。
- 注意区分指针变量和结点变量两个不同的概念,若定义
LinkList p
或LNode *p
,则p
为指向某结点的指针变量,表示该结点的地址;而*p
为对应的结点变量,表示该结点的名称。
6.6单链表初始化
单链表的初始化操作就是构造一个空表。
【算法步骤】
- 生成新结点作为头结点,用头指针 L 指向头结点。
- 头结点的指针域置空。
【算法描述】
//构造一个空的单链表 L
Status InitList(LinkList *L){
L=new LNode; //生成新结点作为头结点,用头指针 L 指向头结点
L->next=NULL; //头结点的指针域置空
return OK;
}
6.7创建单链表
链表和顺序表不同,它是一种动态结构。整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需预先分配划定,而是由系统按需即时生成。因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。
根据结点插入位置的不同,链表的创建方法可分为前插法和后插法。
6.7.1前插法
前插法是通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
【算法步骤】
- 创建一个只有头结点的空链表。
- 根据待创建链表包括的元素个数n,循环n次执行以下操作:
- 生成一个新结点
*p
; - 输入元素值赋给新结点
*p
的数据域; - 将新结点
*p
插入到头结点之后。
- 生成一个新结点
如图所示为线性表(a,b,c,d,e)
前插法的创建过程,因为每次插入在链表的头部,所以应该逆位序输入数据,依次输入 e、d、c、b、a
,输入顺序和线性表中的逻辑顺序是相反的。
【算法描述】
//逆位序输入 n 个元素的值,建立带表头结点的单链表 L
void CreateList_H(LinkList *L,int n){
L=(LinkList)malloc(sizeof(struct LNode));
L->next=NULL; //先建立一个带头结点的空链表
for(i=0;i<n;++i){
p=(LinkList)malloc(sizeof(struct LNode)); //生成新结点*p
//输入元素值赋给新结点*p 的数据域,这里用c++语法描述,c++的输入不用考虑数据类型
cin>>p->data;
p->next=L->next;L->next=p; //将新结点*p 插入到头结点之后
}
}
6.7.2后插法
后插法是通过将新结点逐个插入到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针 r 指向链表的尾结点。
【算法步骤】
- 创建一个只有头结点的空链表。
- 尾指针 r 初始化,指向头结点。
- 根据创建链表包括的元素个数n,循环n次执行以下操作:
- 生成一个新结点
*p
; - 输入元素值赋给新结点
*p
的数据域; - 将新结点
*p
插入到尾结点*r
之后; - 尾指针
r
指向新的尾结点*p
。
- 生成一个新结点
如图所示为线性表(a,b,c,d,e)
后插法的创建过程,读入数据的顺序和线性表中的逻辑顺序是相同的。
【算法描述】
//正位序输入 n 个元素的值,建立带表头结点的单链表 L
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
}
}
6.8单链表的取值
和顺序表不同,链表中逻辑相邻的结点并没有存储在物理相邻的单元中。根据给定的结点位置序号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
。
- 如果指针 p 为空,或者计数器
【算法描述】
//在带头结点的单链表 L 中根据序号 i 获取元素的值,用 e 返回 L 中第 i 个数据元素的值
Status GetElem(LinkList L,int i,ElemType *e){
//初始化,p 指向首元结点,计数器 j 初值赋为 1
p=L->next;j=1;
//顺链域向后扫描,直到 p 为空或 p 指向第 i 个元素
while(p&&j<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)。
假设每个位置上元素的取值概率相等,即 p i = 1 n p_i=\frac{1}{n} pi=n1,则
A S L = 1 n ∑ i = 1 n ( i − 1 ) = n − 1 2 A S L=\frac{1}{n} \sum_{i=1}^{n}(i-1)=\frac{n-1}{2} ASL=n1i=1∑n(i−1)=2n−1
由此可见,单链表取值算法的平均时间复杂度为 O(n)。
6.9单链表查找
链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将结点值和给定值 e
进行比较,返回查找结果。
【算法步骤】
- 用指针
p
指向首元结点。 - 从首元结点开始依次顺着链域
next
向下查找,只要指向当前结点的指针p
不为空,并且p
所指结点的数据域不等于给定值e
,则循环执行以下操作:p
指向下一个结点。 - 返回
p
。若查找成功,p
此时即为结点的地址值,若查找失败,p
的值即为 NULL。
【算法描述】
//在带头结点的单链表 L 中查找值为 e 的元素
LNode *LocateElem(LinkList L,ElemType 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)。
6.10单链表插入
假设要在单链表的两个数据元素 a
和 b
之间插入一个数据元素 x
,已知 p
为其单链表存储结构中指向结点 a
的指针,如图 (a)所示。
为插入数据元素 x
,首先要生成一个数据域为 x
的结点,然后插入到单链表中。根据插入操作的逻辑定义,还需要修改结点 a
中的指针域,令其指向结点 x
,而结点 x
中的指针域应指向结点 b
,从而实现 3 个元素 a
、b
和 x
之间逻辑关系的变化。插入后的单链表如图(b)所示。假设 s 为指向结点 x 的指针,则上述指针修改用语句描述即为
s->next=p->next;
p->next=s;
【算法步骤】
将值为 e
的新结点插入到表的第i
个结点的位置上,即插入到结点 a i − 1 a_{i-1} ai−1 与 a i a_i ai之间。
- 查找结点 a i − 1 a_{i-1} ai−1并由指针
p
指向该结点。 - 生成一个新结点
*s
。 - 将新结点
*s
的数据域置为e
。 - 将新结点
*s
的指针域指向结点 a i a_i ai。 - 将结点
*p
的指针域指向新结点*s
。
【算法描述】
//在带头结点的单链表 L 中第 i 个位置插入值为 e 的新结点
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>n+1 或者 i<1
s=(LinkList)malloc(sizeof(struct 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
个结点。
6.11单链表的删除
要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱结点。如图所示,在单链表中删除元素 b
时,应该首先找到其前驱结点 a
。为了在单链表中实现元素 a
、b
和 c
之间逻辑关系的变化,仅需修改结点 a
中的指针域即可。假设 p
为指向结点 a
的指针,则修改指针的语句为
p->next=p->next->next;
在删除结点 b
时,除了修改结点 a
的指针域外,还要释放结点 b
所占的空间,所以在修改指针前,应该引入另一指针 q
,临时保存结点 b
的地址以备释放。
【算法步骤】
删除单链表的第i
个结点 a i a_i ai的具体 4 个步骤:
① 查找结点 a i − 1 a_{i−1} ai−1并由指针 p
指向该结点。
② 临时保存待删除结点 a i a_{i} ai的地址在 q
中,以备释放。
③ 将结点*p
的指针域指向 a i a_{i} ai 的直接后继结点。
④ 释放结点 a i a_{i} ai 的空间。
【算法描述】
//在带头结点的单链表 L 中,删除第 i 个元素
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)) //当 i>n 或 i<1 时,删除位置不合理
return ERROR;
q=p->next; //临时保存被删结点的地址以备释放
p->next=q->next; //改变删除结点前驱结点的指针域
free(q); //释放删除结点的空间
return OK;
}
删除算法中的循环条件(p->next&&j<i-1
)和插入算法中的循环条件(p&&(j<i−1)
)是有所区别的。因为插入操作中合法的插入位置有n+1
个,而删除操作中合法的删除位置只有n
个,如果使用与插入操作相同的循环条件,则会出现引用空指针的情况,使删除操作失败。
【算法分析】
类似于插入算法,删除算法时间复杂度亦为 O(n)。
以下代码补全了本题题目中List Read()
和void Print( List L )
的具体实现方式,以便读者在本地IDE调试运行程序。
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
typedef struct Node *PtrToNode;
struct Node {
ElementType Data;
PtrToNode Next;
};
typedef PtrToNode List;
List Read(){
List P, Rear;
int N;
scanf("%d", &N);
P = (PtrToNode)malloc(sizeof(struct Node)); P->Next = NULL;
Rear = P;
while ( N-- ) {
Rear->Next = (PtrToNode)malloc(sizeof(struct Node));
scanf("%d", &Rear->Next->Data);
Rear->Next->Next = NULL;
Rear = Rear->Next;
}
return P;
}
void Print( List L ){
while (L->Next) {
L = L->Next;
printf("%d ", L->Data);
}
printf("\n");
}
List Insert( List L, ElementType X );
int main(){
List L;
ElementType X;
L = Read();
scanf("%d", &X);
L = Insert(L, X);
Print(L);
return 0;
}
List Insert( List L, ElementType X ){
List emptyL = (List)malloc(sizeof(struct Node));
List NewL = L;
emptyL->Data = X;
while (NewL->Next) {
if (X<NewL->Next->Data) break;
else NewL = NewL->Next;
}
emptyL->Next = NewL->Next;
NewL->Next = emptyL;
return L;
}