一、前言
本文习题均来自25王道考研的教材课后习题,在给出答案的基础上加上自己的分析,方便大家理解学习,部分题目我列出了我本人的解法和王道书的解法,大家对比着看会更有体会。算法大题不需要找到最优算法,考试中直接使用暴力解法就可以,不要在乎时空复杂度,哪怕用最烂的算法,只要写对,并且对应的时空复杂度写对,最多扣3分,如果是次优算法,只扣1-2分。最优算法是为了学习不是为了答题!
本文是线性表的第二部分——链表的课后题,顺序表的课后题在这一篇:
考研数据结构——线性表相关大题(含解析和代码)_数据结构考研真题线性表-CSDN博客
附一些初始化链表的基本操作代码,方便测试和运行:
#include<iostream>
using namespace std;
//单链表操作
//创建一个带头结点的单链表
struct LNode {
int data; //存储的数据类型
struct LNode* next; //指针指向下一结点
};
typedef LNode LNode;
typedef LNode* LinkList;
//初始化一个简单的空表(带头结点)
bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode)); //给单链表分配空间
if (L == NULL) //分配内存失败
return false;
L->next = NULL; //设置头结点为空
return true;
}
//尾插法建立一个单链表(包括用户输入)
LinkList List_TailInsert(LinkList& L) {
int x; //设置插入元素数据类型为int型
L = (LNode*)malloc(sizeof(LNode)); //申请一块内存空间,建立头结点
L->next = NULL; //初始化空链表
LNode* s, * r = L; //声明两个指针,s是新插入的元素,r指向尾节点
scanf_s("%d", &x); //用户输入插入的数据
while (x != 9999) { //设置一个判定条件
s = (LNode*)malloc(sizeof(LNode)); //为s结点申请内存空间
s->data = x; //s的数据域存放x的值
r->next = s; //连接s和前面的链表
r = s; //尾指针r指向新插入的结点,即永远保持r为尾节点
scanf_s("%d", &x); //继续接收用户传参
}
r->next = NULL; //尾指针的指针域指向NULL
return L;
}
//声明一个带头结点,且元素为101-110的单链表L
void List_TailInsert_M(LinkList& L) {
L = (LinkList)malloc(sizeof(LNode*));
L->next = NULL;
LNode* p, * r = L;
for (int i = 1; i <= 10; i++) {
p = (LinkList)malloc(sizeof(LNode*)); //给p申请一块内存空间
p->data = i + 100;
r->next = p;
r = p;
}
r->next = NULL;
}
//打印函数,打印单链表L的所有元素
void PrintL(LinkList L) {
LNode* p = L->next; //p指向头指针L
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
}
//循环双链表操作
//创建一个循环双链表
typedef struct DNode {
int data;
struct DNode* next;
struct DNode* prior;
}DNode, * DLinkList;
//初始化一个循环双链表
bool InitDList(DLinkList& L) {
L = (DNode*)malloc(sizeof(DNode));
if (L == NULL) //分配内存失败
return false;
L->prior = L;
L->next = L;
return true;
}
//向循环双链表中插入固定数据101-110
void DList_Insert(DLinkList& L) {
DNode* p, * r = L;
for (int i = 1; i <= 10; i++) {
p = (DLinkList)malloc(sizeof(DNode*));
p->data = i + 100; //把p插入到r后面
p->next = r->next;
r->next->prior = p;
p->prior = r; //p的前驱是r
r->next = p; //r的后继是p
r = r->next; //插入结束之后r后移一位,r始终指向尾节点
}
}
//以用户输入方式创建一个循环双链表
void List_TailInsert(DLinkList& L) {
int x; //插入数据类型为int型
DNode* p, * r = L;
scanf_s("%d", &x);
while (x != 9999) {
p = (DNode*)malloc(sizeof(DNode)); //将p插入到r后面
p->data = x;
p->next = r->next; //开始变换指针
r->next->prior = p;
p->prior = r;
r->next = p; //插入结束
r = r->next; //r要向后移一位,r始终指向L的尾节点
scanf_s("%d", &x);
}
}
//打印函数,打印循环双链表L的所有元素
void PrintDL(DLinkList L) {
DNode* p = L->next;
while (p != L) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
//双链表的操作
//创建一个双链表
typedef struct DNode {
int data;
struct DNode* next;
struct DNode* prior;
}DNode, * DLinkList;
//初始化一个双链表
bool InitDList2(DLinkList& L) {
L = (DNode*)malloc(sizeof(DNode));
if (L == NULL)
return false;
L->prior = NULL; //头结点的prior永远指向NULL
L->next = NULL; //头结点之后暂时还没有节点
return true;
}
//向双链表中插入固定数据101-110
void DList_Insert2(DLinkList& L) {
DNode* p = L, * s; //p是工作指针,将s插入到p后面
for (int i = 1; i <= 10; i++) {
s = (DNode*)malloc(sizeof(DNode));
s->data = i + 100;
s->next = p->next;
if (p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
p = p->next; //每次循环结束后,p要往后移一位
}
}
//以用户输入方式创建一个双链表
void DList_TailInsert(DLinkList& L) {
int x; //x用于接收用户传参
DNode* p = L, *s;
scanf_s("%d", &x);
while (x != 9999) {
s = (DNode*)malloc(sizeof(DNode));
s->data = x;
s->next = p->next;
if (p->next != NULL) //当p有后继结点的时候才执行这条语句
p->next->prior = s;
s->prior = p;
p->next = s;
p = p->next;
scanf_s("%d", &x);
}
}
//打印函数,打印双链表L的所有元素
void PrintDL2(DLinkList L) {
DNode* p = L->next;
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
二、题目
1.在带头结点的单链表L中,删除所有值为x的结点,并释放其空间,假设值为x的节点不唯一,试编写算法实现。
思路1:用p从头至尾扫描单链表,pre指向*p结点的前驱。若p所指结点的值为x,则删除,并让p移向下一个节点,否则让pre、p指针同步后移一个结点。
本算法是在无序单链表中删除满足某种条件的所有节点,这里的条件可以任意指定,只要修改if条件判断语句即可。比如要删除值介于a和b之间的节点,将if判断改为“p->data > a && p->data < b”即可。
void Del_X_1(LinkList& L , int x) {
LNode* p = L->next, * pre = L, * q;
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;
}
}
}
思路2:采用尾插法建立单链表。用p指针扫描L的所有节点,当值补位x时,将其链接到L之后,否则将其释放。
void Del_X_2(LinkList& L, int x) {
LNode* p = L->next, * r = L, * q; //r指向尾节点,其初值为头结点L
while (p != NULL) {
if (p->data != x) {
r->next = p; //将p插入到r的后面(后插法)
r = p; //r始终指向尾节点
p = p->next; //p继续向后扫描
}
else {
q = p;
p = p->next; //继续向后扫描
free(q); //释放删除节点的空间
}
}
}
上述两个算法时空复杂度分别为:O(n),O(1)。
2.编写在带头结点的单链表L中删除一个最小值结点的高效算法。(假设该节点唯一)
思路:
-
先分析:第一步要找到最小值结点;第二步要删除最小值结点;
-
要完成第一步很容易,设一个指针p从头至尾遍历单链表就能找出来,但只有一个指针p是无法实现删除操作的,要想实现删除操作就必须有p结点(即被删节点)的前驱结点的指针,设为prep;
-
但只有指针p和他的前驱结点prep还是不够,因为要删除的结点不是p,是最小值结点,当遍历完一边链表L之后,p指针指向链表最后一个元素,不是我们要的最小值节点。于是我们需要另设一个指向最小值的结点指针minp以及它的前驱结点minpre;
-
那这样是不是prep指针就没必要了呢?不是的,prep要和p指针进行比较,从而得出最小值的指针位置。然后把p赋给minp,pre赋给minpre;
-
这样,在扫描完毕时,我们会得到minp指向最小值节点,minpre指向最小值节点的前驱结点,再将minp所指向的结点删除即可。
整体思路:用p从头至尾扫描单链表,pre指向p结点的前驱,用minp保存值最小的结点指针(初值为p),minpre指向minp结点的前驱(初值为pre)。一边扫描,一边比较,若p->data小于minp->data,则将p、pre分别赋值给minp、minpre。在扫描完毕时,我们会得到minp指向最小值节点,minpre指向最小值节点的前驱结点,再将minp所指向的结点删除即可。
LinkList Delete_Min(LinkList& L) {
LNode* pre = L, * p = pre->next; //p为工作指针,pre指向p的前驱
LNode* minpre = pre, * minp = p; //保存最小值节点minp及其前驱minpre
while (p != NULL) {
if (p->data < minp->data) {
minp = p;
minpre = pre;
}
pre = p; //继续扫描下一结点,p和pre都向后移一位
p = p->next;
}
//循环结束之后,minp是最小值的指针,minpre是最小值的前驱结点,目的是删除minp
minpre->next = minp->next; //删除最小值节点
free(minp);
return L;
}
void test02() {
LinkList L;
//L = (LinkList)malloc(sizeof(LNode));
InitList(L); //初始化一个空表
L = List_TailInsert(L); //往表中添加元素
cout << "未删除之前:";
PrintL(L);
L = Delete_Min(L);
cout << endl << "删除之后:";
PrintL(L); //打印表L查看结果
}
3.试编写算法将带头结点的单链表就地逆置,即时间复杂度为O(1)
比较简单,不做解析,但这串代码很重要,一定要掌握。
void Reverse(LinkList& L) {
LNode* p, * s;
p = L->next; //p指向第一个结点,依次遍历
L->next = NULL; //将L的next域置为空,将每一次的p插入到L后面
while (p) {
s = p->next;
p->next = L->next; //将p置为尾指针,p的next域指向NULL
L->next = p; //将p连接到L后面,让L的next域指向p
p = s; //更新p为下一结点
}
}
void test03() {
LinkList L;
InitList(L); //初始化一个空表
L = List_TailInsert(L); //往表中添加元素
cout << "未逆置之前:";
PrintL(L);
Reverse(L);
cout << endl << "逆置之后:";
PrintL(L);
}
4.设在一个带表头结点的单链表中,所有节点的元素值无序,试编写函数删除表中所有介于给定的两个值(作为参数给出)之间的元素(若存在)。
这是王道书的答案,代码肯定没问题,但是我运行的时候报内存异常,尚未解决。可能是我环境配置或者其他什么问题。
void RangeDelete(LinkList& L, int a, int b) {
LNode* p = L->next, * pre = L; //p为工作指针,遍历链表;pre指向p的前驱,用于删除p
while (p != NULL) {
if (p->data > a && p->data < b) {
pre->next = p->next;
free(p);
p = pre->next;
}
else {
pre = p;
p = p->next;
}
}
}
5.给定两个单链表,试分析 找出两个链表的公共节点 的思想(不用写代码)
两个单链表有公共节点,即两个链表从某一节点开始,他们的next都指向同一结点。由于每个单链表结点只有一个next域,因此从第一个公共结点开始,之后的所有节点都是重合的,不可能出现分叉。
暴力解法:
-
在第一个链表上顺序遍历每个结点,每遍历一个结点,在第二个链表上顺序遍历所有的结点,若找到两个相同的结点,则找到了他们的公共结点。
优化思路:
-
先简化问题,若两个链表有一个公共结点,则该公共结点之后的所有节点都是重合的,则他们的最后一个结点必然是重合的。因此,我们判断力两个链表是否有重合的部分时,只需要分别遍历两个链表到最后一个结点。若两个尾节点是一样的,则说明他们有公共结点,否则两个链表没有公共结点。
-
然而,上述思路在找到两个链表的尾节点时,不能保证在两个链表上同时到达尾节点。这是因为两个链表长度不一定一样。假设 A 链表比 B 链表长 k 个节点,我们先在 A 链表上遍历 k 个结点,之后再同步遍历,此时就能保证同时到达最后一个结点。
-
由于两个链表从第一个公共结点开始到链表的尾节点,这一部分是完全重合的,因此他们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共结点。
-
根据这一思路,我们先要分别遍历两个链表得到他们的长度,并求出两个长度之差 k 。在长的链表上线遍历 k 个节点之后,再同步遍历两个链表,直到找到相同的结点,或一直到链表结束。
6.设 C={a1,b1,a2,b2,...,an,bn}为线性表,采用带头结点的单链表存放,设计一个就地算法,将其拆分为两个线性表,使得 A={a1,a2,...,an},B={bn,...,b2,b1}
本例我的数据为 101-110 的整数顺序表。DisCreat_2_M 是我自己写的代码,我的思路是“隔一个遍历,共n次,每次将b的元素用头插法插入到新的链表B中,并删除b,留下来的就是A,函数返回B,L变成了A”,但是好像行不通,报越界异常;DisCreat_2 是王道书的代码,思路如下。
思路:循环遍历链表C,采用尾插法将一个结点插入表A,这个结点为奇数号结点,这样建立的表A与原来结点顺序相同;采用头插法将下一结点插入表B,这个结点为偶数号结点,这样建立的表B与原来的结点顺序正好相反。
注意:采用头插法插入节点后,*p的指针域已改变,若不设变量保存其后继结点,则会引起断链,导致出错。
LinkList DisCreat_2_M(LinkList& L) {
LinkList B;
InitList(B); //初始化一个表B
LNode* p = L->next->next, * pre = L->next, * q; //p指向b1,pre指向a1
while (p) {
q = p;
q->next = B->next;
B->next = q;
if (p->next == NULL) {
free(q);
break;
}
p = p->next->next;
free(q);
}
return B;
}
LinkList DisCreat_2(LinkList& A) {
LinkList B; //创建表B
InitList(B);
LNode* p = A->next, * q; //p为工作指针
LNode* ra = A; //ra始终指向A的尾节点
while (p != NULL) {
ra->next = p; //将*p链到A的表尾,此时p指向a结点,只有在工作时才指向b结点
ra = p; //ra往后移,移动到p的位置,即A的尾节点位置
p = p->next; //p再往后移,移动到b结点,准备工作
if (p != NULL) {
q = p->next; //头插后,*p将断链,因此用q存储*p的后继结点,q指向ai+1结点,即正式工作之前保存p结点的下一结点
p->next = B->next; //将*p插入到B的前端(头插法)
B->next = p;
p = q; //p指向工作之前(p指向b的时候)的后继结点,即ai+1结点
}
}
ra->next = NULL; //A尾节点的next域置空,否则an的next域会指向bn
return B;
}
void test06() {
LinkList A, B;
InitList(A);
List_TailInsert_M(A); //初始化一个值为101-110的链表
cout << "未改动前:";
PrintL(A);
B = DisCreat_2(A);
cout << "改动之后A:";
PrintL(A);
cout << "改动之后B:";
PrintL(B);
}
代码解析:
-
传参是A而不是L,正是“就地”算法的体现;
-
需要几个指针:(建议手推)以第一次循环为例,因为要遍历链表将 b 结点移入 B 链表中,所以需要一个工作指针 p 。p 工作时指向 b 结点,每次工作会隔过去一个结点 a ,如果只有 p 结点,那么第一次工作时将 b1->next=NULL ,第二次工作时将 b2->next=b1 ,会发现 b1和 b2 中间的a结点不见了,发生了断链,所以需要一个指针q来保存每次p指针工作之前的下一结点(即a结点),以维持A链表的连续性。而将b1插入到B链表之后,此时a1的指针仍然指向的是b1,那么就需要修改a1的指针,让它指向a2,而此时我们有p指针指向b1,q指针指向a2,还需要一个ra指针指向a1。故共需要三个指针ra,p,q。
-
ra始终指向A的尾节点,这个A即为最终要求的A,而不是题目给的链表L;
-
p是工作指针,所谓工作就是p指针用来实现将链表中的b结点的所有元素插入到B链表中,p在空闲时指向a节点,工作时指向b结点,每一次循环开始前,p都指向a结点;
-
q指针保存 p 工作之前指向结点(a)的下一结点(b),以让 p 每次工作(将b结点以头插法插入B链表)之后都能回到它本应该指向的位置(a结点);
-
于是,我们知道,每一次 p 在工作的时候指向 bi 结点,此时 ra 指向 p 的前一个结点 ai (也是链表 A 的最后一个节点),q 指向 p 的后一个结点 ai+1(即 p 工作完之后要指向的结点);
-
最后注意将 A 的尾节点的下一结点置为 null ,否则 an 的 next 域会指向 bn。
7.在一个递增有序的单链表中,存在重复元素,设计算法删除重复元素。
思路:依次遍历有序表,用两个指针分别指向当前扫描的结点p和下一结点q,两者对比,相等则删除q节点,不等则分别向后移一位。下面第一个是我写的,后面一个是王道书的答案。
时间复杂度O(n),空间复杂度O(1)。
void Del_Same_M(LinkList& L) {
LNode* p = L->next, * q = p->next; //p指向首节点,q指向p的后继结点
while (q != NULL) {
if (p->data == q->data) { //如果pq相等,删除q结点
q = q->next;
p->next = q;
}
else { //不相等就往后移一位
q = q->next;
p = p->next;
}
}
}
void Del_Same(LinkList& L) {
LNode* p = L->next, * q;
if (p == NULL)
return;
while (p->next != NULL) {
q = p->next;
if (p->data == q->data) {
p->next = q->next;
free(q);
}
else
p = p->next;
}
}
//测试数据
//1 2 2 4 5 6 6 8 9 15 15 19 9999
void test07() {
LinkList L;
InitList(L);
List_TailInsert(L);
cout << "未删除前:";
PrintL(L);
Del_Same(L);
cout << "删除之后:";
PrintL(L);
}
8.设A和B是两个单链表(带头结点),其中元素递增有序。设计一个算法从AB中的公共元素产生单链表C,要求不破坏AB的结点。
思路:分别用两个指针pq指向AB两个链表,作为工作指针。分别比较pq的数据大小,如果相等,就尾插法到C里面,如果不等,哪个小就把哪个指针往后移一位(因为AB是递增的)。
下面是我写的代码,王道书的代码思路和我的一样,写的也差不多,就不放了,避免冗杂。
LinkList Get_Common(LinkList A, LinkList B) {
LinkList C; //建立一个单链表C
InitList(C);
LNode* r = C;
LNode* p = A->next, * q = B->next; //pq分别指向AB的首节点
if (p == NULL || q == NULL)
return C;
while (p != NULL && q != NULL) { //当AB都没有遍历结束时,进行比较
if (p->data == q->data) {
r->next = p; //把p接到C表后面
r = r->next; //r也向后移一位
p = p->next; //pq分别向后移一位
q = q->next;
}
else if (p->data < q->data) //pq哪个小哪个往后移一位
p = p->next;
else if (p->data > q->data)
q = q->next;
}
r->next = NULL;
return C;
}
//A:2 4 5 8 9 16 18 21 25 29 32 9999 4 9 18 19 20 21 22 25 9999
//B:4 9 18 19 20 21 22 25 9999
void test08() {
LinkList A,B;
InitList(A);
InitList(B);
List_TailInsert(A); //插入元素
List_TailInsert(B); //插入元素
cout << "未改变前A:";
PrintL(A);
cout << "未改变前B:";
PrintL(B);
LinkList C;
InitList(C);
cout << "改变之后C:";
C = Get_Common(A, B);
PrintL(C);
}
9.已知两个链表AB分别表示两个集合,其元素递增排列。编写函数,求AB的交集,并存放于A链表中。
思路:整体思路和第8题一样,设置两个工作指针pa,pb,对两个链表进行归并扫描,只有同时出现在两集合中的元素才链接到结果表中且只保留一个,其他的结点全部释放。当一个链表遍历完毕后,释放领一个表中剩下的全部节点。没有设置例子。
LinkList Union(LinkList& A, LinkList& B) {
LNode* pa = A->next, * pb = B->next, * u;
LNode* pc = A; //pc充当A的指针,实现后插操作
while (pa && pb) { //当pa、pb不为空时
if (pa->data < pb->data) { //pa小于pb时,删除pa当前元素,并把pa后移一位
u = pa;
pa = pa->next;
free(u);
}
else if (pa->data > pb->data) { //pb小于pa时,删除pb当前元素,并把pb后移一位
u = pb;
pb = pb->next;
free(u);
}
else { //pa==pb时
pc->next = pa; //pa链到pc后面
pc = pa; //pa后移
pa = pa->next;
u = pb; //释放pb
pb = pb->next; //pb后移
free(u);
}
}
while (pa) {
u = pa;
pa = pa->next;
free(u);
}
while (pb) {
u = pb;
pb = pb->next;
free(u);
}
pc->next = NULL;
free(B); //释放B的头结点
return A;
}
10.两个整数序列A=a1,a2,...,am和B=b1,b2,...,bn已经存入两个单链表中,设计一个算法,判断序列B是否是序列A的连续子序列。
思路:我的思路是用两个指针p、q分别指向A、B两个链表,先将p指针移动到和q指针元素相同的位置,即第一个while循环,然后pq同步进行比较,如果自此之后pq的元素全都一一对应,那么B就是A的连续子序列;但凡有一个不相等,B就不是A的连续子序列。除此之外还要注意B的后端比A长的情况,这一点在后面的代码分析里面有解释。
王道书的思路:因为两个整数序列已经存入两个链表中,操作从两个链表的第一个节点开始,若对应数据相等,则后移指针;若对应数据不等,则A链表从上次开始比较结点的后继开始,B链表扔从第一个结点开始比较,直到B链表到尾表示匹配成功。A链表到尾而B链表没有到尾表示失败。操作中应记住A链表每次的开始节点,以便下次匹配时好从其后继开始。
下面是我的代码,王道书的代码和我的思路不一样,他是直接比较,我是先让pq相等再进行比较,但殊途同归,就没有敲。
void Pattern(LinkList A, LinkList B) {
LNode* p = A->next, * q = B->next;
while (p->data != q->data) { //当pq的数据不相等时,p继续向后,q位置不变
if (p != NULL)
p = p->next;
else if (p == NULL) //若p为空,则直接return,表明AB没有公共元素,B就一定不是A的连续子序列
return;
}
//循环出来之后,p和q的数据一定相等,再判断pq后面的元素是否一一对应相等
while (p != NULL && q != NULL) {
if (p->data != q->data) {
cout << "B不是A的连续子序列" << endl;
return;
}
p = p->next; //相等的话,pq都向后移一位
q = q->next;
}
if(q==NULL)
cout << "B是A的连续子序列" << endl;
else
cout << "B不是A的连续子序列" << endl;
}
//A:1 2 5 8 9 15 18 6 14 4 9999 15 18 6 14 4 9999
//B:15 18 6 14 4 9999
void test09() {
LinkList A, B;
InitList(A);
InitList(B);
List_TailInsert(A); //插入元素
List_TailInsert(B); //插入元素
cout << "初始A:";
PrintL(A);
cout << "初始B:";
PrintL(B);
Pattern(A, B);
}
代码解析:
-
代码本身比较简单,思路也简单,不过要注意一点,就是最后的if条件判断。循环出来之后,要判断如果q==NULL,才能说明B是A的连续子序列,否则如果B的前一部分是A的子序列,但当A遍历结束的时候B还没有遍历结束,就会造成B后面多一节。比如:A:1 2 3 4 5 6;B:4 5 6 7 8,这种情况显然是不对的,但如果不加判断就仍然会判断为B是A的连续子序列。
11.设计一个算法用于判断带头结点的循环双链表是否对称。
思路:设置两个指针p和r,分别指向头结点L的前驱和后继结点,循环遍历L的每一个结点,如果p和r的data相等,就把p前移一位,r后移一位,继续遍历;如果不等,就直接返回并输出“链表不对称”。直到p和r相等(即L遍历完一遍)时,输出“链表对称”。
void Symmetry(DLinkList L) {
DNode* p = L->prior, * r = L->next; //令指针p和r都指向头结点L,分别比较p的前驱和r的后继
while (p != r) {
if (p->data != r->data) {
cout << "该链表不对称!" << endl;
return;
}
//如果他们相等,就把p和r各向后移一位(p朝前移,r朝后移)
p = p->prior;
r = r->next;
}
cout << "该链表对称!" << endl;
}
//三组数据
//1. 1 2 3 3 2 1 9999
//2. 1 2 3 2 1 9999
//3. 1 5 8 1 2 5 3 5 9999
void test10() {
DNode* L; //初始化一个循环双链表L
InitDList(L);
List_TailInsert(L); //向循环链表中输入数据
cout << "L:";
PrintDL(L);
Symmetry(L); //判断L是否对称
}
12.有两个循环单链表,链表头指针分别为h1和h2,编写函数将链表h2链接到链表h1之后,要求链接后的链表扔保持循环链表形式。
思路:我的思路是链接“A的尾节点 和 B的头结点”以及“A 和 B的尾节点”,两两相连就可以。王道书:“先找到两个链表的尾指针,将第一个链表的尾指针与第二个链表的头结点连接起来,再使之成为循环。”。
Link_M 是我写的代码,运行正常。(补充:我好像把题目读错了,我按照循环双链表写的,与题无关,不过可供参考,test也是按照我的函数写的数据,是双链表,与题无关)
Link 是王道书的代码,大家可以对比看下。
void Link_M(DLinkList& A, DLinkList& B) {
DNode* h1 = A, * h2 = B;
h1->prior->next = h2->next; //链接A的尾节点和B的头结点
h2->next->prior = h1->prior;
h1->prior = h2->prior; //链接A和整个链表的尾节点(即B的尾节点)
h2->prior->next = h1;
}
LinkList Link(LinkList& h1, LinkList& h2) {
LNode* p, * q;
p = h1;
while (p->next != h1)
p = p->next;
q = h2;
while (q->next != h2)
q = q->next;
p->next = h2;
q->next = h1;
return h1;
}
void test12() {
DLinkList A, B;
InitDList(A);
InitDList(B);
DList_Insert(A); //插入固定数据101-110
DList_Insert(B);
cout << "A:";
PrintDL(A);
cout << "B:";
PrintDL(B);
Link(A, B); //调用链接函数
cout << "链接之后A:";
PrintDL(A);
}
13.设将n(n>1)个整数存放到不带头结点的单链表L中,设计算法将L中保存的序列循环右移k个位置。例如k=1,将链表{0,1,2,3}变为{3,0,1,2}。
思路:首先遍历链表计算表长n,并找到链表的尾节点,将其与首节点相连,得到一个循环单链表。然后找到新链表的尾节点,它为原链表的第 n-k 个节点,令L指向新链表尾节点的下一个节点,并将环断开,得到新链表。
LinkList Converse(LNode* L, int k) {
LNode* p = L->next;
int n = 1;
while (p != NULL) { //寻找链表L的尾节点,计算链表长度n
p = p->next;
n++;
}
//循环出来之后p指向L的尾节点
p->next = L; //将链表L链成一个环
//原链表的第 n-k 个节点是新链表的尾节点
for (int i = 0; i < n - k; i++)
p = p->next;
L = p->next;
p->next = NULL;
return L;
}
时间复杂度为O(n),空间复杂度为O(1)。
14.单链表有环,使之单链表的最有一个节点的指针指向了链表中的某个结点(通常单链表的最有一个节点的指针域是空的)。试编写算法判断单链表是否存在环,如果存在,则返回环的入口节点。
思路:
-
设置快慢指针fast和slow遍历链表,fast一次走两步,slow一次走一步,那么如果有环,fast一定比slow先进入环,随着步骤进行,slow一定会和fast相遇,相遇的位置称为“相遇点”;如果没有环,fast最后会指向null。至于如何找到“入口点”,需要一定的思想:
-
-
如图所示,是一个有环的链表,没有进入环之前的链表长度是 a ,环上两个点从前至后依次是“入口点”和“相遇点”。设头结点到环的入口点的距离为 a ,环的入口点沿着环的方向到相遇点的距离为 x ,环长为 r ,相遇时 fast 绕过了 n 圈。
-
那么可以知道,slow 指针走到相遇点的距离为 a+x ,则 fast 指针走到相遇点的距离为 2(a+x) ,也可以表示成 a+nr+x,那么有 2(a+x)=a+nr+x ,可得 a=nr-x 。
-
显然从头结点到环的入口点的距离 a 等于 n 倍的环长 r 减去环的入口点到相遇点的距离 x 。因此可设置两个指针 p1 和 p2 ,一个指向 head ,一个指向 slow 和 fast 的相遇点,两个指针同步移动,一次走一步,他们的相遇点即为环的入口点。
-
对上一步的解释:当 p1 走了 a 的距离之后,到达了入口点,此时 p2 也走了 a ,即 p2 走了 nr-x ,而 p2 的起始点是在环上第 x 个位置,那么此时 p2 也在入口点,他们就会相遇。
LNode* FindLoopStart(LNode* head) {
LNode* fast = head, * slow = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow) //如果相遇了
break;
}
//跳出while循环后,要么fast和slow在相遇点,要么fast或fast->next指向NULL
if (fast == NULL || fast->next == NULL) //表明没有环
return NULL;
LNode* p1 = head, * p2 = slow;
while (p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
return p1; //返回入口点
}
时间复杂度为O(n),空间复杂度为O(1)。
15.设有一个长度n(n为偶数)的不带头结点的单链表,且结点值都大于0,设计算法求这个单链表的最大孪生和。孪生和定义为一个节点值与其孪生节点值之和,对于第 i 个节点(从0开始),其孪生节点为第 n-i-1 个结点。
思路:
-
由题可知,扫描的结点和他的孪生节点是关于链表中心对称的,即第0个节点(首节点)的孪生节点就是第n-1个节点(尾节点)。
-
设置快慢指针slow和fast,初始slow指向L(第一个结点),fast指向L->next(第二个结点),之后slow每次走一步,fast每次走两步,因为n为偶数,所以当fast指向表尾时,slow刚好指向第 n/2 个节点,即slow正好指向前半部分的最后一个结点。
-
将链表的后半部分逆置,设置两个指针分别指向链表前半部分和后半部分的首节点,在遍历过程中计算两个指针所指结点的元素之和,维护最大值。
int PairSum(LinkList L) {
LNode* fast = L->next, * slow = L;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
//循环出来之后slow指向中间结点,fast指向尾节点
LNode* newHead = NULL, * p = slow->next, * tmp;
while (p != NULL) { //反转链表的后一半元素,采用头插法
tmp = p->next; //p代表当前要移动的节点,tmp保存p的下一结点,因为p一旦链到newHead前面,会断链
p->next = newHead; //p头插到newHead前面
newHead = p; //newHead重新指向头结点,方便下一次头插操作
p = tmp; //p指向下一个要操作的元素结点
}
int mx = 0; //mx存储最大值
p = L; //p指向链表的头结点,准备遍历
LNode* q = newHead; //q指向链表后半部分的头结点
while (p != NULL) {
if ((p->data + q->data) > mx) {
mx = p->data + q->data;
}
p = p->next;
q = q->next;
}
return mx;
}
16.已知一个带有表头结点的单链表,节点结构有data域和link域(我表示为next),假设该链表只给出了头指针list。在不改变链表的前提下,请设计算法查找链表中倒数第k个位置的结点。若查找成功,算法输出该结点的data域的值,并返回1;否则返回0。
思路:我的思路是创建好一个单链表之后,传参传入头指针list和要找的位置k;让p指向list,作为工作指针,先遍历一遍链表得到链表长度,倒数第k个位置的节点就是第 n-k 个位置的结点,再遍历一次就可以得到了。
王道书的思路是:定义两个指针变量p和q,初始时均指向头结点的下一个结点(链表的第一个结点),p指针沿链表移动;当p指针移动到第k个节点时,q指针开始与p指针同步移动;当p指针移动到最后一个结点时,q指针所指示结点为倒数第k个节点。
FinkK_M 是我写的代码,遍历了两次链表,满分15分的话最高给10分;Search_k 是王道书给的代码,只用遍历一次链表。
int FindK_M(LNode* list, int k) {
LNode* p = list; //p指向list,作为工作指针
int n = 1;
while (list->next != NULL) {
n++;
list = list->next;
}
if (n >= k) {
for (int i = 0; i < n - k; i++)
p = p->next;
cout << p->data;
return 1;
}
cout << "查找失败!" << endl;
return 0;
}
int Search_k(LinkList list, int k) {
LNode* p = list->next, * q = list->next; //pq指向第一个结点
int count = 0; //计数器,记录p扫描到第k个节点
while (p != NULL) {
if (count < k)
count++;
else
q = q->next;
p = p->next;
}
if (count < k)
return 0;
else {
printf("%d", q->data);
return 1;
}
}
void test17() {
LinkList L;
InitList(L);
List_TailInsert(L);
LNode* list = L;
FindK(list, 5);
}
17.假定用带头结点的单链表保存单词,当两个单词有相同的后缀时,可共享相同的后缀存储空间。例如 loading 和 being 的存储映像如下图所示。设str1和str2分别指向两个单词所在单链表的头结点,链表结点由data和next组成,请设计一个算法找出由str1和str2所指向两个链表共同后缀的起始位置(如图中字符 i 所在结点的位置 p)。
解释:本题意思是str1和str2这两个指针已经存在了;他们已经指向了两个链表的头结点;两个单链表也已经存在了;两个单词字符串已经存进去了;需要我们通过两个指针来判断他们的共同后缀的起始位置在哪里。而不是要我们分析两个单独的单链表的共同后缀。
思路:两个链表不一定同样长,假设一个链表比另一个链表长 k 个结点,我们先在长链表上遍历 k 个节点,之后同步遍历两个链表,这样就能保证他们同时到达最后一个结点。因为两个链表从次一个公共结点到链表的尾节点都是重合的,所以他们肯定同时到达第一个公共结点。
//创建一个带头结点的单链表,存储数据为char类型
struct SNode {
char data; //存储的数据类型
struct SNode* next; //指针指向下一结点
};
typedef SNode SNode;
typedef SNode* LinkList;
//计算链表长度
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);
n = listlen(str2);
for (p = str1; m > n; m--) //若m>n,使p指向链表中的第 m-n+1 个节点,即可能的共同后缀起始位置
p = p->next;
for (q = str2; m < n; n--) //若m<n,使q指向链表中的第 m-n+1 个节点,即可能的共同后缀起始位置
q = q->next;
//循环出来之后,pq指向同一位置
while (p->next != NULL && p->next != q->next) { //查找共同后缀起始点
p = p->next; //两个指针同步向后移动
q = q->next;
}
return p->next; //返回共同后缀起始点的起始地址
}
时间复杂度为O(len1+len2)
18.用单链表保存m个整数,结点的结构由data和next组成,且 |data| ≤ n(n为正整数)。现要设计一个时间复杂度尽可能高效的算法对于链表中data的绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如 {21 -15 -15 7 15} 删除后变为 {21 -15 7}。
思路:
-
王道书的思路是:用空间换时间,使用辅助数组记录链表中已经出现的值,从而只需要对链表进行一次扫描。因为 |data|≤n ,故辅助数组q的大小为 n+1 ,各元素的初值均为0。依次扫描链表中的各结点,同时检查 q[|data|] 的值,若为0则保留该节点,并令 q[|data|] =1;否则删除该节点。
-
我没这么好的思路,如果是在考场上,我应该会直接嵌套两个for循环遍历链表,每查找一个值就和前面的所有值比较一遍,时间复杂度是O(n²)。答题完整的情况下扣1~3分。
下面是王道书的代码。
void Del_abs(LinkList h , int n) {
//传入一个链表L和数据最大值x
LinkList p = h, r;
int m;
int* q = (int*)malloc(sizeof(int) * (n + 1)); //声明一个辅助空间q,容量为n+1
for (int i = 0; i < n + 1; i++) //数组元素初值置为0
*(q + i) = 0;
while (p->next != NULL) {
m = p->next->data > 0 ? p->next->data : -p->next->data;
if (*(q + m) == 0) { //判断该节点的data是否已经出现过
*(q + m) = 1; //如果是首次出现
p = p->next; //保留
}
else { //重复出现
r = p->next; //删除
p->next = r->next;
free(r);
}
}
free(q);
}
代码解析:
-
*(q+m) 就相当于 q[m],不熟悉代码中的操作的话可以直接用数组代替。
-
我们拿到的是p节点,但是一直在进行比较的是 p->next 结点的data域,因为这样可以方便删除操作,删除的时候直接删除 p->next 这个结点就可以了。
-
最后要记得释放刚开始申请的数组q的空间。
-
时间复杂度为O(m),空间复杂度O(n)
19.设线性表L=(a1,a2,a3,...,an)采用带头结点的单链表保存,链表中的结点定义如下,请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L'=(a1,an,a2,an-1,a3,an-2,...)
typedef struct LNode{
int data;
struct LNode* next;
}LNode;
思路:我的思路是:先将链表的后半部分逆置,然后让两个指针p、q分别指向前半部分和后半部分的首节点,将q插入到p和p->next之间。
王道书思路:将L后半段原地逆置,先找出链表L的中间节点,为此设置两个指针p和q,指针p每次走一步,指针q每次走两步,当指针q到达链尾时,指针p正好在链表的中间节点;然后将L的后半段结点原地逆置;最后从单链表前后两段中依次各取一个结点,按要求重新排列。
思路没问题,但是我代码不会写,逆置代码还是很重要的,还需要加强练习。下面是王道书的代码:
void change_list(LNode* h) {
LNode* 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; //注意这里将前半段和后半段断开了,将后半段以头插法插入到p的后面
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; //r指向后半段的下一结点
q->next = s->next; //将q所指结点插入到s所指结点之后
s->next = q;
s = q->next; //s指向前半段的下一个插入点
q = r;
}
}
本题主要是 以头插法逆置单链表 这个考点的考察,这段代码要理解并牢牢掌握!!!
时间复杂度为O(n)。
三、总结
1.链表题常用方法有:头插法、尾插法、逆置法、归并法、双指针法等。
2.对于顺序表,因为可以直接存取,所以经常结合排序和查找的几种算法设计思路进行设计,如归并排序、二分查找等。
3.对于算法设计题,若能写出数据结构类型的定义、正确的算法思想,则至少给一半的分数;若能用伪代码写出自然更好;比较复杂的地方可以直接用文字表达。
4.本章节(包括顺序表的内容,即下面这个链接)的所有习题要二刷三刷,一定要掌握!!!