02. 【解答】

2027 · OCR 辅助 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 02. 【解答】

算法思想:用 p 从头至尾扫描单链表,pre 指向 *p 结点的前驱,用 minp 保存值最小的结点指针(初值为 p),minpre 指向 *minp 结点的前驱(初值为 pre)。一边扫描,一边比较,若 p->data 小于 minp->data,则将 p、pre 分别赋值给 minp、minpre,如下图所示。当 p 扫描完毕时,minp 指向最小值结点,minpre 指向最小值结点的前驱结点,再将 minp 所指结点删除即可。

原转写引用的配图未随 Markdown 保存,请回到原 PDF 核验。


<!-- page: 63 -->

第2章 线性表
51

本题代码如下:

LinkList Delete_Min(LinkList &amp;amp;L) {
LNode *pre=L, *p=pre-&amp;gt;next; //p 为工作指针,pre 指向其前驱
LNode *minpre=pre, *minp=p; //保存最小值结点及其前驱
while (p != NULL) {
if (p-&amp;gt;data &amp;lt; minp-&amp;gt;data) {
minp=p; //找到比之前找到的最小值结点更小的结点
minpre=pre;
}
pre=p; //继续扫描下一个结点
p=p-&amp;gt;next;
}
minpre-&amp;gt;next=minp-&amp;gt;next; //删除最小值结点
free(minp);
return L;
}
算法需要从头至尾扫描链表,时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 若本题改为不带头结点的单链表,则实现上会有所不同,请读者自行思考。 ## 03. 【解答】 解法1:将头结点摘下,然后从第一结点开始,依次插入到头结点的后面(头插法建立单链表),直到最后一个结点为止,这样就实现了链表的逆置,如下图所示。

原转写引用的配图未随 Markdown 保存,请回到原 PDF 核验。


本题代码如下:

LinkList Reverse_1(LinkList L) {
LNode *p, *r; //p 为工作指针,r 为 p 的后继,以防断链
p=L-&amp;gt;next; //从第一个元素结点开始
L-&amp;gt;next=NULL; //先将头结点 L 的 next 域置为 NULL
while (p != NULL) { //依次将元素结点摘下
r=p-&amp;gt;next; //暂存 p 的后继
p-&amp;gt;next=L-&amp;gt;next; //将 p 结点插入到头结点之后
L-&amp;gt;next=p;
p=r;
}
return L;
}
解法2:大部分辅导书都只介绍解法1,这对读者的理解和思维是不利的。为了将调整指针这个复杂的过程分析清楚,我们借助图形来进行直观的分析。 假设pre、p和r指向三个相邻的结点,如下图所示。假设经过若干操作后,*pre之前的结点的指针都已调整完毕,它们的next都指向其原前驱结点。现在令*p结点的next域指向*pre结点,注意到一旦调整指针的指向,*p的后继结点的链就会断开,为此需要用r来指向原*p的后继结点。处理时需要注意两点:一是在处理第一个结点时,应将其next域置为NULL,而不是指向头结点(因为它将作为新表的尾结点);二是在处理完最后一个结点后,需要将头结点的指针指向它。

原转写引用的配图未随 Markdown 保存,请回到原 PDF 核验。


<!-- page: 64 -->

2027年数据结构考研复习指导

本题代码如下:

