考研数据结构——链表相关大题(含解析和代码超详细)

一、前言

        本文习题均来自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中删除一个最小值结点的高效算法。(假设该节点唯一)

思路:

  1. 先分析:第一步要找到最小值结点;第二步要删除最小值结点;

  2. 要完成第一步很容易,设一个指针p从头至尾遍历单链表就能找出来,但只有一个指针p是无法实现删除操作的,要想实现删除操作就必须有p结点(即被删节点)的前驱结点的指针,设为prep;

  3. 但只有指针p和他的前驱结点prep还是不够,因为要删除的结点不是p,是最小值结点,当遍历完一边链表L之后,p指针指向链表最后一个元素,不是我们要的最小值节点。于是我们需要另设一个指向最小值的结点指针minp以及它的前驱结点minpre;

  4. 那这样是不是prep指针就没必要了呢?不是的,prep要和p指针进行比较,从而得出最小值的指针位置。然后把p赋给minp,pre赋给minpre;

  5. 这样,在扫描完毕时,我们会得到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域,因此从第一个公共结点开始,之后的所有节点都是重合的,不可能出现分叉。

暴力解法

  • 在第一个链表上顺序遍历每个结点,每遍历一个结点,在第二个链表上顺序遍历所有的结点,若找到两个相同的结点,则找到了他们的公共结点。

优化思路

  1. 先简化问题,若两个链表有一个公共结点,则该公共结点之后的所有节点都是重合的,则他们的最后一个结点必然是重合的。因此,我们判断力两个链表是否有重合的部分时,只需要分别遍历两个链表到最后一个结点。若两个尾节点是一样的,则说明他们有公共结点,否则两个链表没有公共结点。

  2. 然而,上述思路在找到两个链表的尾节点时,不能保证在两个链表上同时到达尾节点。这是因为两个链表长度不一定一样。假设 A 链表比 B 链表长 k 个节点,我们先在 A 链表上遍历 k 个结点,之后再同步遍历,此时就能保证同时到达最后一个结点。

  3. 由于两个链表从第一个公共结点开始到链表的尾节点,这一部分是完全重合的,因此他们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共结点。

  4. 根据这一思路,我们先要分别遍历两个链表得到他们的长度,并求出两个长度之差 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.本章节(包括顺序表的内容,即下面这个链接)的所有习题要二刷三刷,一定要掌握!!!

考研数据结构——线性表相关大题(含解析和代码)_数据结构考研真题线性表-CSDN博客

  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
西南交通大学的考研数据结构和C语言真题主要涵盖了数据结构和C语言的基本概念、常见算法和数据结构的应用,是考研复习中的重点和难点。 数据结构部分的真题主要涉及线性表、栈和队列、链表和树、图和排序等知识点。例如,可能会出现关于数组的插入、删除和查找操作以及对其时间复杂度的分析题目,还可能会要求设计和实现单链表、二叉树或图等数据结构,并进行相应的操作和应用。对于这些题目,考生需要熟悉各种数据结构的特点、使用方法和算法,能够分析算法的时间复杂度和空间复杂度,并灵活应用到实际问题中。 C语言部分的真题主要考察C语言的基本语法、指针和内存管理、函数和库等方面的知识。可能会出现关于函数的声明和定义、指针的使用、内存动态分配和释放等方面的题目。考生需要对C语言的语法、特性和常用库函数有一定的掌握,能够理解和分析C语言程序的执行过程和内存管理机制。 对于准备西南交通大学考研的考生来说,要复习数据结构和C语言,首先要掌握基础概念和常用算法和数据结构的原理和应用。其次,要多做真题和模拟题,加深对知识的理解和应用。同时,还要关注最新的考研动态和备考资料,及时调整和完善复习计划。通过系统的学习和不断的练习,相信考生一定能够顺利应对西南交通大学考研数据结构和C语言的考试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值