第一章绪论
1.数据结构(C语言版 第2版严蔚敏)课后习题
1.简述下列概念:数据、数据元素、数据项、数据对象、数据结构、逻辑结构、存储结
构、抽象数据类型。
答案:
数据 :是客观事物的符号表示,指所有能输入到计算机中并被计算机程序处理的符号的
总称。 如数学计算中用到的整数和实数, 文本编辑所用到的字符串, 多媒体程序处理的图形、
图像、声音、动画等通过特殊编码定义后的数据。
数据元素 :是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。在有些
情况下,数据元素也称为元素、结点、记录等。数据元素用于完整地描述一个对象,如一个
学生记录,树中棋盘的一个格局(状态)、图中的一个顶点等。
数据项 :是组成数据元素的、有独立含义的、不可分割的最小单位。
例如,学生基本信息表中的学号、姓名、性别等都是数据项。
数据对象 :是性质相同的数据元素的集合,是数据的一个子集。
例如:整数数据对象是集合 N={0 ,± 1,± 2,, } ,
字母字符数据对象是集合 C={‘A’, ‘ B’,..., ‘ Z’, ‘ a’,‘ b’,... ,‘ z’},学生基本信息表也可是一个数据对象。
数据结构 :是相互之间存在一种或多种特定关系的数据元素的集合。换句话说,数据结构是带“结构”的数据元素的集合, “结构”就是指数据元素之间存在的关系。
逻辑结构 :从逻辑关系上描述数据,它与数据的存储无关,是独立于计算机的。因此,数据的逻辑结构可以看作是从具体问题抽象出来的数学模型。
存储结构: 数据对象在计算机中的存储表示,也称为 物理结构 。
抽象数据类型 :由用户定义的,表示应用问题的数学模型,以及定义在这个模型上的一组操作的总称。具体包括三部分:数据对象、数据对象上关系的集合和对数据对象的基本操作的集合。
2.试举一个数据结构的例子,叙述其逻辑结构和存储结构两方面的含义和相互关系。
答案:
例如有一张学生基本信息表,包括学生的学号、姓名、性别、籍贯、专业等。每个学生基本信息记录对应一个数据元素,学生记录按顺序号排列,形成了学生基本信息记录的线性序列。
对于整个表来说, 只有一个开始结点 ( 它的前面无记录 ) 和一个终端结点 ( 它的后面无记录 ) ,其他的结点则各有一个也只有一个直接前趋和直接后继。 学生记录之间的这种关系就确定了学生表的逻辑结构,即线性结构。
这些学生记录在计算机中的存储表示就是存储结构。 如果用连续的存储单元 ( 如用数组表示 ) 来存放这些记录,则称为顺序存储结构;如果存储单元不连续,而是随机存放各个记录,然后用指针进行链接,则称为链式存储结构。
即相同的逻辑结构,可以对应不同的存储结构。
3.简述逻辑结构的四种基本关系并画出它们的关系图。
答案:
( 1)集合结构
数据元素之间除了“属于同一集合”的关系外,别无其他关系。例如,确定一名学生是否为班级成员,只需将班级看做一个集合结构。
( 2)线性结构
数据元素之间存在一对一的关系。例如,将学生信息数据按照其入学报到的时间先后顺序进行排列,将组成一个线性结构。
( 3)树结构
数据元素之间存在一对多的关系。例如,在班级的管理体系中,班长管理多个组长,每位组长管理多名组员,从而构成树形结构。
( 4)图结构或网状结构
数据元素之间存在多对多的关系。例如,多位同学之间的朋友关系,任何两位同学都可以是朋友,从而构成图形结构或网状结构。
其中树结构和图结构都属于非线性结构。
4.存储结构由哪两种基本的存储方法实现?
答案:
( 1)顺序存储结构
顺序存储结构是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系,通常
借助程序设计语言的数组类型来描述。
( 2)链式存储结构
顺序存储结构要求所有的元素依次存放在一片连续的存储空间中,而链式存储结构,无
需占用一整块存储空间。但为了表示结点之间的关系,需要给每个结点附加指针字段,用于
存放后继元素的存储地址。所以链式存储结构通常借助于程序设计语言的指针类型来描述。
5.选择题
(1)在数据结构中,从逻辑上可以把数据结构分成( )。
A.动态结构和静态结构 B .紧凑结构和非紧凑结构
C.线性结构和非线性结构 D .内部结构和外部结构
答案: C
(2)与数据元素本身的形式、内容、相对位置、个数无关的是数据的( )。
A.存储结构 B .存储实现
C.逻辑结构 D .运算实现
答案: C
逻辑结构反映的是数据元素之间的逻辑关系(抽象),它与数据元素本身的具体形式、内容、相对位置、个数等无关。而存储结构和存储实现与具体的实际存储方式相关,运算实现则与具体的运算操作相关,这些都与数据元素的具体细节有一定关联。
(3)通常要求同一逻辑结构中的所有数据元素具有相同的特性,这意味着( )。
A .数据具有同一特点
B.不仅数据元素所包含的数据项的个数要相同,而且对应数据项的类型要一致
C.每个数据元素都一样
D.数据元素所包含的数据项的个数要相等
答案: B
(4)以下说法正确的是( )。
A.数据元素是数据的最小单位
B.数据项是数据的基本单位
C.数据结构是带有结构的各数据项的集合
D.一些表面上很不相同的数据可以有相同的逻辑结构
答案: D
解释:数据元素是数据的基本单位,数据项是数据的最小单位,数据结构是带有结构
的各数据元素的集合。
(5)算法的时间复杂度取决于( )。
A.问题的规模
B.待处理数据的初态
C.计算机的配置
D. A 和 B
答案: D
解释:算法的时间复杂度不仅与问题的规模有关,还与问题的其他因素有关。如某些排序的算法,其执行时间与待排序记录的初始状态有关。为此,有时会对算法有最好、最坏以及平均时间复杂度的评价。
(6)以下数据结构中, ()是非线性数据结构
A.树 B .字符串 C .队列 D .栈
答案: A
6.试分析下面各程序段的时间复杂度。
(1)
x=90; y=100;
while(y>0)
if(x>100)
{x=x-10;y--;}
else x++;
答案:
解释:程序的执行次数为常数阶。
( 2)
for (i=0; i<n; i++)
for (j=0; j<m; j++)
a[i][j]=0;
答案:
解释:语句 a[i][j]=0; 的执行次数为 m*n。
( 3)
s=0;
for (i=0; i<n; i++)
for(j=0; j<n; j++)
s+=B[i][j];
//代码相当于把二维数组B中第i行第j列的元素的值与当前s的值相加,然后将结果重新赋值给s
sum=s;
答案:
解释:语句 s+=B[i][j]; 的执行次数为 n2。
( 4)
i=1;
while(i<=n)
i=i*3;
答案:
解释:
在每次循环中,i
从 1 开始,每次乘以 3,直到 i
大于 n
为止。
设循环了 k
次后结束循环,那么 3^k>n
,可以推出 k>log₃(n)
。
所以时间复杂度为 O(log₃(n))
。
( 5)
x=0;
for(i=1; i<n; i++)
for (j=1; j<=n-i; j++)
x++;
答案:
解释:语句 x++; 的执行次数为 n-1+n-2+ …+ 1= n(n-1)/2 。
( 6)
x=n; //n>1
y=0;
while(x≥(y+1)*(y+1))
y++;
答案:
解释:谁变,设谁为k,本题中k为y,停止的条件为n
在每次循环中,y
不断递增,而判断条件是 x >= (y+1)*(y+1)
。即n>=(k+1)^2
可以发现,随着 y
的增长,要满足这个条件需要的次数大致是根号 x
次,也就是 O(√n)
(因为 x=n
)。即k约为
2.王道2025版数据结构
1.2.3
系数再大,不及n大
返回值无关
找出基本运算,设执行次数为t,算出最大次数,直接与n比
第二章 线性表
1.数据结构(C语言版 第2版严蔚敏)课后习题
1.选择题
(1)顺序表中第一个元素的存储地址是100,每个元素的长度为2,则第 5 个元素的地址是()。 A. 110
B . 108
C. 100
D. 120
答案: B
解释:顺序表中的数据连续存储,所以第 5 个元素的地址为: 100+2*4=108 。
(2)在 n 个结点的顺序表中,算法的时间复杂度是 O(1) 的操作是() 。
A .访问第 i 个结点( 1≤ i≤ n)和求第 i 个结点的直接前驱( 2≤ i≤ n)
B .在第 i 个结点后插入一个新结点( 1≤ i≤ n)
C.删除第 i 个结点( 1≤ i≤ n)
D .将 n 个结点从小到大排序
答案: A
解释: 在顺序表中插入一个结点的时间复杂度都是 O(n2) ,
排序的时间复杂度为 O(n2 ) 或 O(nlog2n)。
顺序表是一种随机存取结构,访问第 i 个结点和求第 i 个结点的直接前驱都可以直接通过数组的下标直接定位,时间复杂度是 O(1) 。
( 3)向一个有 127 个元素的顺序表中插入一个新元素并保持原来顺序不变,平均要移动的元素个数为() 。
A. 8
B. 63.5
C . 63
D. 7
答案: B
解释:平均要移动的元素个数为: n/2 。
(4)链接存储的存储结构所占存储空间() 。
A .分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
B .只有一部分,存放结点值
C. 只有一部分,存储表示结点间关系的指针
D .分两部分,一部分存放结点值,另一部分存放结点所占单元数
答案: A
( 5)线性表若采用链式存储结构时,要求内存中可用存储单元的地址() 。
A .必须是连续的
B.部分地址必须是连续的
C.一定是不连续的
D.连续或不连续都可以
答案: D
( 6)线性表L在()情况下适用于使用链式结构实现。
A .需经常修改L中的结点值
B.需不断对L进行删除插入
C.L中含有大量的结点
D.L中结点结构复杂
答案: B
解释:链表最大的优点在于插入和删除时不需要移动数据,直接修改指针即可。
( 7)单链表的存储密度() 。
A .大于 1
B .等于 1
C.小于 1
D.不能确定
答案: C
解释:存储密度是指一个结点数据本身所占的存储空间和整个结点所占的存储空间之比,假设单链表一个结点本身所占的空间为 D,指针域所占的空间为 N,则存储密度为: D/(D+N) ,一定小于 1。
( 8)将两个各有 n 个元素的有序表归并成一个有序表,其最少的比较次数是() 。
A . n
B . 2n-1
C . 2n
D . n-1
答案: A
解释:当第一个有序表中所有的元素都小于(或大于)第二个表中的元素,只需要用第二个表中的第一个元素依次与第一个表的元素比较,总计比较 n 次。
(9)在一个长度为 n 的顺序表中,在第 i 个元素( 1≤ i≤ n+1 )之前插入一个新元素时须向后移动()个元素。
A . n-i
B . n-i+1
C. n-i-1
D . 1
答案: B
解释:插入一个新元素时,把后面的再往后拿
(10) 线性表 L=(a1, a2 , an) ,下列说法正确的是()
A .每个元素都有一个直接前驱和一个直接后继
B .线性表中至少有一个元素
C.表中诸元素的排列必须是由小到大或由大到小
D .除第一个和最后一个元素外,其余每个元素都有一个且仅有一个直接前驱和直接
后继。
答案: D
(11) 创建一个包括 n 个结点的有序单链表的时间复杂度是() 。
A .O(1)
B .O(n)
C .O(n2)
D .O(nlog 2n)
答案: C
解释:单链表创建的时间复杂度是 O(n) ,而要建立一个有序的单链表,则每生成一个新结点时需要和已有的结点进行比较,确定合适的插入位置,所以时间复杂度是O(n2) 。
(12) 以下说法错误的是() 。
A .求表长、定位这两种运算在采用顺序存储结构时实现的效率不比采用链式存储结构时实现的效率低
B .顺序存储的线性表可以随机存取
C.由于顺序存储要求连续的存储区域,所以在存储管理上不够灵活
D.线性表的链式存储结构优于顺序存储结构
答案: D
解释: 链式存储结构和顺序存储结构各有优缺点,有不同的适用场合。
顺序存储结构的优点:
- 随机访问效率高,可以快速通过索引直接访问元素。
- 存储密度高,空间利用率较好。
顺序存储结构的缺点:
- 插入和删除操作可能需要移动大量元素,效率较低。
- 不易动态扩展存储空间,可能需要重新分配较大的连续空间。
链式存储结构的优点:
- 插入和删除操作相对简单,只需修改指针即可,无需大规模移动元素。
- 易于动态扩展,不需要预先分配连续的大空间。
链式存储结构的缺点:
- 随机访问效率较低,需要通过指针依次遍历查找。
- 存储密度相对较低,需要额外的指针空间来存储链接信息。
(13) 在单链表中,要将 s 所指结点插入到 p 所指结点之后,其语句应为() 。
A . s->next=p+1; p->next=s;
B. (*p).next=s; (*s).next=(*p).next;
C. s->next=p->next; p->next=s->next;
D. s->next=p->next; p->next=s;
答案: D
解释:这两行代码通常是在链表操作中使用。
`s->next = p->next` 这一步是让当前节点 `s` 的下一个节点指向原来节点 `p` 的下一个节点。
也就是说,把 `p` 后面的节点链接到 `s` 后面。
`p->next = s` 这一步是将节点 `p` 的下一个节点设置为当前节点 `s` 。
这样就把 `s` 插入到了 `p` 之后,实现了在链表中插入节点 `s` 的操作。通过这两步操作,就成功地将节点 `s` 插入到了节点 `p` 和 `p` 的原来下一个节点之间。
(14) 在双向链表存储结构中,删除 p 所指的结点时须修改指针() 。
A . p->next->prior=p->prior; p->prior->next=p->next;
B . p->next=p->next->next; p->next->prior=p;
C. p->prior->next=p; p->prior=p->prior->prior;
D . p->prior=p->next->next; p->next=p->prior->prior;
答案: A
解释:这两行代码通常解释:也是在对链表进行某种操作。
`p->next->prior=p->prior`
这里先通过 `p->next` 找到 `p` 的下一个节点,然后将这个下一个节点的前驱指针(`prior`)设置为 `p` 的前驱节点(也就是将 `p` 的下一个节点的前驱指向 `p` 的前驱)。
`p->prior->next=p->next`
通过 `p->prior` 找到 `p` 的前驱节点,然后将这个前驱节点的后继指针(`next`)设置为 `p` 的后继节点(也就是将 `p` 的前驱节点的后继指向 `p` 的后继)。
这样的操作通常是在对节点 `p` 进行移除或调整其在链表中的位置等操作时使用,以确保链表的前后链接关系正确调整。
(15) 在双向循环链表中,在 p 指针所指的结点后插入 q 所指向的新结点,其修改指针的操作是() 。
A . p->next=q; q->prior=p; p->next->prior=q; q->next=q;
B . p->next=q; p->next->prior=q; q->prior=p; q->next=p->next;
C. q->prior=p; q->next=p->next; p->next->prior=q; p->next=q;
D. q->prior=p; q->next=p->next; p->next=q; p->next->prior=q;
答案: C
解释:
`q->prior=p`
这一步将节点 `q` 的前驱指针指向节点 `p` ,建立了 `q` 与 `p` 的前向关联。
`q->next=p->next`
让 `q` 的后继指针指向节点 `p` 原本的后继节点,即把 `q` 插入到 `p` 之后的位置。
`p->next->prior=q`
由于 `p` 的后继节点(原本是别的节点,现在是 `q` )的前驱指针要更新为 `q` 。
`p->next=q`
最后这一步正式将 `p` 的后继节点设置为 `q` ,完成节点 `q` 在链表中插入到 `p` 后面的整个操作过程。这样就通过这几步操作成功地将 `q` 插入到了 `p` 之后,并且维护好了相关节点的前后指针关系。
2.算法设计题
( 1)将两个递增的有序链表合并为一个递增的有序链表。 要求结果链表仍使用原来两个
链表的存储空间 , 不另外占用其它的存储空间。表中不允许有重复的数据。
[ 题目分析 ]
合并后的新表使用头指针 Lc 指向, pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为
相应链表的第一个结点,从第一个结点开始进行比较,当两个链表 La 和 Lb 均为到达表尾结
点时,依次摘取其中较小者重新链接在 Lc 表的最后。如果两个表中的元素相等,只摘取 La
表中的元素,删除 Lb 表中的元素,这样确保合并后表中无重复的元素。当一个表 到达表尾结
点,为空时,将非空表的剩余元素直接链接在 Lc 表的最后。
[ 算法描述 ]
void MergeList(LinkList &La,LinkList &Lb,LinkList &Lc)
{// 合并链表 La 和 Lb,合并后的新表使用头指针 Lc 指向
pa=La->next; pb=Lb->next;
//pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为相应链表的第一个结点
Lc=pc=La; // 用 La 的头结点作为 Lc 的头结点
while(pa && pb) {
if(pa->data<pb->data)
{pc->next=pa;pc=pa;pa=pa->next;}
// 取较小者 La 中的元素,将 pa 链接在 pc 的后面, pa 指针后移
else if(pa->data>pb->data)
{pc->next=pb; pc=pb; pb=pb->next;}
// 取较小者 Lb 中的元素,将 pb 链接在 pc 的后面, pb 指针后移
else // 相等时取 La 中的元素,删除 Lb 中的元素
{
pc->next=pa;
pc=pa;pa=pa->next;
q=pb->next;
delete pb ;
pb =q;
}
}
pc->next=pa?pa:pb; // 插入剩余段
delete Lb; // 释放 Lb 的头结点
}
( 2)将两个非递减的有序链表合并为一个非递增的有序链表。 要求结果链表仍使用原来
两个链表的存储空间 , 不另外占用其它的存储空间。表中允许有重复的数据。
[ 题目分析 ]
合并后的新表使用头指针 Lc 指向, pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为
相应链表的第一个结点,从第一个结点开始进行比较,当两个链表 La 和 Lb 均为到达表尾结
点时,依次摘取其中较小者重新链接在 Lc 表的表头结点之后,如果两个表中的元素相等,只
摘取 La 表中的元素,保留 Lb 表中的元素。当一个表到达表尾结点,为空时,将非空表的剩
余元素依次摘取,链接在 Lc 表的表头结点之后。
[ 算法描述 ]
void MergeList(LinkList& La, LinkList& Lb, LinkList& Lc, )
{// 合并链表 La 和 Lb,合并后的新表使用头指针 Lc 指向
pa=La->next; pb=Lb->next;
//pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为相应链表的第一个结点
Lc=pc=La; // 用 La 的头结点作为 Lc 的头结点
Lc->next=NULL;
while(pa||pb )
{// 只要存在一个非空表,用 q 指向待摘取的元素
if(!pa)
{q=pb; pb=pb->next;}
//La 表为空,用 q 指向 pb , pb 指针后移
else if(!pb)
{q=pa; pa=pa->next;}
//Lb 表为空,用 q 指向 pa , pa 指针后移
else if(pa->data<=pb->data)
{q=pa; pa=pa->next;}
// 取较小者(包括相等) La 中的元素,用 q 指向 pa, pa 指针后移
else
{q=pb; pb=pb->next;}
// 取较小者 Lb 中的元素,用 q 指向 pb, pb 指针后移
q->next = Lc->next; Lc->next = q;
// 将 q 指向的结点插在 Lc 表的表头结点之后
}
delete Lb; // 释放 Lb 的头结点
}
( 3)已知两个链表 A 和 B 分别表示两个集合,其元素递增排列。请设计算法求出 A 与 B
的交集,并存放于 A 链表中。
[ 题目分析 ]
只有同时出现在两集合中的元素才出现在结果表中 , 合并后的新表使用头指针 Lc 指向。
pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为相应链表的第一个结点, 从第一个结点开
始进行比较,当两个链表 La 和 Lb 均为到达表尾结点时,如果两个表中相等的元素时,摘取
La 表中的元素,删除 Lb 表中的元素;如果其中一个表中的元素较小时,删除此表中较小的
元素,此表的工作指针后移。当链表 La 和 Lb 有一个到达表尾结点,为空时,依次删除另一
个非空表中的所有元素。
[ 算法描述 ]
void Mix(LinkList& La, LinkList& Lb, LinkList& Lc)
{pa=La->next;pb=Lb->next;
pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为相应链表的第一个结点
Lc=pc=La; // 用 La 的头结点作为 Lc 的头结点
while(pa&&pb)
{
if(pa->data==pb- >data) ∥交集并入结果表中。
{ pc->next=pa;pc=pa;pa=pa->next;
u=pb;pb=pb->next; delete u;}
else if(pa->data<pb->data)
{u=pa;pa=pa->next; delete u;}
else
{u=pb; pb=pb->next; delete u;}
}
while(pa){u=pa; pa=pa->next; delete u;} ∥ 释放结点空间
while(pb) {u=pb; pb=pb->next; delete u ;} ∥释放结点空间
pc- >next=null; ∥置链表尾标记。
delete Lb; // 释放 Lb 的头结点
}
( 4)已知两个链表 A 和 B 分别表示两个集合,其元素递增排列。请设计算法求出两个集
合 A 和 B 的差集(即仅由在 A 中出现而不在 B 中出现的元素所构成的集合) ,并以同样的形
式存储,同时返回该集合的元素个数。
[ 题目分析 ]
求两个集合 A 和 B 的差集是指在 A 中删除 A 和 B 中共有的元素,即删除链表中的相应结
点 , 所以要保存待删除结点的前驱,使用指针 pre 指向前驱结点。 pa 和 pb 分别是链表 La 和
Lb 的工作指针 , 初始化为相应链表的第一个结点,从第一个结点开始进行比较,当两个链表
La 和 Lb 均为到达表尾结点时,如果 La 表中的元素小于 Lb 表中的元素, pre 置为 La 表的工
作指针 pa 删除 Lb 表中的元素;如果其中一个表中的元素较小时,删除此表中较小的元素,
此表的工作指针后移。 当链表 La 和 Lb 有一个为空时, 依次删除另一个非空表中的所有元素。
[ 算法描述 ]
void Difference ( LinkList& La, LinkList& Lb,int *n ) { ∥差集的结果存储于单链表 La 中, *n 是结果集合中元素个数,调用时为 0
pa=La->next; pb=Lb->next;
∥ pa 和 pb 分别是链表 La 和 Lb 的工作指针 , 初始化为相应链表的第一个结点
pre=La; ∥ pre 为 La 中 pa 所指结点的前驱结点的指针
while ( pa&&pb)
{
if ( pa->data<q->data )
{pre=pa;pa=pa->next;*n++;}
∥ A 链表中当前结点指针后移
else if ( pa->data>q->data )
q=q->next; ∥ B 链表中当前结点指针后移
else
{
pre->next=pa->next; ∥处理 A, B 中元素值相同的结点,应删除
u=pa; pa=pa->next;delete u;
} ∥删除结点
}
}
( 5)设计算法将一个带头结点的单链表 A 分解为两个具有相同结构的链表 B、C,其中 B
表的结点为 A 表中值小于零的结点,而 C 表的结点为 A 表中值大于零的结点(链表 A 中的元
素为非零整数,要求 B、 C 表利用 A 表的结点) 。 [ 题目分析 ]
B 表的头结点使用原来 A 表的头结点,为 C 表新申请一个头结点。从 A 表的第一个结点
开始,依次取其每个结点 p,判断结点 p 的值是否小于 0,利用前插法,将小于 0 的结点插入
B 表 , 大于等于 0 的结点插入 C 表。
[ 算法描述 ]
void DisCompose(LinkedList A)
{
B=A;
B->next= NULL; ∥ B 表初始化
C=new LNode;∥为 C 申请结点空间
C->next=NULL; ∥ C 初始化为空表
p=A->next; ∥ p 为工作指针
while(p!= NULL)
{
r=p->next; ∥暂存 p 的后继
if(p->data<0)
{p->next=B->next; B- >next=p; } ∥将小于 0 的结点链入 B 表 , 前插法
else
{p->next=C->next; C- >next=p; }∥将大于等于 0 的结点链入 C 表 , 前插法
p=r; ∥p 指向新的待处理结点。
}
}
( 6)设计一个算法,通过一趟遍历在单链表中确定值最大的结点。
[ 题目分析 ]
假定第一个结点中数据具有最大值,依次与下一个元素比较,若其小于下一个元素,则
设其下一个元素为最大值,反复进行比较,直到遍历完该链表。
[ 算法描述 ]
ElemType Max (LinkList L ){
if(L->next==NULL) return NULL;
pmax=L->next; // 假定第一个结点中数据具有最大值
p=L->next->next;
while(p != NULL ){// 如果下一个结点存在
if(p->data > pmax->data) pmax=p;// 如果 p 的值大于 pmax 的值,则重新赋值
p=p->next;// 遍历链表
}
return pmax->data;
( 7)设计一个算法,通过遍历一趟,将链表中所有结点的链接方向逆转,仍利用原表的
存储空间。
[ 题目分析 ]
从首元结点开始,逐个地把链表 L 的当前结点 p 插入新的链表头部。
[ 算法描述 ]
void inverse(LinkList &L)
{// 逆置带头结点的单链表 L
p=L->next; L->next=NULL;
while ( p) {
q=p->next; // q 指向 *p 的后继
p->next=L->next;
L->next=p; // *p 插入在头结点之后
p = q;
}
}
( 8)设计一个算法,删除递增有序链表中值大于 mink 且小于 maxk 的所有元素( mink
和 maxk 是给定的两个参数,其值可以和表中的元素相同,也可以不同 )。 [ 题目分析 ]
分别查找第一个值 >mink 的结点和第一个值 ≥ maxk 的结点,再修改指针,删除值大于
mink 且小于 maxk 的所有元素。
[ 算法描述 ]
void delete(LinkList &L, int mink, int maxk) {
p=L->next; // 首元结点
while (p && p->data<=mink)
{ pre=p; p=p->next; } // 查找第一个值 >mink 的结点
if (p) {
while (p && p->data<maxk)
p=p->next;
// 查找第一个值 ≥ maxk 的结点
q=pre->next; pre->next=p; // 修改指针
while (q!=p) {
s=q->next; delete q; q=s;
} // 释放结点空间
}//if
}
( 9)已知 p 指向双向循环链表中的一个结点, 其结点结构为 data 、prior 、next 三个域,
写出算法 change§, 交换 p 所指向的结点和它的前缀结点的顺序。
[ 题目分析 ]
知道双向循环链表中的一个结点,与前驱交换涉及到四个结点( p 结点,前驱结点,前
驱的前驱结点,后继结点)六条链。
[ 算法描述 ]
void Exchange ( LinkedList p ) ∥ p 是双向循环链表中的一个结点,本算法将 p 所指结点与其前驱结点交换。
{q=p->llink ;
q->llink->rlink=p ;∥ p 的前驱的前驱之后继为 p
p->llink=q->llink ;∥ p 的前驱指向其前驱的前驱。
q->rlink=p->rlink ;∥ p 的前驱的后继为 p 的后继。
q->llink=p ;∥ p 与其前驱交换
p->rlink->llink=q ;∥ p 的后继的前驱指向原 p 的前驱
p->rlink=q ;∥ p 的后继指向其原来的前驱
} ∥算法 exchange 结束。
( 10)已知长度为 n 的线性表 A 采用顺序存储结构,请写一时间复杂度为 O(n) 、空间复
杂度为 O(1) 的算法,该算法删除线性表中所有值为 item 的数据元素。
[ 题目分析 ]
在顺序存储的线性表上删除元素,通常要涉及到一系列元素的移动(删第 i 个元素,第
i+1 至第 n 个元素要依次前移) 。本题要求删除线性表中所有值为 item 的数据元素,并未要
求元素间的相对位置不变。因此可以考虑设头尾两个指针( i=1 , j=n ),从两端向中间移动,
凡遇到值 item 的数据元素时,直接将右端元素左移至值为 item 的数据元素位置。
[ 算法描述 ]
void Delete ( ElemType A[ ] , int n ) ∥ A 是有 n 个元素的一维数组,本算法删除 A 中所有值为 item 的元素。
{
i=1 ; j=n ;∥设置数组低、高端指针(下标) 。
while ( i<j ){
while ( i<j && A[i]!=item )
i++ ;∥若值不为 item ,左移指针。
if ( i<j )
while ( i<j && A[j]==item )
j-- ;∥若右端元素为 item ,指针左移
if ( i<j ) A[i++]=A[j--] ;
}
2.王道2025版数据结构
一、单项选择题
01.下述( )是顺序存储结构的优点
A.存储密度大
B.插入运算方便
C.删除运算方便
D.方便地运用于各种逻辑结构的存储表示
顺序表不像链表那样要在结点中存放指针域,因此存储密度较大,选项A正确。选项B和C
是链表的优点。选项D是错误的,比如对于树形结构,顺序表显然不如链表表示起来方便。
02.下列关于顺序表的叙述中,正确的是( )。
A.顺序表可以利用一维数组表示,因此顺序表与一维数组在逻辑结构上是相同的
B.在顺序表中,逻辑上相邻的元素物理位置上不一定相邻
C.顺序表和一维数组一样,都可以进行随机存取
D.在顺序表中,每个元素的类型不必相同
03.线性表的顺序存储结构是一种( )。
A.随机存取的存储结构
B.顺序存取的存储结构
C.索引存取的存储结构
D.散列存取的存储结构
04.通常说顺序表具有随机存取的特性,指的是( ).
A.查找值为x的元素的时间与顺序表中元素个数n无关
B. 查找值为x的元素的时间与顺序表中元素个数n有关
C.查找序号为i的元素的时间与顺序表中元素个数n无关
D.查找序号为i的元素的时间与顺序表中元素个数n有关
05.一个顺序表所占用的存储空间大小与( )无关。
A.表的长度
B.元素的存放顺序
C.元素的类型
D.元素中各字段的类型
顺序表所占的存储空间 = 表长xsizeof(元素的类型)
06.若线性表最常用的操作是存取第i个元素及其前驱和后继元素的值,为了提高效率,应采用( )的存储方式。
A.单链表
B. 双向链表
C.单循环链表
D.顺序表
A、B、C都只能从头结点依次顺序查找,时间复杂度为O(n);只有顺序表可以按序号随机存取,时间复杂度为O(1)。
07.一个线性表最常用的操作是存取任意一个指定序号的元素并在最后进行插入、删除操作,则利用( )存储方式可以节省时间。
A.顺序表
B.双链表
C.带头结点的双循环链表
D.单循环链表
只有顺序表可以按序号随机存取,且在最后进行插入和删除操作时不需要移动任何元素。
08.在n个元素的线性表的数组表示中,时间复杂度为O(1)的操作是( )。
I.访问第i(1≤i≤n)个结点和求第i(2≤i≤n)个结点的直接前驱
II.在最后一个结点后插入一个新的结点
Ⅲ.删除第1个结点
IV.在第i(1≤i≤n)个结点后插入一个结点
A.I
B.II、Ⅲ
C. I、Ⅱ
D.I、Ⅱ、Ⅲ
对于I,在最后位置插入新结点不需要移动元素,时间复杂度为O(1):
对于Ⅲ,被删结点后的结点需要依次前移,时间复杂度为O(n);
对于IV,需要后移n-i个结点,时间复杂度为O(n)
09.设线性表有n个元素,严格说来,以下操作中,( )在顺序表上实现要比在链表上实现的效率高。
I.输出第i(l≤i≤n)个元素值
Ⅱ.交换第3个元素与第4个元素的值
Ⅲ.顺序输出这n个元素的值
A. I
B.I、Ⅲ
C.I、Ⅱ
D.Ⅱ、Ⅲ
对于Ⅱ,顺序表只需要3次交换操作;链表则需要分别找到两个结点前驱,第4个结点断链后再插入到第2个结点后,效率较低。对于Ⅲ,需依次顺序访问每个元素,时间复杂度相同
10.在一个长度为n的顺序表中删除第i(1≤i≤n)个元素时,需向前移动( )个元素。
A. n
B. i-1
C.n-i
D.n-i+1
11.对于顺序表,访问第i个位置的元素和在第i个位置插入一个元素的时间复杂度为( ).
A. O(n),O(n)
B. O(n),O(1)
C.O(1),O(n)
D.O(1),O(1)
12.对于顺序存储的线性表,其算法时间复杂度为O(1)的运算应该是( )。
A.将n个元素从小到大排序
B.删除第i(1≤i≤n)个元素
C.改变第i(1≤i≤n)个元素的值
D.在第i(1≤i≤n)个元素后插入一个新元素
对n个元素进行排序的时间复杂度最小也要O(n)(初始有序时),通常为O(nlogzn)或O(n2)
13.若长度为n的非空线性表采用顺序存储结构,在表的第i个位置插入一个数据元素,则
i的合法值应该是( )。
A. l≤i≤n
B. l≤i≤n+1
C.0≤i≤n-1
D.0≤i≤n
14.顺序表的插入算法中,当n个空间已满时,可再申请增加分配m个空间,若申请失败,
则说明系统没有( )可分配的存储空间。
A.m个
B.m个连续
C.n+m个
D.n+m个连续
15.【2023 统考真题】在下列对顺序存储的有序表(长度为 n)实现给定操作的算法中,平均时间复杂度为O(1)的是()。
A.查找包含指定值元素的算法
B.插入包含指定值元素的算法
C.删除第i(1≤i≤n)个元素的算法
D.获取第i(l≤i≤n)个元素的算法
对于顺序存储的有序表,查找指定值元素可以采用顺序查找法或折半查找法,平均时间复杂度最少为O(logn)。
插入指定值元素需要先找到插入位置,然后将该位置及之后的元素依次后移一个位置,最后将指定值元素插入到该位置,平均时间复杂度为O(n)。
删除第i个元素需要将该元素之后的全部元素依次前移一个位置,平均时间复杂度为O(n)。
获取第i个元素只需直接根据下标读取对应的数组元素即可,时间复杂度为O(1)
二、综合应用题
01.从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删元素的值。空出的位
置由最后一个元素填补,若顺序表为空,则显示出错信息并退出运行。
算法思想:搜索整个顺序表,查找最小值元素并记住其位置,
搜索结束后用最后一个元素填补空出的原最小值元素的位置。
---------------------------------------------------------------------
bool Del_Min(Sqlist &L,ElemType &value){
//删除顺序表L中最小值元素结点,并通过引用型参数 value 返回其值
//若删除成功,则返回true;否则返回 false
if(L.length==0)
return false; //表空,中止操作返回
value=L.data[0];
int pos=0; //假定0号元素的值最小
for(int i=1;i<L.length;i++) //循环,寻找具有最小值的元素
if(L.data[i]<value){ //让 value 记忆当前具有最小值的元素
value=L.data[i];
pos=i;
}
L.data[pos]=L.data[L.length-1]; //空出的位置由最后一个元素填补
L.length--;
return true; //此时,value 即为最小值
}
//本题也可用函数返回值返回,两者的区别是:
//函数返回值只能返回一个值,而参数返回(引用传参)可以返回多个值
----------------------------------------------------------
02.设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)。
算法思想:扫描顺序表L的前半部分元素,对于元素L.data[i](0<=i<L.length/2),
将其与后半部分的对应元素L.data[L.length-i-1]进行交换。
本题代码如下:
void Reverse(Sqlist 6L){
ElemType temp; //辅助变量
for(int i=0;i<L.length/2;i++){
temp=L.data[i]; //交换L.data[i]与L.data[L.length-i-1]
L.data[i]=L,data[L.length-i-1];
L.data[L.length-i-1]=temp;
}
}
03.对长度为n的顺序表 L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除顺序表中所有值为x的数据元素。
解法1:用k记录顺序表L中不等于x的元素个数(即需要保存的元素个数),扫描时将不等
于x的元素移动到下标k的位置,并更新k值。扫描结束后修改L的长度。
本题代码如下:
void del_x_1(SqList kL,ElemType x){
//本算法实现删除顺序表L中所有值为x的数据元素
int k=0,i; //记录值不等于x的元素个数
for(i=0;i<L.length;i++)
if(L.data[i]!=x){
L.data[k]=L.data[i];
k++; //不等于x的元素增1
}
L.length=k; 1/顺序表L的长度等于k
)
解法2:用k记录顺序表L中等于x的元素个数,一边扫描L,一边统计k,并将不等于x的
元素前移k个位置。扫描结束后修改L的长度。
本题代码如下:
void del_x_2(SqList 6L,ElemType x){
int k=0,i=0; //k 记录值等于x的元素个数
while(i<L.length)(
if(L.data[i]==x)
k++;
else
L.data[i-k]=L.data[i];//当前元素前移k个位置
i++;
}
L.length=L.length-k; //顺序表L的长度递减
)
04.从顺序表中删除其值在给定值s和1之间(包含s和t,要求s<1)的所有元素,若s或1不合理或顺序表为空,则显示出错信息并退出运行。
算法思想:从前向后扫描顺序表L,用k记录值在s和1之间的元素个数(初始时k=0)。对
于当前扫描的元素,若其值不在s和t之间,则前移 k个位置;否则执行 k++。由于每个不在s
和t之间的元素仅移动一次,因此算法效率高。
本题代码如下:
bool Del_s_t(SqList 6L,ElemType s,ElemType t)(
//删除顺序表L中值在给定值s和t(要求s<t)之间的所有元素
int i,k=0;
if(L.length==0l|s>=t)
return false; //线性表为空或 s、t不合法,返回
for(i=0;i<L.length;i++)(
if(L.data[i]>=s66L.data[i]<=t)
k++;
else
L.data[i-k]=L.data[i]; //当前元素前移k个位置
}//for
L.length-=k; //长度减小
return true;
}
05.从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。
算法思想:注意是有序顺序表,值相同的元素一定在连续的位置上,
用类似于直接插入排序的思想,初始时将第一个元素视为非重复的有序表。
之后依次判断后面的元素是否与前面非重复有序表的最后一个元素相同,
若相同,则继续向后判断,
若不同,则插入前面的非重复有序表的,
最后,直至判断到表尾为止。
本题代码如下:
bool Delete_Same(SeqList6 L){
if(L.length==0)
return false;
int i,j; //i存储第一个不相同的元素,j为工作指针
for(i=0,j=1;j<L.length;j++)
if(L.data[i]!=L.data[j]) //查找下一个与上个元素值不同的元素
L.data[++i]=L.data[j]; //找到后,将元素前移
L.length=i+1;
return true;
}
06.将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。
算法思想:首先,按顺序不断取下两个顺序表表头较小的结点存入新的顺序表中。
然后,看哪个表还有剩余,将剩下的部分加到新的顺序表后面。
本题代码如下:
bool Merge(Seqlist A,Seqlist B,Seqlist &ac){
//将有序顺序表 A与B合并为一个新的有序顺序表 c
if(A.length+B.length>C.maxSize) //Ⅱ大于顺序表的最大长度
return false;
int i=0,j=0,k=0;
while(i<A.lengthabj<B.length){ //循环,两两比较,小者存入结果表
if(A.data[i]<=B,data[j])
C.data[k++]=A.data[i++];
else
C.data[k++]=B.data[j++];
}
while(i<A.length) //还剩一个没有比较完的顺序表
C.data[k++]=A.data[i++];
while(j<B.length)
C.data[k++]=B.data[j++];
C.length=k;
return true;
}
07.已知在一维数组A[m+m]中依次存放两个线性表(a1,a2,a3,…,am)和(b1,b2,b3…,bm).编写一个函数,将数组中两个顺序表的位置互换,即将(b1,b2,b3…,bm).放在(a1,a2,a3,…,am)的前面。
算法思想:首先将数组A[m+n]中的全部元素(a?,az,a?,…,am,b?,bz,b?,…,b,)原地逆置为
(bm,b?,b?-2,…,b?,am,am-1,am-2…,ai),然后对前n个元素和后m个元素分别使用逆置算法,
即可得到(b?,b?,b?,…,b,,aj,a?,ay,…,am),从而实现顺序表的位置互换。
本题代码如下:
typedef int DataType;
void Reverse(DataType A[],int left,int right,int arraysize){
//逆转(aleft,aleft+1,aleft+2…,aright)为(aright,aright-1,…,aleft)
if(left>=rightllright>=arraySize)
return;
int mid=(left+right)/2;
for(int i=0;i<=mid-left;i++){
DataType temp=A[left+i];
A[left+i]=A[right-i];
A[right-i]=temp;
}
}
void Exchange(DataType A[],int m,int n,int arraysize){
/*数组 A[m+n]中,从0到m-1存放顺序表(a1,a2,a3,…,am),从m到m+n-1存放顺序表
(b1,b2,b3,…,bn),算法将这两个表的位置互换*/
Reverse(A,0,m+n-1,arraysize);
Reverse(A,0,n-1,arraysize);
Reverse(A,n,m+n-1,arraysize);
}
08.线性表(a1,a2,a3,…,an)中的元素递增有序且按顺序存储于计算机内。要求设计一个算法,完成用最少时间在表中查找数值为x的元素,若找到,则将其与后继元素位置相交换,若找不到,则将其插入表中并使表中元素仍递增有序。
算法思想:顺序存储的线性表递增有序,可以顺序查找,也可以折半查找。题目要求“用最
少的时间在表中查找数值为x的元素”,这里应使用折半查找法。
本题代码如下:
void SearchExchangeInsert(ElemType A[],ElemType x)(
int low=0,high=n-1,mid; //low 和 high指向顺序表下界和上界的下标
while(low<=high)(
mid=(1ow+high)/2; //找中间位置
if(A[mid]==x) break; //找到x,退出 while 循环
else if(A[mid]<x)low=mid+1; //到中点mid的右半部去查
else high=mid-1; //到中点mid的左半部去查
) //下面两个if 语句只会执行一个
if(A[mid]==x66mid!=n-1){ //若最后一个元素与x相等,则不存在与其后继交换的操作
t=A[mid];A[mid]=A[mid+1];A[mid+1]=t;
}
if(low>high)( //查找失败,插入数据元素x
for(i=n-1;i>high;i--)A[i+1]=A[i]; //后移元素
A[i+1]=x; //插入x
} //结束插入
}
09.给定三个序列A、B、C,长度均为n,且均为无重复元素的递增序列,请设计一个时间上尽可能高效的算法,逐行输出同时存在于这三个序列中的所有元素。例如,数组A为{1,2,3],数组B为{2,3,4],数组C为{-1,0,2},则输出2。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你的算法的时间复杂度和空间复杂度。
1)算法的基本设计思想。
使用三个下标变量从小到大遍历数组。
当三个下标变量指向的元素相等时,输出并向前推进指针,
否则仅移动小于最大元素的下标变量,
直到某个下标变量移出数组范围,即可停止。
2)算法的实现。
void samekey(int A[],int B[],int C[],int n)(
int i=0,j=0,k=0; Ⅱ定义三个工作指针
while(i<n66j<n&6k<n){ //相同则输出,并集体后移
if(A[i]==B[j]&6B[j]==C[k])(
printf("?n",A[i]);
i++;j++;k++;
}else{
int maxNum=max(A[i],max(B[j],c[k]));
if(A[i]<maxNum)i++;
if(B[j]<maxNum)j++;
if(C[k]<maxNum)k++;
}
}
}
3)每个指针移动的次数不超过n次,且每次循环至少有一个指针后移,
所以时间复杂度为O(n),算法只用到了常数个变量,空间复杂度为O(1)。
10.【2010统考真题】设将n(n>1)个整数存放到一维数组R中。设计一个在时间和空间两方面都尽可能高效的算法。将R中保存的序列循环左移p(0<p<n)个位置,即将R中的数据由(X0,X1,…,Xn-1)变换为(XpXp+1,…,Xn-i,X0,X1,…,Xp-1).要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:
可将问题视为把数组ab转换成数组ba
(a代表数组的前p个元素,b代表数组中余下的n-p个元素),
先将a逆置得到a-1b,再将b逆置得到a-1b-1,最后将整个a-1b-1逆置得到(a-1b-1)-1=ba。
设Reverse函数执行将数组逆置的操作,对 abcdefgh向左循环移动3(p=3)个位置的过程如下:
Reverse(0,p-1)得到 cbadefgh;
Reverse(p,n-1)得到cbahgfed;
Reverse(0,n-1)得到 defghabc。
注:在 Reverse 中,两个参数分别表示数组中待转换元素的始末位置。
2)使用C 语言描述算法如下:
void Reverse(int R[],int from,int to){
int i,temp;
for(i=0;i<(to-from+1)/2;i++)
{temp=R[from+i];R[from+i]=R[to-i];R[to-i]=temp;)
}
void Converse(int R[],int n,int p){
Reverse(R,0,p-1);
Reverse(R,p,n-1);
Reverse(R,0,n-1);
}
3)上述算法中三个 Reverse 函数的时间复杂度分别为O(p/2)、O((n-p)2)和O(n/2),故所
设计的算法的时间复杂度为O(n),空间复杂度为O(1)
11.【2011 统考真题】一个长度为 L(L≥1)的升序序列 S,处在第[L/2]个位置的数称为 S的中位数。例如,若序列S1=(11,13,15,17,19),则S1的中位数是15,两个序列的中位数是含它们所有元素的升序序列的中位数。例如,若S2=(2,4,6,8,20),则S1和S2的中位数是11.现在有两个等长升序序列A和 B,试设计一个在时间和空间两方面都尽可能高效的算法,找出两个序列A和B的中位数。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想如下。
分别求两个升序序列A、B的中位数,设为a和b,求序列A、B的中位数过程如下:
①若a=b,则a或b即为所求中位数,算法结束。
②若a<b,则舍弃序列A中较小的一半,同时舍弃序列B中较大的一半,要求两次舍弃的长度相等。
③若a>b,则舍弃序列A中较大的一半,同时舍弃序列B中较小的一半,要求两次舍弃的长度相等。
在保留的两个升序序列中,重复过程①、②、③,直到两个序列中均只含一个元素时为止,较小者即为所求的中位数。
2)本题代码如下:
#include<stdio.h>
-----------------------------------------------------------------------------------
// 找出两个等长升序序列的中位数
int M_Search(int A[], int B[], int n) {
int s1, d1, m1, s2, d2, m2;
s1 = 0;
d1 = n - 1;
s2 = 0;
d2 = n - 1;
while ((s1!= d1) || (s2!= d2)) { // 使用括号使逻辑更清晰
m1 = (s1 + d1) / 2;
m2 = (s2 + d2) / 2;
if (A[m1] == B[m2])
return A[m1]; // 满足条件①
if (A[m1] < B[m2]) { // 满足条件②
if ((s1 + d1) % 2 == 0) { // 若元素个数为奇数
s1 = m1; // 舍弃 A 中间点以前的部分,且保留中间点
d2 = m2; // 舍弃 B 中间点以后的部分,且保留中间点
}
else { // 元素个数为偶数
s1 = m1 + 1; // 舍弃 A 的前半部分
d2 = m2; // 舍弃 B 的后半部分
}
}
else { // 满足条件③
if ((s1 + d1) % 2 == 0) { // 若元素个数为奇数
d1 = m1; // 舍弃 A 中间点以后的部分,且保留中间点
s2 = m2; // 舍弃 B 中间点以前的部分,且保留中间点
}
else { // 元素个数为偶数
d1 = m1; // 舍弃 A 的后半部分
s2 = m2 + 1; // 舍弃 B 的前半部分
}
}
}
return A[s1] < B[s2]? A[s1] : B[s2];
}
--------------------------------------------------------------------------------
int main() {
int n;
printf("请输入序列长度:");
scanf("%d", &n);
int A[n], B[n];
printf("请输入序列 A 的元素:\n");
for (int i = 0; i < n; i++) {
scanf("%d", &A[i]);
}
printf("请输入序列 B 的元素:\n");
for (int i = 0; i < n; i++) {
scanf("%d", &B[i]);
}
int median = M_Search(A, B, n);
printf("两个序列的中位数为:%d\n", median);
return 0;
}
3)算法的时间复杂度为O(n),空间复杂度为O(1)
12.【2013统考真题】已知一个整数序列A=(ao,aj,…,a-1),其中O≤a<n(O≤i<n)。若存在ap1=ap2=…=apm=x且m>n/2(0≤p<n,1≤k≤m),则称x为A的主元素。例如A=(0,5,5,3,5,7,5,5),则5为主元素;又如A=(0,5,5,3,5,1,5,7),则A中没有主元素。假设A中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出A的主元素。若存在主元素,则输出该元素;否则输出-1。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或 C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:算法的策略是从前向后扫描数组元素,标记出一个可能成为主元素的元素Num。然后重新计数,确认Num是否是主元素。
算法可分为以下两步:
①选取候选的主元素。依次扫描所给数组中的每个整数,将第一个遇到的整数 Num 保存到c中,记录 Num的出现次数为1;若遇到的下一个整数仍等于Num,则计数加1,否则计数减1;当计数减到0时,将遇到的下一个整数保存到c中,计数重新记为1,开始新一轮计数,即从当前位置开始重复上述过程,直到扫描完全部数组元素。
②判断c中元素是否是真正的主元素。再次扫描该数组,统计c中元素出现的次数,若大于n/2,则为主元素;否则,序列中不存在主元素。
2)算法实现如下:
#include <iostream>
------------------------------------------------------------------------------
// 找出整数序列中的主元素
int Majority(int A[], int n) {
int i, c, count = 1;
c = A[0]; // c 用来保存候选主元素,count 用来计数
// 设置 A[0] 为候选主元素
for (i = 1; i < n; i++) { // 查找候选主元素
if (A[i] == c)
count++; // 对 A 中的候选主元素计数
else {
if (count > 0) // 处理不是候选主元素的情况
count--;
else { // 更换候选主元素,重新计数
c = A[i];
count = 1;
}
}
}
if (count > 0) {
for (i = count = 0; i < n; i++) {
if (A[i] == c)
count++; // 统计候选主元素的实际出现次数
}
if (count > n / 2)
return c; // 确认候选主元素
else
return -1; // 不存在主元素
}
}
--------------------------------------------------------------------------
int main() {
int A[] = {0, 5, 5, 3, 5, 7, 5, 5};
int n = sizeof(A) / sizeof(A[0]);
int result = Majority(A, n);
if (result!= -1)
std::cout << "主元素是: " << result << std::endl;
else
std::cout << "不存在主元素" << std::endl;
return 0;
}
3)实现的程序的时间复杂度为O(n),空间复杂度为O(1)。
13.【2018 统考真题】给定一个含n(n≥1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正整数是 1;数组{1,2,3;}中未出现的最小正整数是4。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:
要求在时间上尽可能高效,因此采用空间换时间的办法。分配一个用于标记的数组B[n],用
来记录 A中是否出现了1~n中的正整数,B[0]对应正整数1,B[n-1]对应正整数 n,初始化 B
中全部为0。由于A中含有n个整数,因此可能返回的值是1~n+1,当A中n个数恰好为1~n
时返回n+1。当数组A中出现了小于或等于0或大于n的值时,会导致1~n中出现空余位置,返
回结果必然在1~n中,因此对于A中出现了小于或等于0或大于n的值,可以不采取任何操作。
经过以上分析可以得出算法流程:从A[0]开始遍历A,若0<A[i]<=n,则令B[A[i]-1]=1;
否则不做操作。对A遍历结束后,开始遍历数组B,若能查找到第一个满足B[i]==0的下标i,
返回i+1即为结果,此时说明A中未出现的最小正整数在1和n之间。若B[i]全部不为0,返
回i+1(跳出循环时i=n,i+1等于n+1),此时说明A中未出现的最小正整数是n+1。
2)算法实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-------------------------------------------------------------------
// 找出数组中未出现的最小正整数
int findMissMin(int A[], int n) {
int i, *B; // 标记数组
B = (int *)malloc(sizeof(int) * n); // 分配空间
memset(B, 0, sizeof(int) * n); // 赋初值为 0
for (i = 0; i < n; i++) {
if (A[i] > 0 && A[i] <= n) // 若 A[i] 的值介于 1~n, 则标记数组 B
B[A[i] - 1] = 1;
}
for (i = 0; i < n; i++) { // 扫描数组 B, 找到目标值
if (B[i] == 0)
break;
}
free(B); // 释放动态分配的内存
return i + 1; // 返回结果
}
-------------------------------------------------------------------------------
int main() {
int A[] = {-5, 3, 2, 3};
int n = sizeof(A) / sizeof(A[0]);
int result = findMissMin(A, n);
printf("未出现的最小正整数: %d\n", result);
return 0;
}
3)时间复杂度:遍历A一次,遍历B一次,两次循环内操作步骤为O(1)量级,因此时间复
杂度为O(n)。空间复杂度:额外分配了B[n],空间复杂度为O(n)。
14.【2020统考真题】定义三元组(a,b,c)(a,b,c均为整数)的距离D=|a-b|+|b-c|+|c-a|。给定3个非空整数集合S1、S2和S3,按升序分别存储在3个数组中。请设计一个尽可能高效的算法,计算并输出所有可能的三元组(a,b,c)(a∈S1,b∈S2,c∈S3)中的最小距离。例如 S1={-1,0,9},S2={-25,-10,10,11},S3={2,9,17,30,41},则最小距离为2,相应的三元组为(9,10,9)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C语言或 C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想
① 使用 Dmin记录所有已处理的三元组的最小距离,初值为一个足够大的整数。
②集合S、S?和S?分别保存在数组A、B、C中。数组的下标变量i=j=k=0,当i<|S|、
j<|S?I且k<|S;时(IS|表示集合S中的元素个数),循环执行下面的a)~c)。
a)计算(A[],BU),C[k])的距离 D;(计算D)
b)若 D<Dmn,则Dmn=D;(更新 D)
c)将A[I]、BL]、C[K]中的最小值的下标+1; (对照分析:最小值为a,最大值为c,这里c
不变而更新 a,试图寻找更小的距离D)
③输出 Dmin,结束。
2)算法实现:
#include <stdio.h>
#include <limits.h>
-------------------------------------------------------------------
#define INT_MAX 0x7fffffff
// 计算绝对值
int abs_(int a) {
if (a < 0)
return -a;
else
return a;
}
// 判断 a 是否是三个数中的最小值
bool xls_min(int a, int b, int c) {
if (a <= b && a <= c)
return true;
return false;
}
// 找出三个集合组成的三元组的最小距离
int findMinofTrip(int A[], int n, int B[], int m, int C[], int p) {
// D_min 用于记录三元组的最小距离,初值赋为 INT_MAX
int i = 0, j = 0, k = 0, D_min = INT_MAX, D;
while (i < n && j < m && k < p && D_min > 0) {
D = abs_(A[i] - B[j]) + abs_(B[j] - C[k]) + abs_(C[k] - A[i]); // 计算 D
if (D < D_min)
D_min = D; // 更新 D
if (xls_min(A[i], B[j], C[k]))
i++; // 更新 a
else if (xls_min(B[j], C[k], A[i]))
j++;
else
k++;
}
return D_min;
}
--------------------------------------------------------------------------
int main() {
int A[] = {-1, 0, 9};
int B[] = {-25, -10, 10, 11};
int C[] = {2, 9, 17, 30, 41};
int n = sizeof(A) / sizeof(A[0]);
int m = sizeof(B) / sizeof(B[0]);
int p = sizeof(C) / sizeof(C[0]);
int minDistance = findMinofTrip(A, n, B, m, C, p);
printf("最小距离: %d\n", minDistance);
return 0;
}
3)设n=(|S1|+|S3|+|S3|),时间复杂度为O(n),空间复杂度为O(1)。
三、单项选择题
01.关于线性表的顺序存储结构和链式存储结构的描述中,正确的是( )。
I.线性表的顺序存储结构优于其链式存储结构
II.链式存储结构比顺序存储结构能更方便地表示各种逻辑结构
Ⅲ.若频繁使用插入和删除结点操作,则顺序存储结构更优于链式存储结构
IV.顺序存储结构和链式存储结构都可以进行顺序存取
A.I、Ⅱ、Ⅲ B. II、IV C.I、I D.Ⅲ、IV
两种存储结构适用于不同的场合,不能简单地说谁好谁坏,1错误。链式存储用指针表示逻
辑结构,而指针的设置是任意的,因此比顺序存储结构能更方便地表示各种逻辑结构,Ⅱ正确。
在顺序存储中,插入和删除结点需要移动大量元素,效率较低,Ⅲ的描述刚好相反。顺序存储结
构既能随机存取又能顺序存取,而链式结构只能顺序存取,IV 正确。
02.对于一个线性表,既要求能进行较快速地插入和删除,又要求存储结构能反映数据之间的逻辑关系,则应该用( )。
A.顺序存储方式
B.链式存储方式
C.散列存储方式
D.以上均可以
首先直接排除A和 D。散列存储通过散列函数映射到物理空间,不能反映数据之间的逻辑关
系,排除C。链式存储能方便地表示各种逻辑关系,且插入和删除操作的时间复杂度为O(1)。
03.链式存储设计时,结点内的存储单元地址( )。
A.一定连续
B.一定不连续
C.不一定连续
D.部分连续,部分不连续
链式存储设计时,各个不同结点的存储空间可以不连续,但结点内的存储单元地址必须连续。
04.下列关于线性表说法中,正确的是( ).
I.顺序存储方式只能用于存储线性结构
Ⅱ.在一个设有头指针和尾指针的单链表中,删除表尾元素的时间复杂度与表长无关
Ⅲ.带头结点的单循环链表中不存在空指针
IV.在一个长度为n的有序单链表中插入一个新结点并仍保持有序的时间复杂度为O(n)
V.若用单链表来表示队列,则应该选用带尾指针的循环链表
A.I、Ⅱ B. I、H、IV、V C.IV、V D.Ⅲ、IV、V
顺序存储方式同样适用于存储图和树,I错误。删除表尾结点时,必须从头开始找到表尾结点的前驱,其时间与表长有关,Ⅱ错误。循环单链表中最后一个结点的指针不是 NULL,而是指向头结点,整个链表形成一个环,因此不存在空指针,Ⅲ正确。有序单链表只能依次查找插入位置,时间复杂度为O(n),IV正确。队列需要在表头删除元素,表尾插入元素,采用带尾指针的循环链表较为方便,插入和删除的时间复杂度都为O(1),V正确
05.设线性表中有2n个元素,( )在单链表上实现要比在顺序表上实现效率更高。
A.删除所有值为x的元素
B.在最后一个元素的后面插入一个新元素
C.顺序输出前k个元素
D.交换第i个元素和第2n-i-1个元素的值(i=0,…,n-1)
对于A,在单链表和顺序表上实现的时间复杂度都为O(n),但后者要移动很多元素,因此在单链表上实现效率更高。对于B和 D,顺序表的效率更高。C无区别。
06.在一个单链表中,已知q所指结点是P所指结点的前驱结点,若在q和p之间插入结点S,则执行()
A.s->next=p->next;p->next=s;
B. p->next=s->next;s->next=p;
C.q->next=s;s->next=p;
D. p->next=s;s->next=q;
s插入后,q成为s的前驱,而p成为s的后继,选C
07.给定有n个元素的一维数组,建立一个有序单链表的最低时间复杂度是( )。
A. O(1)
B. O(n)
C. O(n2)
D.O(nlog2n)
建立链表,然后依次插入建立有序表,则每插入一个元素就需遍历链表寻找插入位置,
即直接插入排序,时间复杂度为O(n2)。若先将数组排好序,然后建立链表,建立链表的时间复
杂度为O(n),数组排序的最好时间复杂度为O(nlogzn),总时间复杂度为O(nlogzn)。故选D。
08.将长度为n的单链表链接在长度为m的单链表后面,其算法的时间复杂度采用大O形式表示应该是( )。
A. O(1)
B. O(n)
C.O(m)
D. O(n+m)
09.单链表中,增加一个头结点的目的是( )。
A.使单链表至少有一个结点
B.标识表结点中首结点的位置
C.方便运算的实现
D.说明单链表是线性表的链式存储
单链表设置头结点的目的是方便运算的实现,主要好处体现在:
第一,有头结点后,插入和删除数据元素的算法就统一了,不再需要判断是否在第一个元素之前插入或删除第一个元素;
第二,不论链表是否为空,其头指针是指向头结点的非空指针,链表的头指针不变,因此空表和非
空表的处理也就统一了。
10.在一个长度为n的带头结点的单链表h上,设有尾指针r,则执行( )操作与链表的表长有关。
A.删除单链表中的第一个元素
B.删除单链表中的最后一个元素
C.在单链表第一个元素前插入一个新元素
D.在单链表最后一个元素后插入一个新元素
删除单链表的最后一个结点需置其前驱结点的指针域为 NULL,需要从头开始依次遍历找到该前驱结点,需要O(n)的时间,与表长有关。其他操作均与表长无关,读者可自行模拟
11.对于一个头指针为 head的带头结点的单链表,判定该表为空表的条件是(B );对于不带头结点的单链表,判定空表的条件为( A)。
A. head==NULL
B. head->next==NULL
C. head->next==head
D. head!=NULL
在带头结点的单链表中,头指针 head指向头结点,头结点的 next域指向第一个元素结点,
head->next==NULL 表示该单链表为空。在不带头结点的单链表中,head 直接指向第一个元
素结点,head==NULL表示该单链表为空。
12.在线性表 ao,aj,“,aioo中,删除元素aso需要移动( )个元素。
A. 0
B.50
C. 51
D.0或50
线性表有顺序存储和链式存储两种存储结构。若采用链式存储结构,则删除元素ag不需要
移动元素;若采用顺序存储结构,则需要依次移动50个元素。
13.通过含有n(n>1)个元素的数组a,采用头插法建立单链表L,则L中的元素次序( ).
A.与数组a的元素次序相同
B.与数组a的元素次序相反
C.与数组a的元素次序无关
D.以上都错误
当采用头插法建立单链表时,数组后面的元素插入到单链表L的最前端,所以L中的元素次
序与数组a的元素次序相反。
14.下面关于线性表的一些说法中,正确的是( )。
A. 对一个设有头指针和尾指针的单链表执行删除最后一个元素的操作与链表长度无关
B.线性表中每个元素都有一个直接前驱和一个直接后继
C.为了方便插入和删除数据,可以使用双链表存放数据
D.取线性表第i个元素的时间与i的大小有关
A显然错误。B表中第一个元素和最后一个元素不满足题设要求。双链表能很方便地访问前
驱和后继,故删除和插入数据较为方便,C正确。D未考虑顺序存储的情况
15.在双链表中向p所指的结点之前插入一个结点q的操作为( )。
A. p->prior=q;q->next=p;p->prior->next=q; q->prior=p->prior;
B. q->prior=p->prior;p->prior->next=q; q->next=p; p->prior=q->next;
C. q->next=p;p->next=q;q->prior->next=q; q->next=p;
D.p->prior->next=q;q->next=p;q->prior=p->prior; p->prior=q;
为了在p之前插入结点q,可以将p的前一个结点的next域指向q,将q的 next 域指向p,将q的prior域指向p的前一个结点,将p的prior域指向q。仅D满足条件。
16.在双向链表存储结构中,删除p所指的结点时必须修改指针( )。
A. p->prior->next=p->next;p->next->prior=p->prior;
B.p->prior=p->prior->prior;p->prior->next=p;
C. p->next->prior=p;p->next=p->next->next;
D. p->next=p->prior->prior; p->prior=p->next->next;
17.在如下图所示的双链表中,已知指针p指向结点A,若要在结点A和C之间插入指针q
所指的结点 B,则依次执行的语句序列可以是( )。
①q->next=p->next;
②q->prior=p;
③ p->next=q;
④ p->next->prior=q;
A.①②④③ B.④③②① C.③④①② D.①③④②
18.在双链表的两个结点之间插入一个新结点,需要修改( )个指针域。
A. 1
B. 3
C. 4
D. 2
当在双链表的两个结点(分别用第一个、第二个结点表示)之间插入一个新结点时,需要修改四个指针域,分别是:
新结点的前驱指针域,指向第一个结点;
新结点的后继指针域,指向第二个结点;
第一个结点的后继指针域,指向新结点;
第二个结点的前驱指针域,指向新结点。
19.在长度为n的有序单链表中插入一个新结点,并仍然保持有序的时间复杂度是( )。
A. O(1)
B. O(n)
C. O(n2)
D. O(nlog2n)
设单链表递增有序,首先要在单链表中找到第一个大于x的结点的直接前驱p,在p之后插
入该结点。查找的时间复杂度为O(n),插入的时间复杂度为O(1),总时间复杂度为O(n)。
20.与单链表相比,双链表的优点之一是( )。
A.插入、删除操作更方便
B.可以进行随机访问
C.可以省略表头指针或表尾指针
D.访问前后相邻结点更灵活
在插入和删除操作上,单链表和双链表都不用移动元素,都很方便,但双链表修改指针的操
作更为复杂,A错误。双链表中可以快速访问任何一个结点的前驱和后继结点,D正确。
21.对于一个带头结点的循环单链表 L,判断该表为空表的条件是( )。
A.头结点的指针域为空
B.L的值为NULL
C.头结点的指针域与L的值相等
D.头结点的指针域与L的地址相等
带头结点的循环单链表L为空表时,满足L->next=L,即头结点的指针域与L的值相等,
而不是头结点的指针域与L的地址相等。注意,带头结点的单循环链表中不存在空指针
22.对于一个带头结点的双循环链表 L,判断该表为空表的条件是( )。
A.L->prior==L&&L->next==NULL
B.L->prior==NULL&&L->next==NULL
C.L->prior==NULL&&L->next==L
D.L->prior==L&&L->next==L
循环双链表L判空的条件是头结点(头指针)的prior和next域都指向它自身
23.一个链表最常用的操作是在末尾插入结点和删除结点,则选用( )最节省时间。
A.带头结点的双循环链表
B.单循环链表
C.带尾指针的单循环链表
D.单链表
在链表的末尾插入和删除一个结点时,需要修改其相邻结点的指针域。而寻找尾结点及尾结
点的前驱结点时,只有带头结点的双循环链表所需要的时间最少。
24.设对n(n>1)个元素的线性表的运算只有4种:删除第一个元素;删除最后一个元素;在第一个元素之前插入新元素;在最后一个元素之后插入新元素,则最好使用( )。
A.只有尾结点指针没有头结点指针的循环单链表
B. 只有尾结点指针没有头结点指针的非循环双链表
C.只有头结点指针没有尾结点指针的循环双链表
D.既有头结点指针又有尾结点指针的循环单链表
对于A,删除尾结点*p时,需要找到*p的前一个结点,时间复杂度为O(n)。对于 B,删除
首结点*p时,需要找到*p结点,这里没有直接给出头结点指针,而通过尾结点的prior 指针找
到*p 结点的时间复杂度为O(n)。对于 D,删除尾结点*p时,需要找到*p 的前一个结点,时间
复杂度为 O(n)。对于C,执行这四种算法的时间复杂度均为O(1)。
25.设有两个长度为n的循环单链表,若要求两个循环单链表的头尾相接的时间复杂度为O(1),则对应两个循环单链表各设置一个指针,分别指向( )。
A.各自的头结点
B.各自的尾结点
C.各自的首结点
D.一个表的头结点,另一个表的尾结点
要求用O(1)的时间将两个循环单链表头尾相接,并未指明哪个链表接在另一个链表之后,所
以对两个链表都要在O(1)的时间找到头结点和尾结点。因此,两个指针应都指向尾结点。
26.设有一个长度为n的循环单链表,若从表中删除首元结点的时间复杂度达到O(n),则此时采用的循环单链表的结构可能是( )。
A.只有表头指针,没有头结点
B.只有表尾指针,没有头结点
C.只有表尾指针,带头结点
D.只有表头指针,带头结点
在循环单链表中,删除首元结点后,要保持链表的循环性,因此需要找到首元结点的前驱。当链表带头结点时,其前驱就是头结点,因此不论是表头指针还是表尾指针,删除首元结点的时间都为O(1)。当链表不带头结点时,其前驱是尾结点,因此,若有表尾指针,就可在O(1)的时间找到尾结点;若只有表头指针,则需要遍历整个链表找到尾结点,时间为O(n)。
27.某线性表用带头结点的循环单链表存储,头指针为head,当head->next->next==head成立时,线性表的长度可能是( )。
A. 0
B. 1
C.2
D.可能为0或1
对一个空循环单链表,有 head->next==head,推理 head->next->next==head->
next==head。对含有一个元素的循环单链表,头结点(头指针 head 指示)的 next 域指向这个
唯一的元素结点,该元素结点的 next域指向头结点,因此也有 head->next->next=head。
28.有两个长度都为n的双链表,若以h?为头指针的双链表是非循环的,以h?为头指针的
双链表是循环的,则下列叙述中正确的是( )。
A.对于双链表 h,删除首结点的时间复杂度是O(n)
B.对于双链表h?,删除首结点的时间复杂度是O(n)
C.对于双链表h,删除尾结点的时间复杂度是O(1)
D.对于双链表 h?,删除尾结点的时间复杂度是O(1)
对于两种双链表,删除首结点的时间复杂度都是O(1)。对于非循环双链表,删除尾结点的时
间复杂度是O(n);对于循环双链表,删除尾结点的时间复杂度是O(1)。
29.一个链表最常用的操作是在最后一个元素后插入一个元素和删除第一个元素,则选用( )
最节省时间。
A.不带头结点的单循环链表
B.双链表
C. 单链表
D.不带头结点且有尾指针的单循环链表
对于 A,在最后一个元素之后插入元素的情况与普通单链表相同,时间复杂度为O(n);而删除第一个元素时,为保持单循环链表的性质(尾结点指向第一个结点),要先遍历整个链表找到尾结点,再做删除操作,时间复杂度为O(n)。对于B,双链表的情况与单链表的相同,一个是O(n),一个是O(1)。对于C,在最后一个元素之后插入一个元素,要遍历整个链表才能找到插入位置,时间复杂度为O(n);删除第一个元素的时间复杂度为O(1)。对于 D,与A的分析对比,有尾结点的指针,省去了遍历链表的过程,因此时间复杂度均为O(1)。
30.需要分配较大空间,插入和删除不需要移动元素的线性表,其存储结构为( )。
A.单链表
B.静态链表
C.顺序表
D.双链表
静态链表采用数组表示,因此需要预先分配较大的连续空间,静态链表同时还具有一般链表
的特点,即插入和删除不需要移动元素。
31.下列关于静态链表的说法中,正确的是( )。
I.静态链表兼具顺序表和单链表的优点,因此存取表中第i个元素的时间与i无关
Ⅱ.静态链表能客纳的最大元素个数在表定义时就确定了,以后不能增加
Ⅲ.静态链表与动态链表在元素的插入、删除上类似,不需要移动元素
VI.相比动态链表,静态链表可能浪费较多的存储空间
A.I、Ⅱ、Ⅲ B.I、Ⅲ、VI C.I、Ⅲ、VI D.1、Ⅱ、VI
32.【2016统考真题】已知一个带有表头结点的双向循环链表L,结点结构为
其中 prev和next分别是指向其直接前驱和直接后继结点的指针。现要删除指针p所
指的结点,正确的语句序列是( ).
A. p->next->prev=p->prev; p->prev->next=p->prev; free(p);
B. p->next->prev=p->next; p->prev->next=p->next; free(p);
C. p->next->prev=p->next; p->prev->next=p->prev; free(p);
D.p->next->prev=p->prev; p->prev->next=p->next; free(p);
A的第二句代码,相当于将p前驱结点的后继指针指向其自身,错误;B和C的第一句代码,
相当于将p后继结点的前驱指针指向其自身,错误。只有D正确
33.【2016统考真题】已知表头元素为c的单链表在内存中的存储状态如下表所示。
现将f存放于1014H处并插入单链表,若f在逻辑上位于a和e之间,则a、e、f的“链接地址”依次是( )。
A. 1010H、1014H、1004H
B. 1010H、1004H、1014H
C. 1014H、1010H、1004H
D. 1014H、1004H、1010H
其中“链接地址”是指结点 next所指的内存地址。当结点f插入后,a指向f,f指向e,e指
向b。显然a、e和f的“链接地址”分别是f、b和e的内存地址,即1014H、1004H和 1010H。
34.【2021 统考真题】已知头指针 h 指向一个带头结点的非空单循环链表,结点结构为其中 next是指向直接后继结点的指针,p是尾指针,q是临时指针。现要删除该链表的第一个元素,正确的语句序列是( )。
A. h->next=h->next->next;q=h->next;free(q);
B.q=h->next;h->next=h->next->next;free(q);
C.q=h->next;h->next=q->next;if(p!=q)p=h;free(q);
D. q=h->next;h->next=q->next;if(p==q)p=h;free(q);
如图1所示,要删除带头结点的非空单循环链表中的第一个元素,就要先用临时指针q指向待
删结点,q=h->next;然后将q从链表中断开,h->next=q->next(这一步也可写成h->next=
h->next->next);此时要考虑一种特殊情况,若待删结点是链表的尾结点,即循环单链表中只
有一个元素(p和q指向同一个结点),如图2所示,则在删除后要将尾指针指向头结点,即if(p==q)
p=h;最后释放 q结点即可,答案选D
35.【2023 统考真题】现有非空双向链表 I,其结点结构为 是指向直接前驱结点的指针,next是指向直接后继结点的指针。若要在L中指针p所指向的结点(非尾结点)之后插入指针s指向的新结点,则在执行语句序列“s->next=p->next;p->next=s;”后,下列语句序列中还需要执行的是( )。
A.s->next->prev=p; s->prev=p;
B.p->next->prev=s; s->prev=p;
C.s->prev=s->next->prev; s->next->prev=s;
D. p->next->prev=s->prev; s->next->prev=p;
四、综合应用题
01.在带头结点的单链表工中,删除所有值为x的结点,并释放其空间,假设值为x的结点
不唯一,试编写算法以实现上述操作。
解法1:用p从头至尾扫描单链表,pre指向*p结点的前驱。若p所指结点的值为x,则删
除,并让p移向下一个结点,否则让pre、p 指针同步后移一个结点。
本题代码如下:
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
----------------------------------------------------------
// 删除链表中值为 x 的节点
void Del_X_1(LinkList *L, ElemType x) {
LNode *p = L->head->next, *pre = L->head, *q; // 置 p 和 pre 的初始值
while (p!= NULL) {
if (p->data == x) {
q = p; // q 指向被删结点
p = p->next;
pre->next = p; // 将 *q 结点从链表中断开
free(q); // 释放 *q 结点的空间
}
else { // 否则,pre 和 p 同步后移
pre = p;
p = p->next;
}
} // while
}
-------------------------------------------------------------------------
本算法是在无序单链表中删除满足某种条件的所有结点,这里的条件是结点的值为x。实际
上,这个条件是可以任意指定的,只要修改if条件即可。比如,我们要求删除值介于mink 和
maxk之间的所有结点,则只需将if语句修改为if(p->data>mink66p->data<maxk)。
解法2:采用尾插法建立单链表。用p指针扫描L的所有结点,当其值不为x时,将其链接
到L之后,否则将其释放。
本题代码如下:
------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 删除链表中值为 x 的节点函数
void Del_X_2(LinkList *L, ElemType x) {
LNode *p = L->head->next, *r = L->head, *q; // r 指向尾结点,其初值为头结点
while (p!= NULL) {
if (p->data!= x) { // p 结点值不为 x 时将其链接到 L 尾部
r->next = p;
r = p;
p = p->next; // 继续扫描
} // p 结点值为 x 时将其释放
else {
q = p;
p = p->next; //继续扫描
free(q); //释放空间
}
}
r->next = NULL; // 插入结束后置尾结点指针为 NULL
}
上述两个算法扫描一遍链表,时间复杂度为O(n),空间复杂度为O(1)。
02.试编写在带头结点的单链表工中删除一个最小值结点的高效算法(假设该结点唯一)。
算法思想:用p从头至尾扫描单链表,pre指向*p结点的前驱,用minp 保存值最小的结
点指针(初值为p),minpre指向*minp结点的前驱(初值为pre)。一边扫描,一边比较,若
p->data小于minp->data,则将p、pre分别赋值给 minp、minpre,如下图所示。当p扫
描完毕时,minp指向最小值结点,minpre 指向最小值结点的前驱结点,再将minp所指结点删
除即可。
------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 删除链表中的最小值节点
LinkList Delete_Min(LinkList *L) {
LNode *pre = L->head, *p = pre->next; // p 为工作指针,pre 指向其前驱
LNode *minpre = pre, *minp = p; // 保存最小值节点及其前驱
while (p!= NULL) {
if (p->data < minp->data) { // 找到比之前找到的最小值节点更小的节点
minp = p;
minpre = pre;
}
pre = p; // 继续扫描下一个节点
p = p->next;
}
minpre->next = minp->next; // 删除最小值节点
free(minp);
return *L;
}
-----------------------------------------------------------------------
算法需要从头至尾扫描链表,时间复杂度为O(n),空间复杂度为O(1)
03.试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间复杂度为O(1).
解法1:将头结点摘下,然后从第一结点开始,依次插入到头结点的后面(头插法建立单链
表),直到最后一个结点为止,这样就实现了链表的逆置,如下图所示。
-----------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
---------------------------------------------------------------------
// 链表逆置函数
LinkList Reverse_1(LinkList L) {
LNode *p, *r; // 定义工作指针 p 和后继指针 r
p = L.head->next; // p 指向第一个元素结点
L.head->next = NULL; // 先将头结点 L 的 next 域置为 NULL
while (p!= NULL) { // 当 p 不为 NULL 时,进行处理//依次将元素结点摘下
r = p->next; // 暂存 p 的后继
p->next = L.head->next; // 将 p 结点插入到头结点之后
L.head->next = p;
p = r; // p 指向下一个待处理的节点
}
return L; // 返回逆置后的链表
}
解法 2:大部分辅导书都只介绍解法 1,这对读者的理解和思维是不利的。为了将调整指针
这个复杂的过程分析清楚,我们借助图形来进行直观的分析。
假设 pre、p和r指向三个相邻的结点,如下图所示。假设经过若干操作后,*pre 之前的
结点的指针都已调整完毕,它们的 next 都指向其原前驱结点。现在令*p结点的 next 域指向
*pre结点,注意到一旦调整指针的指向,*p的后继结点的链就会断开,为此需要用r来指向原
*p的后继结点。处理时需要注意两点:一是在处理第一个结点时,应将其next 域置为NULL,
而不是指向头结点(因为它将作为新表的尾结点);二是在处理完最后一个结点后,需要将头结
点的指针指向它
---------------------------------------------------------------------------
Linklist Reverse_2(LinkList L) {
LNode *pre, *p = L->next, *r = p->next;
p->next = NULL; // 处理第一个结点
while (r!= NULL) { // r 为空,则说明 p 为最后一个结点
pre = p; // 依次继续遍历
p = r;
r = r->next;
p->next = pre; // 指针反转
}
L->next = p; // 处理最后一个结点
return L;
}
上述两个算法的时间复杂度为O(n),空间复杂度为O(1)。
04.设在一个带表头结点的单链表中,所有结点的元素值无序,试编写一个函数,删除表中
所有介于给定的两个值(作为函数参数给出)之间的元素(若存在)。
因为链表是无序的,所以只能逐个结点进行检查,执行删除
-------------------------------------------------
void RangeDelete(LinkList &L, int min, int max) {
LNode *pr = L, *p = L->link; // p 是检测指针,pr 是其前驱
while (p!= NULL) {
if (p->data > min && p->data < max) { // 寻找到被删结点,删除
pr->link = p->link;
free(p);
p = pr->link;
}
else { // 否则继续寻找被删结点
pr = p;
p = p->link;
}
}
}
05.给定两个单链表,试分析找出两个链表的公共结点的思想(不用写代码)。
两个单链表有公共结点,即两个链表从某一结点开始,它们的 next 都指向同一结点。
由于每个单链表结点只有一个next 域,因此从第一个公共结点开始,之后的所有结点都是重合的,
不可能再出现分叉。所以两个有公共结点而部分重合的单链表,拓扑形状看起来像Y,而不可能像X。
本题极容易联想到“蛮”方法:在第一个链表上顺序遍历每个结点,每遍历一个结点,
在第二个链表上顺序遍历所有结点,若找到两个相同的结点,则找到了它们的公共结点。
显然,该算法的时间复杂度为O(lenl×len2)。
接下来我们试着去寻找一个线性时间复杂度的算法。先把问题简化:
如何判断两个单向链表有没有公共结点?应注意到这样一个事实:
若两个链表有一个公共结点,则该公共结点之后的所有结点都是重合的,
即它们的最后一个结点必然是重合的。
因此,我们判断两个链表是不是有重合的部分时,只需要分别遍历两个链表到最后一个结点。
若两个尾结点是一样的,则说明它们有公共结点,否则两个链表没有公共结点。
然而,在上面的思路中,顺序遍历两个链表到尾结点时,
并不能保证在两个链表上同时到达尾结点。这是因为两个链表长度不一定一样。
但假设一个链表比另一个长k个结点,我们先在长的链表上遍历k个结点,之后再同步遍历,
此时我们就能保证同时到达最后一个结点。由于两个链表从第一个公共结点开始到链表的尾结点,
这一部分是重合的,因此它们肯定也是同时到达第一公共结点的。
于是在遍历中,第一个相同的结点就是第一个公共的结点。
根据这一思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。
在长的链表上先遍历长度之差个结点之后,再同步遍历两个链表,直到找到相同的结点,
或者一直到链表结束。此时,该方法的时间复杂度为O(len1+len2)。
06.设C={aj,b?,a?,b?,…,amb,}为线性表,采用带头结点的单链表存放,设计一个就地算
法,将其拆分为两个线性表,使得A={ay,a?,…,a。},B={b,,…,b?,b?}.
算法思想:循环遍历链表C,采用尾插法将一个结点插入表A,这个结点为奇数号结点,这
样建立的表A与原来的结点顺序相同;采用头插法将下一结点插入表 B,这个结点为偶数号结点,
这样建立的表B与原来的结点顺序正好相反。
本题代码如下:
-----------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 将链表 A 拆分为 A 和 B,B 由 A 中节点交替头插法得到
LinkList DisCreat_2(LinkList A) {
LinkList B = (LinkList)malloc(sizeof(LNode)); // 创建 B 表表头
B->next = NULL; // B 表的初始化
LNode *p = A->head->next, *q; // p 为工作指针
LNode *ra = A->head; // ra 始终指向 A 的尾结点
while (p!= NULL) {
ra->next = p;
ra = p; // 将 *p 链到 A 的表尾
p = p->next;
if (p!= NULL) {
q = p->next; // 头插后,*p 将断链,因此用 q 记忆 *p 的后继
p->next = B->head->next; // 将 *p 插入到 B 的前端
B->head->next = p;
p = q;
}
}
ra->next = NULL; // A 尾结点的 next 域置空
return B;
}
----------------------------------------------------------------------
该算法特别需要注意的是,采用头插法插入结点后,*p 的指针域已改变,若不设变量保存
其后继结点,则会引起断链,从而导致算法出错。
07.在一个递增有序的单链表中,存在重复的元素。设计算法删除重复的元素,例如(7,10,
10,21,30,42,42,42,51,70)将变为(7,10,21,30,42,51,70)。
算法思想:由于是有序表,因此所有相同值域的结点都是相邻的。用p扫描递增单链表 L,
若*p结点的值域等于其后继结点的值域,则删除后者,否则p移向下一个结点。
本题代码如下:
--------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 删除链表中的重复节点
void Del_Same(LinkList *L) {
LNode *p = L->head->next, *q; // p 为扫描工作指针
if (p == NULL) {
return;
}
while (p->next!= NULL) {
q = p->next; // q 指向 *p 的后继结点
if (p->data == q->data) { // 找到重复值的结点
p->next = q->next; // 释放 *q 结点
free(q); // 释放相同元素值的结点
} else {
p = p->next;
}
}
}
---------------------------------------------------------------------
本算法的时间复杂度为O(n),空间复杂度为O(1)。
本题也可采用尾插法,将头结点摘下,然后从第一结点开始,依次与已经插入结点的链表的
最后一个结点比较,若不等则直接插入,否则将当前遍历的结点删除并处理下一个结点,直到最
后一个结点为止。
08.设A和B是两个单链表(带头结点),其中元素递增有序。设计一个算法从A和B中的
公共元素产生单链表 C,要求不破坏A、B的结点。
算法思想:表A、B都有序,可从第一个元素起依次比较A、B两表的元素,若元素值不等,
则值小的指针往后移,若元素值相等,则创建一个值等于两结点的元素值的新结点,使用尾插法
插入到新的链表中,并将两个原表指针后移一位,直到其中一个链表遍历到表尾。
本题代码如下:
--------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 获取两个链表的公共元素并生成新链表 C
void Get_Common(LinkList A, LinkList B) {
LNode *p = A->head->next, *q = B->head->next, *r, *s;
LinkList C = (LinkList)malloc(sizeof(LNode)); // 建立表 C
r = C; // r 始终指向 C 的尾结点
while (p!= NULL && q!= NULL) { // 循环跳出条件
if (p->data < q->data) {
p = p->next; // 若 A 的当前元素较小,后移指针
} else if (p->data > q->data) {
q = q->next; // 若 B 的当前元素较小,后移指针
} else { // 找到公共元素结点
s = (LNode*)malloc(sizeof(LNode));
s->data = p->data; // 复制产生结点 *s
r->next = s; // 将 *s 链接到 C 上(尾插法)
r = s;
p = p->next; // 表 A 和 B 继续向后扫描
q = q->next;
}
}
r->next = NULL; // 置 C 尾结点指针为空
}
--------------------------------------------------------------------------
09.已知两个链表A和B分别表示两个集合,其元素递增排列。编制函数,求A与B的交
集,并存放于A链表中。
算法思想:采用归并的思想,设置两个工作指针pa和pb,对两个链表进行归并扫描,只有
同时出现在两集合中的元素才链接到结果表中且仅保留一个,其他的结点全部释放。当一个链表
遍历完毕后,释放另一个表中剩下的全部结点。
本题代码如下:
--------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 求两个链表的并集
LinkList Union(LinkList *la, LinkList *lb) {
LNode *pa = la->head->next; // 设工作指针分别为 pa 和 pb
LNode *pb = lb->head->next;
LNode *u, *pc = la->head; // 结果表中当前合并结点的前驱指针 pc
while (pa && pb) {
if (pa->data == pb->data) { // 交集并入结果表中
pc->next = pa; // A 中结点链接到结果表
pc = pa;
pa = pa->next;
u = pb; // B 中结点释放
pb = pb->next;
free(u);
} else if (pa->data < pb->data) { // 若 A 中当前结点值小于 B 中当前结点值
u = pa;
pa = pa->next; // 后移指针
free(u); // 释放 A 中当前结点
} else { // 若 B 中当前结点值小于 A 中当前结点值
u = pb;
pb = pb->next; // 后移指针
free(u); // 释放 B 中当前结点
}
}
while (pa) { // B 已遍历完,A 未完
u = pa;
pa = pa->next;
free(u); // 释放 A 中剩余结点
}
while (pb) { // A 已遍历完,B 未完
u = pb;
pb = pb->next;
free(u); // 释放 B 中剩余结点
}
pc->next = NULL; // 置结果链表尾指针为 NULL
free(lb->head); // 释放 B 表的头结点
return *la;
}
--------------------------------------------------------------------------
链表归并类型的试题在各学校历年真题中出现的频率很高,故应扎实掌握解决此类问题的思
想。该算法的时间复杂度为O(len1+len2),空间复杂度为O(1)。
10.两个整数序列A=aj,a?,a,…,am和B=b?,b?,b?,…,b?已经存入两个单链表中,设计一
个算法,判断序列B是否是序列A的连续子序列。
算法思想:因为两个整数序列已存入两个链表中,操作从两个链表的第一个结点开始,若对
应数据相等,则后移指针;若对应数据不等,则A链表从上次开始比较结点的后继开始,B链表
仍从第一个结点开始比较,直到B链表到尾表示匹配成功。A链表到尾而B链表未到尾表示失败。
操作中应记住 A链表每次的开始结点,以便下次匹配时好从其后继开始。
本题代码如下:
------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 判断 B 链表是否为 A 链表的子序列
int Pattern(LinkList A, LinkList B) {
LNode *p = A; // p 为 A 链表的工作指针,本题假定 A 和 B 均无头结点
LNode *pre = p; // pre 记住每趟比较中 A 链表的开始结点
LNode *q = B; // q 是 B 链表的工作指针
while (p && q) {
if (p->data == q->data) { // 结点值相同
p = p->next;
q = q->next;
} else {
pre = pre->next;
p = pre; // A 链表新的开始比较结点
q = B; // q 从 B 链表第一个结点开始
}
}
if (q == NULL) // B 已经比较结束
return 1; // 说明 B 是 A 的子序列
else
return 0; // B 不是 A 的子序列
}
11.设计一个算法用于判断带头结点的循环双链表是否对称。
算法思想:让 p从左向右扫描,q从右向左扫描,直到它们指向同一结点(p==q,当循环
双链表中结点个数为奇数时)或相邻(p->next=q或q->prior=p,当循环双链表中结点个数
为偶数时)为止,若它们所指结点值相同,则继续进行下去,否则返回 0。若比较全部相等,则
返回1。
本题代码如下:
----------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义双向链表节点结构体
typedef struct DNode {
int data;
struct DNode *prior;
struct DNode *next;
} DNode;
// 定义双向链表结构体
typedef struct {
DNode *head;
DNode *tail;
} DLinkList;
// 判断双向链表是否对称
int Symmetry(DLinkList L) {
DNode *p = L.head->next; // 前向工作指针
DNode *q = L.tail->prior; // 后向工作指针
while (p!= q && q->next!= p) { // 循环跳出条件
if (p->data == q->data) { // 所指结点值相同则继续比较
p = p->next;
q = q->prior;
} else {
return 0; // 否则,返回 0
}
}
return 1; // 比较结束后返回 1
}
12.有两个循环单链表,链表头指针分别为h1和h2,编写一个函数将链表 h2链接到链表
h1之后,要求链接后的链表仍保持循环链表形式。
算法思想:先找到两个链表的尾指针,将第一个链表的尾指针与第二个链表的头结点链接起
来,再使之成为循环的。
本题代码如下:
----------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 将循环链表 h2 链接到循环链表 h1 之后,使之仍保持循环链表的形式
LinkList Link(LinkList h1, LinkList h2) {
LNode *p, *q; // 分别指向两个链表的尾结点
p = h1.head;
while (p->next!= h1.head) { // 寻找 h1 的尾结点
p = p->next;
}
q = h2.head;
while (q->next!= h2.head) { // 寻找 h2 的尾结点
q = q->next;
}
p->next = h2.head; // 将 h2 链接到 h1 之后
q->next = h1.head; // 令 h2 的尾结点指向 h1
return h1;
}
13.设有一个带头结点的非循环双链表 L,其每个结点中除有pre、data和 next 域外,
还有一个访问频度域freq,其值均初始化为零。每当在链表中进行一次 Locate(L,x)
运算时,令值为x的结点中freq域的值增1,并使此链表中的结点保持按访问频度递
减的顺序排列,且最近访问的结点排在频度相同的结点之前,以便使频繁访问的结点总
是靠近表头。试编写符合上述要求的 Locate(L,x)函数,返回找到结点的地址,类型为指针型。
算法思想:首先在双向链表中查找数据值为x的结点,查到后,将结点从链表上摘下,然后
顺着结点的前驱链查找该结点的插入位置(频度递减,且排在同频度的第一个,即向前找到第一
个比它的频度大的结点,插入位置为该结点之后),并插入到该位置。
本题代码如下:
-----------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义双向链表节点结构体
typedef struct DNode {
ElemType data;
int freq;
struct DNode *prior;
struct DNode *next;
} DNode;
// 定义双向链表结构体
typedef struct {
DNode *head;
DNode *tail;
} DLinkList;
// 在双向链表中查找值为 x 的节点,并进行相应处理
DLinkList Locate(DLinkList L, ElemType x) {
DNode *p = L.head->next, *q; // p 为工作指针,q 为 p 的前驱,用于查找插入位置
while (p && p->data!= x) { // 查找值为 x 的结点
p = p->next;
}
if (!p) { // 不存在值为 x 的结点
exit(0);
} else {
p->freq++; // 令元素值为 x 的结点的 freq 域加 1
if (p->prior == L.head || p->prior->freq > p->freq) {
return p; // p 是链表首结点,或 freq 值小于前驱
}
if (p->next!= NULL) {
p->next->prior = p->prior;
}
p->prior->next = p->next; // 将 p 结点从链表上摘下
q = p->prior; // 以下查找 p 结点的插入位置
while (q!= L.head && q->freq <= p->freq) {
q = q->prior;
}
p->next = q->next;
if (q->next!= NULL) {
q->next->prior = p; // 将 p 结点排在同频率的第一个
}
p->prior = q;
q->next = p;
}
return p; // 返回值为 x 的结点的指针
}
14.设将n(n>1)个整数存放到不带头结点的单链表L中,设计算法将L中保存的序列循环
右移k(0<k<n)个位置。例如,若k=1,则将链表{0,1,2,3}变为{3,0,1,2}。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:
首先,遍历链表计算表长 n,并找到链表的尾结点,将其与首结点相连,得到一个循环单链
表。然后,找到新链表的尾结点,它为原链表的第n-k个结点,令L指向新链表尾结点的下一
个结点,并将环断开,得到新链表。
2)本题代码如下:
----------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 实现链表的指定位置反转
LNode *Converse(LNode *L, int k) {
int n = 1; // n 用来保存链表的长度
LNode *p = L; // p 为工作指针
while (p->next!= NULL) { // 计算链表的长度
p = p->next;
n++;
} // 循环执行完后,p 指向链表尾结点
p->next = L; // 将链表连成一个环
for (int i = 1; i <= n - k; i++) { // 寻找链表的第 n - k 个结点
p = p->next;
}
L = p->next; // 令 L 指向新链表尾结点的下一个结点
p->next = NULL; // 将环断开
return L;
}
----------------------------------------------------------------------
3)本算法的时间复杂度为O(n),空间复杂度为O(1)。
15.单链表有环,是指单链表的最后一个结点的指针指向了链表中的某个结点(通常单链表
的最后一个结点的指针域是空的)。试编写算法判断单链表是否存在环。
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度和空间复杂度。
设置快慢两个指针分别为 fast 和 slow 最初都指向链表头 head。slow 每次走一步,即
slow=slow->next;fast每次走两步,即 fast=fast->next->next。fast 比slow 走得
快,若有环,则 fast一定先进入环,而slow 后进入环。两个指针都进入环后,经过若干操作
后两个指针定能在环上相遇。这样就可以判断一个链表是否有环。
如下图所示,当slow 刚进入环时,fast 早已进入环。因为 fast 每次比 slow 多走一步
且 fast与slow的距离小于环的长度,所以fast与slow 相遇时,slow 所走的距离不超过环的
长度
如下图所示,设头结点到环的入口点的距离为 a,环的入口点沿着环的方向到相遇点的距离
为x,环长为r,相遇时 fast 绕过了n圈。
则有2(a+x)=a+n*r+x,即a=nr-x。显然从头结点到环的入口点的距离等于n倍的环长减去
环的入口点到相遇点的距离。因此可设置两个指针,一个指向 head,一个指向相遇点,两个指
针同步移动(均为一次走一步),相遇点即为环的入口点。
2)本题代码如下:
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 查找链表中环的入口节点
LNode* FindLoopStart(LNode *head) {
LNode *fast = head, *slow = head; // 设置快慢两个指针
while (fast!= NULL && fast->next!= NULL) {
slow = slow->next; // 慢指针每次走一步
fast = fast->next->next; // 快指针每次走两步
if (slow == fast) break; // 相遇
}
if (fast == NULL || fast->next == NULL)
return NULL; // 没有环,返回 NULL
LNode *p1 = head, *p2 = slow; // 分别指向开始点、相遇点
while (p1!= p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1; // 返回入口点
}
3)当 fast与slow相遇时,slow肯定没有遍历完链表,故算法的时间复杂度为O(n),空间复杂度为O(1)
16.设有一个长度n(n为偶数)的不带头结点的单链表,且结点值都大于0,设计算法求这
个单链表的最大孪生和。孪生和定义为一个结点值与其孪生结点值之和,对于第i个结
点(从0开始),其孪生结点为第n-i-1个结点。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你的算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:
设置快、慢两个指针分别为 fast和slow,初始时slow指向L(第一个结点),fast 指向
L->next(第二个结点),之后slow每次走一步,fast每次走两步。当 fast指向表尾(第n个
结点)时,slow正好指向链表的中间点(第n/2个结点),即slow正好指向链表前半部分的最后
一个结点。将链表的后半部分逆置,然后设置两个指针分别指向链表前半部分和后半部分的首结点,
在遍历过程中计算两个指针所指结点的元素之和,并维护最大值。
2)本题代码如下:
---------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct LNode {
int data;
struct LNode *next;
} LNode;
// 定义链表结构体
typedef struct {
LNode *head;
} LinkList;
// 计算链表中两部分节点值两两相加的最大值
int PairSum(LinkList L) {
LNode *fast = L.head->next, *slow = L.head; // 利用快慢双指针找到链表的中间点
while (fast!= NULL && fast->next!= NULL) {
fast = fast->next->next; // 快指针每次走两步
slow = slow->next; // 慢指针每次走一步
}
LNode *newHead = NULL, *p = slow->next, *tmp;
while (p!= NULL) { // 反转链表后一半部分的元素,采用头插法
// p 指向当前待插入结点,令 tmp 指向其下一结点
tmp = p->next;
p->next = newHead; //将p所指结点插入到新链表的首结点之前
newHead = p; //newHead 指向刚才新插入的结点,作为新的首结点
p = tmp; // 当前待处理结点变为下一结点
}
int mx = 0;
p = L.head;
LNode *q = newHead;
while (p!= NULL) { // 用 p 和 q 分别遍历两个链表
if ((p->data + q->data) > mx) // 用 mx 记录最大值
mx = p->data + q->data;
p = p->next;
q = q->next;
}
return mx;
}
---------------------------------------------------------------------------
3)本算法的时间复杂度为O(n),空间复杂度为O(1)。
17.【2009 统考真题】已知一个带有表头结点的单链表,结点结构为
假设该链表只给出了头指针 list。在不改变链表的前提下,请设计一个尽可能高效的
算法,查找链表中倒数第k个位置上的结点(k为正整数)。若查找成功,算法输出该结
点的 data域的值,并返回1;否则,只返回 0。要求:
1)描述算法的基本设计思想。
2)描述算法的详细实现步骤。
3)根据设计思想和实现步骤,采用程序设计语言描述算法(使用 C、C++或 Java语言
实现),关键之处请给出简要注释。
1)算法的基本设计思想如下:
问题的关键是设计一个尽可能高效的算法,通过链表的一次遍历,找到倒数第k个结点的位
置。算法的基本设计思想是:定义两个指针变量p和q,初始时均指向头结点的下一个结点(链
表的第一个结点),p指针沿链表移动;当p指针移动到第k个结点时,q指针开始与p指针同
步移动;当p指针移动到最后一个结点时,q指针所指示结点为倒数第k个结点。以上过程对链
表仅进行一遍扫描。
2)算法的详细实现步骤如下:
① count=0,p和q指向链表表头结点的下一个结点。
② 若p为空,转⑤。
③若 count 等于k,则q指向下一个结点:否则,count=count+1。
④ p指向下一个结点,转②。
⑤若 count等于k,则查找成功,输出该结点的data域的值,返回1;否则,说明 k值
超过了线性表的长度,查找失败,返回0。
⑥算法结束。
3)算法实现如下:
---------------------------------------------------------------------
#include <stdio.h>
typedef int ElemType; // 链表数据的类型定义
typedef struct LNode { // 链表结点的结构定义
ElemType data; // 结点数据
struct LNode *link; // 结点链接指针
} LNode, *LinkList;
// 在链表中查找第 k 个元素
int Search_k(LinkList list, int k) {
LNode *p = list->link, *q = list->link; // 指针 p、q 指示第一个结点
int count = 0;
while (p!= NULL) { // 遍历链表直到最后一个结点
if (count < k)
count++; // 计数,若 count < k 只移动 p
else
q = q->link;
p = p->link; // 之后让 p、q 同步移动
} // while
if (count < k)
return 0; // 查找失败返回 0
else { // 否则打印并返回 1
printf("%d", q->data);
return 1;
}
}
---------------------------------------------------------------------
评分说明:若所给出的算法采用一遍扫描方式就能得到正确结果,可给满分 15分;若采用两
遍或多遍扫描才能得到正确结果,最高分为10分。若采用递归算法得到正确结果,最高给10分;
若实现算法的空间复杂度过高(使用了大小与k有关的辅助数组),但结果正确,最高给10分。
18.【2012 统考真题】假定采用带头结点的单链表保存单词,当两个单词有相同的后级时,
可共享相同的后缀存储空间,例如,loading和being的存储映像如下图所示。
设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为
请设计一个时间上尽可能高效的算法,找出由str1和str2所指向两个链表共同后级
的起始位置(如图中字符i所在结点的位置p)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++或Java语言描述算法,关键之处给出注释。
3)说明你所设计算法的时间复杂度。
1)算法的基本设计思想如下:
①分别求出str1和 str2 所指的两个链表的长度m和n。
②将两个链表以表尾对齐:令指针p、q分别指向str1和 str2的头结点,若m≥n,则
指针p先走,使p指向链表中的第m-n+1个结点;若m<n,则使q指向链表中的第
n-m+1个结点,即使指针p和q所指的结点到表尾的长度相等。
③反复将指针p和q同步向后移动,,并判断它们是否指向同一结点。当p、q指向同一结
点,则该点即为所求的共同后缀的起始位置。
2)本题代码如下:
------------------------------------------------------------
#include <stdio.h>
// 定义链表节点结构体
typedef struct Node {
char data;
struct Node *next;
} SNode;
/* 求链表长度的函数 */
int listlen(SNode *head) {
int len = 0;
while (head->next!= NULL) { // 遍历链表计算长度
len++;
head = head->next;
}
return len;
}
/* 找出共同后缀的起始地址 */
SNode* find_list(SNode *str1, SNode *str2) {
int m, n;
SNode *p, *q;
m = listlen(str1); // 求 str1 的长度,O(m)
n = listlen(str2); // 求 str2 的长度,O(n)
for (p = str1; m > n; m--) { //若m > n,使p指向链表中的第m - n + 1 个结点
p = p->next;
}
for (q = str2; m < n; n--) { //若m < n,使q指向链表中的第 n - m + 1 个结点
q = q->next;
}
while (p->next!= NULL && p->next!= q->next) { // 查找共同后缀起始点
p = p->next; // 两个指针同步向后移动
q = q->next;
}
return p->next; // 返回共同后缀的起始地址
}
------------------------------------------------------------
3)时间复杂度为O(lenl+len2)或O(max(len?,len2)),其中len1、len2分别为两个链表的长度。
19.【2015统考真题】用单链表保存m个整数,结点的结构为[data][link],且|data|≤
n(n为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中 data的
绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如,若
给定的单链表 head如下:
则删除结点后的 head 为
要求:
1)给出算法的基本设计思想。
2)使用 C或C++语言,给出单链表结点的数据类型定义。
3)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
4)说明你所设计算法的时间复杂度和空间复杂度。
1)算法的基本设计思想:
·算法的核心思想是用空间换时间。使用辅助数组记录链表中已出现的数值,从而只需对链
表进行一趟扫描。
·因为|data|≤n,故辅助数组q的大小为n+1,各元素的初值均为0。依次扫描链表中
的各结点,同时检查q[ldata|]的值,若为0则保留该结点,并令q[|datal]=1;否
则将该结点从链表中删除。
2)使用C语言描述的单链表结点的数据类型定义:
-----------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node *link;
} NODE;
typedef NODE *PNODE;
// 去除链表中重复值的函数
void func(PNODE h, int n) {
PNODE p = h, r;
int *q, m;
q = (int *)malloc(sizeof(int) * (n + 1)); // 申请 n + 1 个位置的辅助空间
for (int i = 0; i < n + 1; i++) // 数组元素初值置 0
*(q + i) = 0;
while (p->link!= NULL) {
m = p->link->data > 0? p->link->data : -p->link->data;
if (*(q + m) == 0) { // 判断该结点的 data 是否已出现过
*(q + m) = 1; // 首次出现
p = p->link;
} else { // 重复出现
r = p->link; // 删除
p->link = r->link;
free(r);
}
}
free(q);
}
4)参考答案所给算法的时间复杂度为O(m),空间复杂度为O(n)
20.【2019统考真题】设线性表L=(a,a?,a?,…,a,?,a,a)采用带头结点的单链表保存,链
表中的结点定义如下:
typedef struct node
( int data;
struct node*next;
)NODE;
请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得
到线性表L'=(a1,a2,a3,...an-2,an-1,an)。要求:
1)给出算法的基本设计思想。
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
3)说明你所设计的算法的时间复杂度
1)算法的基本设计思想
先观察L=(a,a?,a?…,a?,a,a?)和L'=(a?a?,a?,a??,a?,a,?,…),发现L'是由L摘取第
一个元素,再摘取倒数第一个元素……依次合并而成的。为了方便链表后半段取元素,需要先将
L后半段原地逆置[题目要求空间复杂度为 O(1),不能借助栈],否则每取最后一个结点都需要遍
历一次链表。①先找出链表L的中间结点,为此设置两个指针p和q,指针p每次走一步,指针
g每次走两步,当指针q到达链尾时,指针p正好在链表的中间结点;②然后将L的后半段结点
原地逆置。③从单链表前后两段中依次各取一个结点,按要求重排。
2)算法实现
------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node *link;
} NODE;
// 对链表进行特定操作的函数
void change_list(NODE *h) {
NODE *p, *q, *r, *s;
p = q = h;
while (q->next!= NULL) { // 寻找中间结点
p = p->next; // p 走一步
q = q->next;
if (q->next!= NULL)
q = q->next; // q 走两步
}
q = p->next; // p 所指结点为中间结点,q 为后半段链表的首结点
p->next = NULL;
while (q!= NULL) { // 将链表后半段逆置
r = q->next;
q->next = p->next;
p->next = q;
q = r;
}
s = h->next; // s 指向前半段的第一个数据结点,即插入点
q = p->next; // q 指向后半段的第一个数据结点
p->next = NULL;
while (q!= NULL) { // 将链表后半段的结点插入到指定位置
r = q->next;
q->next = s->next; // 将 q 所指结点插入到 s 所指结点之后
s->next = q;
s = q->next; // s 指向前半段的下一个插入点
q = r;
}
}
------------------------------------------------------------------------------
3)第一步找中间结点的时间复杂度为O(n),第二步逆置的时间复杂度为O(n),第三步合并
链表的时间复杂度为O(n),所以该算法的时间复杂度为O(n)
第三章 栈、队列和数组
1.数据结构(C语言版 第2版严蔚敏)课后习题
1.选择题
(1)若让元素 1, 2, 3, 4, 5 依次进栈,则出栈次序不可能出现在()种情况。
A.5, 4, 3, 2, 1
B.2, 1, 5, 4, 3
C.4, 3, 1, 2, 5
D.2, 3, 5, 4, 1
答案: C
解释:栈是后进先出的线性表,不难发现 C 选项中元素 1 比元素 2 先出栈,违背了栈的后进先出原则,所以不可能出现 C 选项所示的情况。
(2)若已知一个栈的入栈序列是 1,2,3… n,其输出序列为 p1,p2,p3 … pn, 若 p1=n ,则 pi 为()。
A.i
B .n-i
C .n-i+1
D.不确定
答案: C
解释:栈是后进先出的线性表,一个栈的入栈序列是 1, 2, 3,, , n,而输出序列的第一个元素为 n,说明 1,2,3,, , n 一次性全部进栈, 再进行输出, 所以 p1=n ,p2=n-1 ,pi=n-i+1 。
(3)数组Q[n]用来表示一个循环队列,f为当前队列头元素的前一位置,r为队尾元素的位置,假定队列中元素的个数小于n,计算队列中元素个数的公式为() 。
A .r-f
B .(n+f-r)%n
C .n+r-f
D.(n+r-f)%n
答案: D
解释:对于非循环队列,尾指针和头指针的差值便是队列的长度,而对于循环队列,差值可能为负数, 所以需要将差值加上 MAXSIZE (本题为n),然后与 MAXSIZE (本题为n)求余,即(n+r-f)%n 。
(4)链式栈结点为: (data,link) , top 指向栈顶 .若想摘除栈顶结点,并将删除结点的值保存到 x 中 ,则应执行操作() 。
A.x=top->data;top=top->link ;
B.top=top->link;x=top->link ;
C.x=top;top=top->link ;
D.x=top->link ;
答案: A
解释:x=top->data 将结点的值保存到 x 中,top=top->link 栈顶指针指向栈顶下一结点,
即摘除栈顶结点。
(5)设有一个递归算法如下
int fact(int n) { //n 大于等于 0
if(n<=0) return 1;
else return n*fact(n-1);
}
则计算 fact(n) 需要调用该函数的次数为()。
A.n+1
B.n-1
C.n
D.n+2
答案: A
解释:特殊值法。设 n=0,易知仅调用一次 fact(n) 函数,故选 A 。
(6)栈在 ()中有所应用。
A.递归调用
B.函数调用
C.表达式求值
D.前三个选项都有
答案: D
解释:递归调用、函数调用、表达式求值均用到了栈的后进先出性质。
(7)为解决计算机主机与打印机间速度不匹配问题,通常设一个打印数据缓冲区。主机将要输出的数据依次写入该缓冲区,而打印机则依次从该缓冲区中取出数据。该缓冲区的逻辑结构应该是() 。
A.队列
B.栈
C.线性表
D.有序表
答案: A
解释:解决缓冲区问题应利用一种先进先出的线性表,而队列正是一种先进先出的线性表。
(8)设栈 S 和队列 Q 的初始状态为空,元素 e1、 e2、 e3、 e4、 e5 和 e6 依次进入栈 S,
个元素出栈后即进入 Q,若 6 个元素出队的序列是 e2、 e4、e3、e6、e5 和 e1,则栈 S 的容
量至少应该是()。
A.2 B.3 C.4 D.6
答案: B
解释:元素出队的序列是 e2、e4、e3、e6、e5 和 e1 ,可知元素入队的序列是 e2、e4、e3、 e6、 e5 和 e1,即元素出栈的序列也是 e2、 e4、 e3、 e6、 e5 和 e1,而元素 e1、 e2、 e3、e4、 e5 和 e6 依次进入栈,易知栈 S 中最多同时存在 3 个元素,故栈 S 的容量至少为 3。
(9)若一个栈以向量 V[1…n] 存储,初始栈顶指针 top 设为 n+1 ,则元素 x 进栈的正确操作是 () 。
A.top++; V[top]=x;
B.V[top]=x; top++;
C.top–; V[top]=x;
D.V[top]=x; top–;
答案: C
解释:初始栈顶指针 top 为 n+1 ,说明元素从数组向量的高端地址进栈,又因为元素
存储在向量空间 V[1…n] 中,所以进栈时 top 指针先下移变为 n,之后将元素 x 存储在 V[n] 。
(10)设计一个判别表达式中左,右括号是否配对出现的算法,采用()数据结构最佳。
A.线性表的顺序存储结构
B.队列
C. 线性表的链式存储结构
D. 栈
答案: D
解释:利用栈的后进先出原则。
(11)用链接方式存储的队列,在进行删除运算时()。
A. 仅修改头指针
B. 仅修改尾指针
C. 头、尾指针都要修改
D. 头、尾指针可能都要修改
答案: D
解释:一般情况下只修改头指针,但是,当删除的是队列中最后一个元素时,队尾指针也丢失了,因此需对队尾指针重新赋值。
(12)循环队列存储在数组 A[0…m] 中,则入队时的操作为()。
A. rear=rear+1
B. rear=(rear+1)%(m-1)
C. rear=(rear+1)%m
D. rear=(rear+1)%(m+1)
答案: D
解释:数组 A[0…m] 中共含有 m+1 个元素,故在求模运算时应除以 m+1。
(13)最大容量为 n 的循环队列, 队尾指针是 rear ,队头是 front ,则队空的条件是 ()。
A. (rear+1)%n==front
B. rear==front
C.rear+1==front
D. (rear-l)%n==front
答案: B
解 释 :最大容量为n的循环队列 , 队满条件是(rear+1)%n==front, 队空条件是rear==front 。
(14)栈和队列的共同点是()。
A. 都是先进先出
B. 都是先进后出
C. 只允许在端点处插入和删除元素
D. 没有共同点
答案: C
解释:栈只允许在栈顶处进行插入和删除元素,队列只允许在队尾插入元素和在队头
删除元素。
(15)一个递归算法必须包括()。
A. 递归部分
B. 终止条件和递归部分
C. 迭代部分
D. 终止条件和迭代部分
答案: B
2.算法设计题(1)将编号为 0 和 1 的两个栈存放于一个数组空间 V[m] 中,栈底分别处于数组的两端。当第 0 号栈的栈顶指针 top[0] 等于 -1 时该栈为空,当第 1 号栈的栈顶指针 top[1] 等于 m 时该栈为空。两个栈均从两端向中间增长。试编写双栈初始化,判断栈空、栈满、进栈和出栈等
算法的函数。双栈数据结构的定义如下
Typedef struct
{
int top[2],bot[2]; //栈顶和栈底指针
SElemType *V; //栈数组
int m; //栈最大可容纳元素个数
}DblStack
[ 题目分析 ]
两栈共享向量空间,将两栈栈底设在向量两端,初始时,左栈顶指针为 -1 ,右栈顶为 m。
两栈顶指针相邻时为栈满。两栈顶相向、迎面增长,栈顶指针指向栈顶元素。
[ 算法描述 ]
(1) 栈初始化
int Init() {
S.top[0]=-1;
S.top[1]=m;
return 1; // 初始化成功
}
(2) 入栈操作:
int push(stk S ,int i,int x)
∥ i 为栈号, i=0 表示左栈, i=1 为右栈, x 是入栈元素。入栈成功返回 1,失败返回 0
{
if(i<0||i>1){
cout<< “栈号输入不对 ”<<endl;exit(0);}
if(S.top[1]-S.top[0]==1) {
cout<< “栈已满 ”<<endl;return(0);}
switch(i) {
case 0: S.V[++S.top[0]]=x; return(1); break;
case 1: S.V[--S.top[1]]=x; return(1);
}
} ∥ push
(3) 退栈操作
ElemType pop(stk S,int i)
∥退栈。 i 代表栈号, i=0 时为左栈, i=1 时为右栈。退栈成功时返回退栈元素
∥否则返回 -1
{
if(i<0 || i>1){cout<< “栈号输入错误 ”<<endl ; exit(0);}
switch(i) {
case 0:
if(S.top[0]==-1) {
cout<< “栈空 ”<<endl ; return ( -1); }
else return(S.V[S.top[0]--]);
case 1:
if(S.top[1]==m {
cout<< “栈空 ”<<endl; return(-1);}
else return(S.V[S.top[1]++]);
} ∥ switch
} ∥算法结束
(4) 判断栈空
int Empty();
{
return (S.top[0]==-1 && S.top[1]==m);
}
[算法讨论 ]
请注意算法中两栈入栈和退栈时的栈顶指针的计算。左栈是通常意义下的栈,而右栈入
栈操作时,其栈顶指针左移(减 1),退栈时,栈顶指针右移(加 1)。 (2)回文是指正读反读均相同的字符序列, 如“ abba”和“ abdba ”均是回文, 但“ good ”
不是回文。试写一个算法判定给定的字符向量是否为回文。 (提示:将一半字符入栈 )
[ 题目分析 ]
将字符串前一半入栈,然后,栈中元素和字符串后一半进行比较。即将第一个出栈元素
和后一半串中第一个字符比较,若相等,则再出栈一个元素与后一个字符比较, , ,直至
栈空, 结论为字符序列是回文。 在出栈元素与串中字符比较不等时, 结论字符序列不是回文。
[ 算法描述 ]
#define StackSize 100 // 假定预分配的栈空间最多为 100 个元素
typedef char DataType;// 假定栈元素的数据类型为字符
typedef struct {
DataType data[StackSize];
int top;
}SeqStack;
int IsHuiwen( char *t) { // 判断 t 字符向量是否为回文,若是,返回 1,否则返回 0
SeqStack s;
int i , len;
char temp;
InitStack( &s);
len=strlen(t); // 求向量长度
for ( i=0; i<len/2; i++)// 将一半字符入栈
Push( &s, t[i]);
while( !EmptyStack( &s)){ // 每弹出一个字符与相应字符比较
temp=Pop (&s);
if( temp!=S[i]) return 0 ;// 不等则返回 0
else i++;
}
return 1 ; // 比较完毕均相等则返回 1
}
(3)设从键盘输入一整数的序列: a1, a2, a3, …, an,试编写算法实现:用栈结构存储
输入的整数,当 ai≠ -1 时,将 ai 进栈;当 ai=-1 时,输出栈顶整数并出栈。算法应对异常情
况(入栈满等)给出相应的信息。
[ 算法描述 ]
#define maxsize 栈空间容量
void InOutS(int s[maxsize])
//s 是元素为整数的栈,本算法进行入栈和退栈操作。
{
int top=0; //top 为栈顶指针,定义 top=0 时为栈空。
for(i=1; i<=n; i++) //n 个整数序列作处理。
18
{cin>>x); // 从键盘读入整数序列。
if(x!=-1) // 读入的整数不等于 -1 时入栈。
{
if(top==maxsize-1)
{cout<< “栈满” <<endl;exit(0);}
else s[++top]=x; //x 入栈。
}
else // 读入的整数等于 -1 时退栈。
{
if(top==0)
{cout<< “栈空” <<endl;exit(0);}
else cout<< “出栈元素是” <<s[top--]<<endl;}
}
}// 算法结束。
(4)从键盘上输入一个后缀表达式,试编写算法计算表达式的值。规定:逆波兰表达式的长度不超过一行,以 符 作 为 输 入 结 束 , 操 作 数 之 间 用 空 格 分 隔 , 操 作 符 只 可 能 有 + 、 − 、 ∗ 、 / 四 种 运 算 。 例 如 : 23434 + 2 ∗ 符作为输入结束,操作数之间用空格分隔 , 操作符只可能有 +、 - 、 * 、 / 四种运算。例如: 234 34+2*符作为输入结束,操作数之间用空格分隔,操作符只可能有+、−、∗、/四种运算。例如:23434+2∗ 。
[ 题目分析 ]
逆波兰表达式 (即后缀表达式 ) 求值规则如下:设运算数栈 OPND,对表达式从左到右扫描 (读入 ) ,当表达式中扫描到数时, 压入 OPND栈。 当扫描到运算符时, 从 OPND退出两个数,进行相应运算,结果再压入 OPND 栈。这个过程一直进行到读出表达式结束符 $,这时 OPND栈中只有一个数,就是结果。
[ 算法描述 ]
float expr()
// 从键盘输入逆波兰表达式,以‘ $’表示输入结束,本算法求逆波兰式表达式的值。
{
float OPND[30]; // OPND 是操作数栈。
init(OPND); // 两栈初始化。
float num=0.0; // 数字初始化。
cin>>x;//x 是字符型变量。
while(x!= ’$’)
{
switch
{
case ‘0’<=x<=’9’:
while((x>= ’0’&&x<=’9’)||x== ’. ’) // 拼数
if(x!= ’. ’) // 处理整数
{
num=num*10+ (ord(x)- ord(‘0’) ) ; cin>>x;
}
else // 处理小数部分。
{
scale=10.0; cin>>x;
while(x>= ’0’&&x<=’9’)
{num=num+(ord(x)- ord(‘0’)/scale;
scale=scale*10; cin>>x; }
}//else
push(OPND,num); num=0.0;// 数压入栈,下个数初始化
case x= ‘ ’:break; // 遇空格,继续读下一个字符。
case x= ‘+’:push(OPND,pop(OPND)+pop(OPND));break;
case x= ‘ - ’ :x1=pop(OPND);x2=pop(OPND);push(OPND,x2-x1);break;
case x= ‘*’:push(OPND,pop(OPND)*pop(OPND));break;
case x= ‘ / ’ :x1=pop(OPND);x2=pop(OPND);push(OPND,x2/x1);break;
default: // 其它符号不作处理。
}// 结束 switch
cin>>x;// 读入表达式中下一个字符。
}// 结束 while (x! =‘$’)
cout<< “后缀表达式的值为” <<pop(OPND);
}// 算法结束。
[ 算法讨论 ] 假设输入的后缀表达式是正确的,未作错误检查。算法中拼数部分是核心。若遇到大于等于‘ 0’且小于等于‘ 9’的字符,认为是数。这种字符的序号减去字符‘ 0’的序号得出数。对于整数,每读入一个数字字符,前面得到的部分数要乘上 10 再加新读入的数得到新的部分数。当读到小数点,认为数的整数部分已完,要接着处理小数部分。小数部分的数要除以 10 (或 10 的幂数)变成十分位,百分位,千分位数等等,与前面部分数相加。在拼数过程中,若遇非数字字符,表示数已拼完,将数压入栈中,并且将变量 num 恢复为 0,准备下一个数。这时对新读入的字符进入‘ +’、‘ - ’、‘ * ’、‘ / ’及空格的判断,因此在结束处理数字字符的 case 后,不能加入 break 语句。
(5)假设以 I 和 O 分别表示入栈和出栈操作。栈的初态和终态均为空,入栈和出栈的操
作序列可表示为仅由 I 和 O 组成的序列, 称可以操作的序列为合法序列, 否则称为非法序列。
①下面所示的序列中哪些是合法的?
A. IOIIOIOO B. IOOIOIIO C. IIIOIOIO D. IIIOOIOO
②通过对①的分析, 写出一个算法, 判定所给的操作序列是否合法。 若合法, 返回 true ,
否则返回 false (假定被判定的操作序列已存入一维数组中) 。
答案:
① A 和 D 是合法序列, B 和 C 是非法序列。
②设被判定的操作序列已存入一维数组 A 中。
int Judge(char A[])
// 判断字符数组 A 中的输入输出序列是否是合法序列。如是,返回 true ,否则返回
false 。
{
i=0; //i 为下标。
j=k=0; //j 和 k 分别为 I 和字母 O 的的个数。
while(A[i]!= ‘ 0’) // 当未到字符数组尾就作。
{
switch(A[i])
{
case ‘I ’: j++; break; // 入栈次数增 1。
case ‘O’: k++; if(k>j){
cout<< “序列非法” <<ednl ; exit(0);}
}
i++; // 不论 A[i] 是‘ I ’或‘ O’,指针 i 均后移。
}
if(j!=k) {cout<< “序列非法” <<endl ; return(false);}
else {cout<< “序列合法” <<endl ; return(true);}
}// 算法结束。
[ 算法讨论 ] 在入栈出栈序列(即由‘ I ’和‘ O’组成的字符串)的任一位置,入栈次数
(‘ I ’的个数)都必须大于等于出栈次数(即‘ O’的个数) ,否则视作非法序列,立即给出
信息,退出算法。整个序列(即读到字符数组中字符串的结束标记‘ \0 ’),入栈次数必须等
于出栈次数(题目中要求栈的初态和终态都为空) ,否则视为非法序列。
(6)假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾元素站点 (注意不
设头指针 ) ,试编写相应的置空队、判队空、入队和出队等算法。
[ 题目分析 ]
置空队就是建立一个头节点,并把头尾指针都指向头节点,头节点是不存放数据的;判
队空就是当头指针等于尾指针时,队空;入队时,将新的节点插入到链队列的尾部,同时将
尾指针指向这个节点;出队时,删除的是队头节点,要注意队列的长度大于 1 还是等于 1 的
情况,这个时候要注意尾指针的修改,如果等于 1,则要删除尾指针指向的节点。
[算法描述 ]
//先定义链队结构 :
typedef struct queuenode
{
Datatype data;
struct queuenode *next;
}QueueNode; // 以上是结点类型的定义
typedef struct
{
queuenode *rear;
}LinkQueue; // 只设一个指向队尾元素的指针
(1) 置空队
void InitQueue(LinkQueue *Q)
{ // 置空队:就是使头结点成为队尾元素
QueueNode *s;
Q->rear = Q->rear->next;// 将队尾指针指向头结点
while (Q->rear!=Q->rear->next)// 当队列非空,将队中元素逐个出队
{
s=Q->rear->next;
Q->rear->next=s->next;
}
delete s;
}// 回收结点空间
}
(2) 判队空
int EmptyQueue(LinkQueue *Q)
{ // 判队空。当头结点的 next 指针指向自己时为空队
return Q->rear->next->next==Q->rear->next;
}
(3) 入队
void EnQueue(LinkQueue *Q, Datatype x)
{ // 入队。也就是在尾结点处插入元素
QueueNode *p=new QueueNode;// 申请新结点
p->data=x; p->next=Q->rear->next;// 初始化新结点并链入
Q-rear->next=p;
Q->rear=p;// 将尾指针移至新结点
}
(4) 出队
Datatype DeQueue(LinkQueue *Q)
{// 出队 ,把头结点之后的元素摘下
Datatype t;
QueueNode *p;
if(EmptyQueue(Q ))
Error("Queue underflow");
p=Q->rear->next->next; //p 指向将要摘下的结点
x=p->data; // 保存结点中数据
if (p==Q->rear)
{// 当队列中只有一个结点时, p 结点出队后,要将队尾指针指向头结点
Q->rear = Q->rear->next;
Q->rear->next=p->next;
}
else
Q->rear->next->next=p->next;// 摘下结点 p
delete p;// 释放被删结点
return x;
}
(7)假设以数组 Q[ m] 存放循环队列中的元素 , 同时设置一个标志 tag ,以 tag== 0 和 tag == 1 来区别在队头指针 (front )和队尾指针 (rear )相等时,队列状态为 “空 ”还是 “满 ”。试编写与此结构相应的插入 (enqueue )和删除 (dlqueue )算法。
[算法描述 ]
(1) 初始化
SeQueue QueueInit(SeQueue Q)
{// 初始化队列
Q.front=Q.rear=0; Q.tag=0;
return Q;
}
(2) 入队
SeQueue QueueIn(SeQueue Q,int e)
{// 入队列
if((Q.tag==1) && (Q.rear==Q.front))
cout<<" 队列已满 "<<endl;
else
{
Q.rear=(Q.rear+1) % m;
Q.data[Q.rear]=e;
if(Q.tag==0) Q.tag=1; // 队列已不空
}
return Q;
}
(3) 出队
ElemType QueueOut(SeQueue Q)
{// 出队列
if(Q.tag==0) {
cout<<" 队列为空 "<<endl; exit(0);}
else
{Q.front=(Q.front+1) % m;
e=Q.data[Q.front];
if(Q.front==Q.rear) Q.tag=0; // 空队列
}
return(e);
}
(8 )如果允许在循环队列的两端都可以进行插入和删除操作。要求:
① 写出循环队列的类型定义;
② 写出“从队尾删除”和“从队头插入”的算法。
[ 题目分析 ] 用一维数组 v[0…M-1] 实现循环队列,其中 M 是队列长度。设队头指针front 和队尾指针 rear ,约定 front 指向队头元素的前一位置, rear 指向队尾元素。定义front=rear 时为队空,(rear+1)%m=front 为队满。约定队头端入队向下标小的方向发展,队尾端入队向下标大的方向发展。
[ 算法描述 ]
①
#define M 队列可能达到的最大长度
typedef struct
{
elemtp data[M];
int front,rear;
}cycqueue;
②
elemtp delqueue ( cycqueue Q)
//Q 是如上定义的循环队列,本算法实现从队尾删除,若删除成功,返回被删除元素,
否则给出出错信息。
{
if (Q.front==Q.rear) {
cout<<" 队列空 "<<endl; exit(0);}
Q.rear=(Q.rear-1+M)%M; // 修改队尾指针。
return(Q.data[(Q.rear+1+M)%M]); // 返回出队元素。
}// 从队尾删除算法结束
void enqueue (cycqueue Q, elemtp x)
// Q 是顺序存储的循环队列,本算法实现“从队头插入”元素 x。
{
if(Q.rear==(Q.front-1+M)%M)
{cout<<" 队满 "<<endl; exit(0);)
Q.data[Q.front]=x; //x 入队列
Q.front=(Q.front-1+M)%M; // 修改队头指针。
}// 结束从队头插入算法。
(9)已知 Ackermann 函数定义如下 :
① 写出计算 Ack(m,n) 的递归算法,并根据此算法给出出 Ack(2,1) 的计算过程。
② 写出计算 Ack(m,n) 的非递归算法。
[ 算法描述 ]
int Ack(int m,n)
{
if (m==0) return(n+1);
else if(m!=0&&n==0) return(Ack(m-1,1));
else return(Ack(m-1,Ack(m,m-1));
}// 算法结束
① Ack(2,1) 的计算过程
Ack(2,1)=Ack(1,Ack(2,0)) // 因 m<>0,n<>0 而得
=Ack(1,Ack(1,1)) // 因 m<>0,n=0 而得
=Ack(1,Ack(0,Ack(1,0))) // 因 m<>0,n<>0 而得
=Ack(1,Ack(0,Ack(0,1))) // 因 m<>0,n=0 而得
=Ack(1,Ack(0,2)) // 因 m=0 而得
=Ack(1,3) // 因 m=0 而得
=Ack(0,Ack(1,2)) // 因 m<>0,n<>0 而得
= Ack(0,Ack(0,Ack(1,1))) // 因 m<>0,n<>0 而得
= Ack(0,Ack(0,Ack(0,Ack(1,0)))) // 因 m<>0,n<>0 而得
= Ack(0,Ack(0,Ack(0,Ack(0,1)))) // 因 m<>0,n=0 而得
= Ack(0,Ack(0,Ack(0,2))) // 因 m=0 而得
= Ack(0,Ack(0,3)) // 因 m=0 而得
= Ack(0,4) // 因 n=0 而得
=5 // 因 n=0 而得
②
int Ackerman(int m, int n)
{
int akm[M][N];int i,j;
for(j=0;j<N;j++) akm[0][j]=j+1;
for(i=1;i<m;i++)
{
akm[i][0]=akm[i-1][1];
for(j=1;j<N;j++)
akm[i][j]=akm[i-1][akm[i][j-1]];
}
return(akm[m][n]);
}// 算法结束
(10)已知 f 为单链表的表头指针 , 链表中存储的都是整型数据,试写出实现下列运算
的递归算法:
①求链表中的最大整数;
②求链表的结点个数;
③求所有整数的平均值。
[算法描述 ]
①
int GetMax(LinkList p)
{
if(!p->next)
return p->data;
else
{
int max=GetMax(p->next);
return p->data>=max ? p->data:max;
}
}
②
int GetLength(LinkList p)
{
if(!p->next)
return 1;
else
{
return GetLength(p->next)+1;
}
}
③
double GetAverage(LinkList p , int n)
{
if(!p->next)
return p->data;
else
{
double ave=GetAverage(p->next,n-1);
return (ave*(n-1)+p->data)/n;
}
}