LinkList Reverse_2(LinkList L) {
LNode *pre, *p=L-&amp;gt;next, *r=p-&amp;gt;next;
p-&amp;gt;next=NULL; //处理第一个结点
while (r!=NULL) { //r 为空,则说明 p 为最后一个结点
pre=p; //依次继续遍历
p=r;
r=r-&amp;gt;next;
p-&amp;gt;next=pre; //指针反转
L-&amp;gt;next=p; //处理最后一个结点
return L;
}
上述两个算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 ## 04. 【解答】 因为链表是无序的,所以只能逐个结点进行检查,执行删除。 本题代码如下:
void RangeDelete(LinkList &amp;amp;L,int min,int max) {
LNode *pr=L,*p=L-&amp;gt;link; //p 是检测指针,pr 是其前驱
while (p!=NULL)
if (p-&amp;gt;data&amp;gt;min&amp;amp;&amp;amp;p-&amp;gt;data&amp;lt;max) { //寻找到被删结点,删除
pr-&amp;gt;link=p-&amp;gt;link;
free(p);
p=pr-&amp;gt;link;
}
else { //否则继续寻找被删结点
pr=p;
p=p-&amp;gt;link;
}
## 05. 【解答】 两个单链表有公共结点,即两个链表从某一结点开始,它们的 next 都指向同一结点。每个单链表结点只有一个 next 域,因此从第一个公共结点开始,之后的所有结点都是重合的,不可能再出现分叉。所以两个有公共结点而部分重合的单链表,拓扑形状看起来像 Y,而不可能像 X。 本题极容易联想到“蛮”方法:在第一个链表上顺序遍历每个结点,每遍历一个结点,在第二个链表上顺序遍历所有结点,若找到两个相同的结点,则找到了它们的公共结点。显然,该算法的时间复杂度为 $O(\mathrm{len1} \cdot \mathrm{len2})$。 接下来我们试着去寻找一个线性时间复杂度的算法。先把问题简化:如何判断两个单链表有没有公共结点?应注意到这样一个事实:若两个链表有一个公共结点,则该公共结点之后的所有结点都是重合的,即它们的最后一个结点必然是重合的。因此,我们判断两个链表是不是有重合的部分时,只需要分别遍历两个链表到最后一个结点。若两个尾结点是一样的,则说明它们有公共结点,否则两个链表没有公共结点。 然而,在上面的思路中,顺序遍历两个链表到尾结点时,并不能保证在两个链表上同时到达尾结点。这是因为两个链表长度不一定一样。但假设一个链表比另一个长 $k$ 个结点,我们先在长的链表上遍历 $k$ 个结点,之后再同步遍历,此时我们就能保证同时到达最后一个结点。两个链表从第一个公共结点开始到链表的尾结点,这一部分是重合的,因此它们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。 根据这一思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。在长 <!-- page: 65 --> 第2章 线性表 53 的链表上先遍历长度之差个结点之后,再同步遍历两个链表,直到找到相同的结点,或者一直到链表结束。此时,该方法的时间复杂度为 $O(\mathrm{len1} + \mathrm{len2})$。 ## 06. 【解答】 算法思想:循环遍历链表 C,采用尾插法将一个结点插入表 A,这个结点为奇数号结点,这样建立的表 A 与原来的结点顺序相同;采用头插法将下一结点插入表 B,这个结点为偶数号结点,这样建立的表 B 与原来的结点顺序正好相反。 本题代码如下:
LinkList DisCreat_2(LinkList &amp;amp;A) {
LinkList B = (LinkList)malloc(sizeof(LNode)); //创建 B 表表头
B-&amp;gt;next = NULL; //B 表的初始化
LNode *p = A-&amp;gt;next, *q; //p 为工作指针
LNode *ra = A; //ra 始终指向 A 的尾结点
while (p != NULL) {
ra-&amp;gt;next = p; ra = p; //将 *p 链到 A 的表尾
p = p-&amp;gt;next;
if (p != NULL) {
q = p-&amp;gt;next; //头插后,*p 将断链,因此用 q 记忆 *p 的后继
p-&amp;gt;next = B-&amp;gt;next; //将 *p 插入到 B 的前端
B-&amp;gt;next = p;
p = q;
}
}
ra-&amp;gt;next = NULL; //A 尾结点的 next 域置空
return B;
}
要特别注意的是,该算法采用头插法插入结点后,*p 的指针域已改变,若不设变量保存其后继结点,则会引起断链,从而导致算法出错。 ## 07. 【解答】 算法思想:题中链表是有序表,因此所有相同值域的结点都是相邻的。用 p 扫描递增单链表 L,若 *p 结点的值域等于其后继结点的值域,则删除后者,否则 p 移向下一个结点。 本题代码如下:
void Del_Same(LinkList &amp;amp;L) {
LNode *p = L-&amp;gt;next, *q; //p 为扫描工作指针
if (p == NULL)
return;
while (p-&amp;gt;next != NULL) {
q = p-&amp;gt;next; //q 指向 *p 的后继结点
if (p-&amp;gt;data == q-&amp;gt;data) { //找到重复值的结点
p-&amp;gt;next = q-&amp;gt;next; //释放 *q 结点
free(q); //释放相同元素值的结点
}
else
p = p-&amp;gt;next;
}
}
本算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 本题也可采用尾插法,将头结点摘下,然后从第一结点开始,依次与已经插入结点的链表的最后一个结点比较,若不等则直接插入,否则将当前遍历的结点删除并处理下一个结点,直到最后一个结点为止。 ## 08. 【解答】 <!-- page: 66 --> 2027年数据结构考研复习指导 算法思想:表A、B都有序,可从第一个元素起依次比较A、B两表的元素,若元素值不等,则值小的指针往后移,若元素值相等,则创建一个值等于两结点的元素值的新结点,使用尾插法插入到新的链表中,并将两个原表指针后移一位,直到其中一个链表遍历到表尾。 本题代码如下:
void Get_Common(LinkList A, LinkList B) {
LNode *p=A-&amp;gt;next, *q=B-&amp;gt;next, *r, *s;
LinkList C=(LinkList)malloc(sizeof(LNode)); //建立表C
r=C; //r始终指向C的尾结点
while (p!=NULL&amp;amp;&amp;amp;q!=NULL) { //循环跳出条件
if (p-&amp;gt;data&lt;q-&gt;data)
p=p-&amp;gt;next; //若A的当前元素较小,后移指针
else if (p-&amp;gt;data&amp;gt;q-&amp;gt;data)
q=q-&amp;gt;next; //若B的当前元素较小,后移指针
else { //找到公共元素结点
s=(LNode*)malloc(sizeof(LNode));
s-&amp;gt;data=p-&amp;gt;data; //复制产生结点*s
r-&amp;gt;next=s; //将*s链接到C上(尾插法)
r=s;
p=p-&amp;gt;next; //表A和B继续向后扫描
q=q-&amp;gt;next;
}
}
r-&amp;gt;next=NULL; //置C尾结点指针为空
}
## 09. 【解答】 算法思想:采用归并的思想,设置两个工作指针 $pa$ 和 $pb$,对两个链表进行归并扫描,只有同时出现在两集合中的元素才链接到结果表中且仅保留一个,其他的结点全部释放。当一个链表遍历完毕后,释放另一个表中剩下的全部结点。 本题代码如下:
LinkList Union(LinkList &amp;amp;la, LinkList &amp;amp;lb) {
LNode *pa=la-&amp;gt;next; //设工作指针分别为 pa 和 pb
LNode *pb=lb-&amp;gt;next;
LNode *u, *pc=la; //结果表中当前合并结点的前驱指针 pc
while (pa&amp;amp;&amp;amp;pb) {
if (pa-&amp;gt;data==pb-&amp;gt;data) { //交集并入结果表中
pc-&amp;gt;next=pa; //A中结点链接到结果表
pc=pa;
pa=pa-&amp;gt;next;
u=pb; //B中结点释放
pb=pb-&amp;gt;next;
free(u);
}
else if (pa-&amp;gt;data&amp;lt;pb-&amp;gt;data) { //若A中当前结点值小于B中当前结点值
u=pa;
pa=pa-&amp;gt;next; //后移指针
free(u); //释放A中当前结点
}
else { //若B中当前结点值小于A中当前结点值
u=pb;
pb=pb-&amp;gt;next; //后移指针
free(u); //释放B中当前结点
}
} //while 结束&lt;/q-&gt;

&lt;!-- page: 67 --&gt;

第2章 线性表
55

```txt
while(pa) { //B 已遍历完,A 未完
u=pa;
pa=pa-&amp;gt;next;
free(u); //释放 A 中剩余结点
}
while(pb) { //A 已遍历完,B 未完
u=pb;
pb=pb-&amp;gt;next;
free(u); //释放 B 中剩余结点
}
pc-&amp;gt;next=NULL; //置结果链表尾指针为 NULL
free(lb); //释放 B 表的头结点
return la;
链表归并类型的试题在各学校历年真题中出现的频率很高,故应扎实掌握解决此类问题的思想。该算法的时间复杂度为 $O(\mathrm{len}1 + \mathrm{len}2)$,空间复杂度为 $O(1)$。 ## 10. 【解答】 算法思想:因为两个整数序列已存入两个链表中,操作从两个链表的第一个结点开始,若对应数据相等,则后移指针;若对应数据不等,则 A 链表从上次开始比较结点的后继开始,B 链表仍从第一个结点开始比较,直到 B 链表到尾表示匹配成功。A 链表到尾而 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&amp;amp;&amp;amp;q)
if(p-&amp;gt;data==q-&amp;gt;data) { //结点值相同
p=p-&amp;gt;next;
q=q-&amp;gt;next;
}
else{
pre=pre-&amp;gt;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-&gt;next=q 或 q-&gt;prior=p,当循环双链表中结点个数为偶数时)为止,若它们所指结点值相同,则继续进行下去,否则返回 0。若比较全部相等,则返回 1。 本题代码如下: <!-- page: 68 --> 2027年数据结构考研复习指导
int Symmetry(DLinkList L) {
DNode *p=L-&amp;gt;next, *q=L-&amp;gt;prior; //两头工作指针
while (p!=q&amp;amp;&amp;amp;p-&amp;gt;next!=q) //循环跳出条件
if (p-&amp;gt;data==q-&amp;gt;data) { //所指结点值相同则继续比较
p=p-&amp;gt;next;
q=q-&amp;gt;prior;
}
else //否则,返回 0
return 0;
return 1; //比较结束后返回 1
}
**注意**
while 循环第二个判断条件易误写成 q-&amp;gt;next!=p,分析这样会产生什么问题,
## 12. 【解答】 算法思想:先找到两个链表的尾指针,将第一个链表的尾指针与第二个链表的头结点链接起来,再使之成为循环的。 本题代码如下:
LinkList Link(LinkList &amp;amp;h1, LinkList &amp;amp;h2) {
//将循环链表 h2 链接到循环链表 h1 之后,使之仍保持循环链表的形式
LNode *p, *q; //分别指向两个链表的尾结点
p=hl;
while (p-&amp;gt;next!=hl) //寻找 h1 的尾结点
p=p-&amp;gt;next;
q=h2;
while (q-&amp;gt;next!=h2) //寻找 h2 的尾结点
q=q-&amp;gt;next;
p-&amp;gt;next=h2; //将 h2 链接到 h1 之后
q-&amp;gt;next=hl; //令 h2 的尾结点指向 h1
return h1;
}
## 13. 【解答】 算法思想:首先在双链表中查找数据值为 x 的结点,查到后,将结点从链表上摘下,然后顺着结点的前驱链查找该结点的插入位置(频度递减,且排在同频度的第一个,即向前找到第一个比它的频度大的结点,插入位置为该结点之后),并插入到该位置。 本题代码如下:
DLinkList Locate(DLinkList &amp;amp;L, ElemType x) {
DNode *p=L-&amp;gt;next, *q; //p 为工作指针,q 为 p 的前驱,用于查找插入位置
while (p&amp;amp;&amp;amp;p-&amp;gt;data!=x)
p=p-&amp;gt;next; //查找值为 x 的结点
if (!p)
exit(0); //不存在值为 x 的结点
else{
p-&amp;gt;freq++; //令元素值为 x 的结点的 freq 域加 1
if (p-&amp;gt;pre==L || p-&amp;gt;pre-&amp;gt;freq &amp;gt; p-&amp;gt;freq)
return p; //p 是链表首结点,或 freq 值小于前驱
if (p-&amp;gt;next!=NULL) p-&amp;gt;next-&amp;gt;pre=p-&amp;gt;pre;
p-&amp;gt;pre-&amp;gt;next=p-&amp;gt;next; //将 p 结点从链表上摘下
q=p-&amp;gt;pre; //以下查找 p 结点的插入位置
while (q!=L&amp;amp;&amp;amp;q-&amp;gt;freq&amp;lt;=p-&amp;gt;freq)
q=q-&amp;gt;pre;
p-&amp;gt;next=q-&amp;gt;next;
}

&lt;!-- page: 69 --&gt;

第2章 线性表
57

```c
if(q-&amp;gt;next != NULL) q-&amp;gt;next-&amp;gt;pre = p; //将 p 结点排在同频率的第一个
p-&amp;gt;pre = q;
q-&amp;gt;next = p;
}
return p; //返回值为 x 的结点的指针
## 14. 【解答】 ### 1)算法的基本设计思想: 首先,遍历链表计算表长 $n$,并找到链表的尾结点,将其与首结点相连,得到一个循环单链表。然后,找到新链表的尾结点,它为原链表的第 $n-k$ 个结点,令 $L$ 指向新链表尾结点的下一个结点,并将环断开,得到新链表。 ### 2)本题代码如下:
LNode *Converse(LNode *L,int k){
int n=1; //n 用来保存链表的长度
LNode *p=L; //p 为工作指针
while(p-&amp;gt;next != NULL) { //计算链表的长度
p=p-&amp;gt;next;
n++;
} //循环执行后,p 指向链表尾结点
p-&amp;gt;next=L; //将链表连成一个环
for(int i=1;i&amp;lt;=n-k;i++) //寻找链表的第 n-k 个结点
p=p-&amp;gt;next;
L=p-&amp;gt;next; //令 L 指向新链表尾结点的下一个结点
p-&amp;gt;next=NULL; //将环断开
return L;
}
### 3)本算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 ## 15. 【解答】 ### 1)算法的基本设计思想: 设置快慢两个指针分别为 fast 和 slow 最初都指向链表头 head。slow 每次走一步,即 slow=slow-&gt;next;fast 每次走两步,即 fast=fast-&gt;next-&gt;next。fast 比 slow 走得快,若有环,则 fast 一定先进入环,而 slow 后进入环。两个指针都进入环后,经过若干操作后两个指针定能在环上相遇。这样就可以判断一个链表是否有环。 如下图所示,当 slow 刚进入环时,fast 早已进入环。因为 fast 每次比 slow 多走一步且 fast 与 slow 的距离小于环的长度,所以 fast 与 slow 相遇时,slow 所走的距离不超过环的长度。

原转写引用的配图未随 Markdown 保存,请回到原 PDF 核验。


如下图所示,设头结点到环的入口点的距离为 $a$,环的入口点沿着环的方向到相遇点的距离为 $x$,环长为 $r$,相遇时 fast 绕过了 $n$ 圈。

原转写引用的配图未随 Markdown 保存,请回到原 PDF 核验。


<!-- page: 70 -->

2027年数据结构考研复习指导

则有 $2(a + x) = a + n * r + x$,即 $a = nr - x$。显然从头结点到环的入口点的距离等于 $n$ 倍的环长减去环的入口点到相遇点的距离。因此可设置两个指针,一个指向 head,另一个指向相遇点,两个指针同步移动(均为一次走一步),相遇点即环的入口点。

2)本题代码如下:

LNode* FindLoopStart(LNode *head) {
LNode *fast = head, *slow = head; //设置快慢两个指针
while (fast != NULL &amp;amp;&amp;amp; fast-&amp;gt;next != NULL) {
slow = slow-&amp;gt;next; //每次走一步
fast = fast-&amp;gt;next-&amp;gt;next; //每次走两步
if (slow == fast) break; //相遇
}
if (fast == NULL || fast-&amp;gt;next == NULL)
return NULL; //没有环,返回 NULL
LNode *p1 = head, *p2 = slow; //分别指向开始点、相遇点
while (p1 != p2) {
p1 = p1-&amp;gt;next;
p2 = p2-&amp;gt;next;
}
return p1; //返回入口点
}
3)当 fast 与 slow 相遇时,slow 肯定没有遍历完链表,故算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。 16. 【解答】 1)算法的基本设计思想: 设置快、慢两个指针分别为 fast 和 slow,初始时 slow 指向 L(第一个结点),fast 指向 L-&gt;next(第二个结点),之后 slow 每次走一步,fast 每次走两步。当 fast 指向表尾(第 $n$ 个结点)时,slow 正好指向链表的中间点(第 $n/2$ 个结点),即 slow 正好指向链表前半部分的最后一个结点。将链表的后半部分逆置,然后设置两个指针分别指向链表前半部分和后半部分的首结点,在遍历过程中计算两个指针所指结点的元素之和,并维护最大值。 2)本题代码如下:
int PairSum(LinkList L) {
LNode *fast = L-&amp;gt;next, *slow = L; //利用快慢双指针找到链表的中间点
while (fast != NULL &amp;amp;&amp;amp; fast-&amp;gt;next != NULL) {
fast = fast-&amp;gt;next-&amp;gt;next; //快指针每次走两步
slow = slow-&amp;gt;next; //慢指针每次走一步
}
LNode *newHead = NULL, *p = slow-&amp;gt;next, *tmp;
while (p != NULL) {
tmp = p-&amp;gt;next; //p 指向当前待插入结点,令 tmp 指向其下一结点
p-&amp;gt;next = newHead; //将 p 所指结点插入到新链表的首结点之前
newHead = p; //newHead 指向刚才新插入的结点,作为新的首结点
p = tmp; //当前待处理结点变为下一结点
}
int mx = 0; p = L;
LNode *q = newHead;
while (q != NULL) {
if ((p-&amp;gt;data + q-&amp;gt;data) &amp;gt; mx) //用 mx 记录最大值
mx = p-&amp;gt;data + q-&amp;gt;data;
p = p-&amp;gt;next;
q = q-&amp;gt;next;
}
}

&lt;!-- page: 71 --&gt;

第2章 线性表
59

```txt
return mx;
}
3)本算法的时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。