C 408—《数据结构》算法题基础篇—链表(下)
【摘要】 408考研——《数据结构》算法题基础篇之链表(下)。
目录
十、两个链表存储两个单词求共同后缀的起始位置 [2012真题]
Δ前言
- 从408应试的角度来看,算法题主要分为四部分——①线性表(包括数组和链表);②二叉树;③图;④查找。本篇博文主要讲链表相关的基础算法(由于链表相关的算法题目较多,所以up分为了上下两篇博文)。
- 博文中涉及到的题目全部参考自《王道数据结构考研复习指导》和《竟成408考研复习全书》。
- 注意事项——①点击文章侧边栏目录或者文章开头的目录可以进行跳转。②所有题目都符合408算法题的题目格式,即第一问算法设计思想、第二问C语言描述该算法、第三问计算该算法的时空复杂度。
- 良工不示人以朴,up所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!
一、两个升序链表归并为一个降序链表
0.题目:
已知有两个升序的单链表(按照结点的值递增排序),现要求将这两个升序的单链表归并为一个降序的单链表,并且要求除了原本两个升序链表的结点外,在归并过程中不允许创建新的结点。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
我们早在“
”一文中就接触过归并思想。它的算法思想是:从头到尾依次比较两个顺序表中的元素,并把更小的那个元素放在最终的顺序表中,直到合并完毕;只不过现在从"顺序表"换成"链表"了,在顺序中我们是通过三个下标实现的,那么在链表中我们自然要通过三个指针来实现了。p1指针指向listA链表的结点,p2指针指向listB链表的结点。利用while循环遍历两个升序链表,每次将更小的那个结点头插到新的链表中。(题干要求我们从“升序”变为“降序”,不难想到要用到头插法,因为头插法可以逆置链表)
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
O(m + n). 这是合并两个链表的最佳时间复杂度.
(2) 空间复杂度 :
O(1). 因为我们没有创建新的结点(题目也不允许我们创建), 整个过程只使用了几个临时的辅助变量, 所以该算法是"原地工作"的.
二、两个链表的所有相同值结点生成一个新链表
0.题目:
设listA和listB是两个带有头结点的单链表,且listA和listB都是升序链表(结点的值递增有序),现要求在不破坏原有链表结点的前提下,产生一个新链表listC,listC中保存的结点是listA和listB的相同值的结点(注意是值相同)。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
题干中要求“不破坏原有链表结点”,说明我们在遍历过程中需要创建新的结点。
使用while循环遍历listA和listB链表,每次循环都要比较当前listA和listB结点的值的大小(预设p,q指针分别指向listA和listB当前的结点),若值不相等,令值更小的指针后移;若值相等,就创建一个与当前值相等的结点,并将该结点插入到listC链表中,随后同时移动p,q指针后移,当p,q指针某一方为NULL时,说明对应的链表已经遍历完毕,此时结束while循环,返回listC链表。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
The time complexity is O(n + m), where n and m are the lengths of listA and listB respectively. This is optimal for this problem.(2) 空间复杂度:
The space complexity is O(min{n, m}) in the worst case, which is also optimal.
三、两个链表的所有相同值结点放入到其中一个原链表中
0.题目:
已知两个升序的单链表listA和listB,要求将两个链表的所有公共值结点全部放入到单链表listA中。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
此题要与上一题类比进行理解,上一题不允许我们破坏原有结点,所以我们需要创建新的结点来生成最终的链表,这就导致算法的空间复杂度更高了,达不到算法”原地工作“的效果。而此题中,要求我们用原来的链表listA维护所有的公共值结点,所以肯定涉及到了链表的删除,就需要我们在上一题的基础上做一些小小的改动。
仍然利用while循环遍历listA和listB链表,对于listA链表,使用两个指针currentA和prevA分别指向listA链表的当前结点和前一个结点。每次循环都要比较当前listA和listB结点的值的大小,若值不相等,令值更小的指针后移,如果值更小的结点是listA链表中的结点,就删除该结点,并令指向listA结点的两个指针都后移一位,如果值更小的结点是listB链表中的结点,就仅移动指向listB链表中的结点,不删除结点;若值相等,就同时移动指向listA和listB链表的指针,如果listA链表比listB链表长,那么当listB链表遍历完毕时,listA链表还没有遍历完毕,但可以肯定剩下的元素都不是公共值元素,所以还需要补一个while循环用来删除listA表中剩余的结点。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
The time complexity is O(n + m), where n and m are the lengths of listA and listB respectively. This is optimal for this problem.
(2) 空间复杂度:
The space complexity is O(1) as it operates in-place on listA.
四、判断一个链表是否为另一个链表的连续子序列
0.题目:
现有两个带有头结点的单链表listA和listB,要求判断listB是否为listA链表的一个连续子序列。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
- “暴力法”,分别用currentA和currentB两个指针指向listA和listB的当前结点,另设指针prevA记录每一轮循环的currentA是从哪个结点开始遍历的,
- listA和listB均从头开始遍历,判断当前对应的结点是否相等(指值相等),
- 对于第一次判断,如果不相等,令currentA指针后移一位(此时currentB指针仍然指向listB链表的首结点),重新开始判断;如果相等,就令currentA和currentB指针同时向右移,并再次判断新指向的对应的结点是否相等,
- 如果再次判断的结果不相等,说明之前判断相等的结点到这儿就卡住了,形不成同listB一样的连续序列,也就是说此次循环中,cuttentA从prevA记录的位置开始遍历listA是找不到和B一样的连续序列的(注意此时currentB指针已经发生了移动),
- 那么,出现失败的情况后,对于listA,令currentA指向prevA记录结点的下一个结点,并令currentB的指针重新回到listB的首结点,开始新的一轮循环,
- 如果发现任意一方的指针指向NULL时,跳出循环,此时判断currentB是否为NULL,只要currentB为NULL,说明一定在listA中找到了一个与listB相同的连续子序列。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
In the worst case scenario:
You might need to check every node in listA as a potential starting point for a match.
For each starting point, you might need to compare with all nodes in listB.
So the time complexity is O(n * m) , where n is the number of nodes in listA and m is the number of nodes in listB.
(2) 空间复杂度:
Uses only a fixed number of additional pointers (currentA, lastTimeA, currentB) regardless of the input size.
It doesn't use any additional data structures that grow with the input size.
Therefore, the space complexity is O(1) - constant space.
五、判断一个循环双链表是否对称
0.题目:
有一个带有头结点的循环双链表,判断其是否为对称的链表(讨论对称不包括头结点)。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
注意,此题与之前所有的题目都不同——此题的操作对象是双链表,而且是循环双链表,这意味着我们需要去重新定义新的数据结构,以及新的用于链表初始化的方法,和用于遍历链表,释放链表内存的方法。
关于链表是否对称,其实就是从链表的两端开始,每一对对应的结点的值都相同,不难想到会出现链表个数为奇数和链表个数为偶数这两种情况(一般讨论链表结点数时,不考虑头结点,因为头结点并不承载有效数据),使用两个指针left 和 right分别初始化为链表的尾结点和首结点,left指针以尾结点到首结点方向从右向左遍历,right指针则从首结点开始,依次向右遍历,只要left和right指向的结点的数据域相同,就继续遍历下去。
奇数个结点和偶数个结点的差别,体现在遍历什么时候结束。对于奇数个结点的循环双链表,如果left和right即将指向的下一个结点是同一个结点,说明之前遍历过的对应结点都是data域相同的,到这里就该结束了;对于偶数个结点的循环双链表,如果left指向的下一个结点是right当前指向的结点,或者如果right指向的下一个结点是left当前指向的结点,说明遍历到这里也该结束了,这是一个对称的链表。反之,只要在遍历过程中发现有任何一对结点的值不对应相等,说明这不是一个对称的链表,直接return就可以了。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
The time complexity is O(n/2), which simplifies to O(n), where n is the number of nodes in the list.
(2) 空间复杂度:
Therefore, the space complexity is O(1) (constant space).
六、两个循环单链表的连接 (链接)
0.题目:
给出两个均带有头结点的循环单链表,链表头指针分别为pHeadA and pHeadB,请设法将链表pHeadB链接到链表pHeadA的后面,要求得到的新链表仍然保持循环的特性。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
这个没啥可讲的吧,就和链表结点的尾插类似,只不过现在是要插一个链表,把指针看清楚别瞎几把指就行。
对了,这个题和上一个题又不一样,这个题的操作对象是循环单链表,这意味着我们得在之前单链表定义好的数据结构和相关函数的基础上,做一些改动,比方说遍历链表的函数,以及释放链表内存的函数。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
The total time complexity is O(n + m).
(2) 空间复杂度:
Space Complexity of insertList: O(1).
七、循环单链表反复找到最小值结点并删除
0.题目:
已知一个带有头结点的循环单链表,要求不断地找出链表中的最小值结点,把最小值结点的值打印出来并且删除该结点,直到该链表一个有效结点不剩,最后再删除头结点。
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
在“链表专题的上篇,有一道题目是删除链表的最小值结点,当时up已经较为详细地介绍了删除最小值结点需要的两个操作“找”和“删”,这里就不再赘述,如果有对思路或者相关代码有不懂的朋友们可以点击链接再去回顾一下(就在第二道题);
”一文中,也就是而此题相当于之前那道题的升级版,主要差异在了两个地方——①本题操作的链表是循环链表;②本题并不是只删除一个最小值结点,而是要逐个击破,赶尽杀绝(bushi,所以up重点说一下如何处理这两个问题:针对第一个差异,只需要注意循环链表的判空条件与非循环链表不同即可;针对第二个差异,只需要在之前那道题的基础上再套一层循环即可。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果 :
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
- The outer while loop runs until the list is empty, so it executes n times, where n is the initial number of nodes in the list.
- For each iteration of the outer loop, the inner while loop traverses the entire remaining list to find the minimum value.
- In the first iteration, it checks n nodes, in the second n-1, then n-2, and so on.
- This forms an arithmetic sequence: n + (n-1) + (n-2) + ... + 2 + 1
- The sum of this sequence is n(n+1)/2
- Therefore, the time complexity is O(n^2)
(2) 空间复杂度:
- The algorithm uses a fixed number of pointers (tempPre, minPre, temp, min) regardless of the input size.
- No additional data structures are created that grow with the input size.
- The space used is constant throughout the execution of the algorithm
- Overall Space Complexity: O(1)
八、非循环双链表查改结点并按照结点访问频度进行排序
0.题目:
已知一个带有头结点的非循环双向链表,该链表的每个结点都维护有四个属性——①pev(指向当前结点的前驱结点);②data(当前结点的有效值);③freq(当前结点的访问频度,初始值为0);④next(指向当前结点的后继结点)。现定义一个Locate(pHead, data)操作,该操作会访问链表中元素值为data的结点,并将该结点的freq域的值加一,同时,还会令当前链表结点保持freq域递减的顺序进行排列,并且,当两个结点的freq域的值相同时,要求最近被访问的结点优先排在前面,以便使得频繁访问的结点总是更靠近链表头。要求定义一个函数实现上述Locate(pHead, data)操作,并且返回目标结点的指针。(假设给定的链表已经按照freq降序排序)
请设计一个算法来实现上述操作,用C语言描述该算法,并且计算出该算法的时间复杂度和空间复杂度。
1.算法设计思想:
先对题干进行分解,可知该算法共需要完成五个步骤—①查找链表中指定值的结点;②修改该结点的值;③取出该结点(涉及到双链表中结点的删除);④查找到第一个freq域比该结点大的结点;⑤将该结点插入到第一个freq域比该结点大的结点的后面(涉及到双链表中结点的插入)。
这五个操作都很基础,因此这是一道综合题,但算不上难题,每个操作本身都没难度,注意双链表结点的删除和插入时,指针给指对就行,别瞎🐔⑧乱指。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果:
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
Finding the target node:
- In the worst case, we might need to traverse the entire list.
- This takes O(n) time, where n is the number of nodes in the list.
- Update the target node:
- It takes only O(1) time.
Removing the node:
- This is a constant time operation, O(1).
Finding the correct position to insert:
- In the worst case, we might need to traverse the entire list again.
- This also takes O(n) time.
Inserting the node:
- This is a constant time operation, O(1).
Overall Time Complexity: O(n)
(2) 空间复杂度:
- The function uses a fixed number of pointers (aim, insert_pos) regardless of the input size.
- No additional data structures are created that grow with the input size.
- The space used is constant throughout the execution of the function.
Overall Space Complexity: O(1)
九、查找单链表中倒数第K个结点 [2009真题]
0.题目:
已知一个带有表头结点的单链表,且结点的数据结构为一个data数据域和一个link指针域,假设该链表只给出头指针list,在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表的倒数第K个位置上的结点(K为正整数)。若查找成功,算法输出该结点data域的值,并返回1;否则,只返回0。要求:
1)描述算法的基本设计思想。
2)描述算法的详细实现步骤。
3)根据设计思想和实现步骤,采用程序设计语言描述算法(使用C、或者C++实现)关键之处给出简要注释。
1.算法设计思想:
设计思想:
由于链表倒数第K个结点到链表尾的距离是一定的,即只需移动K-1次就可以到达链表尾;所以,可以先从链表头开始向后遍历,移动K-1次就可以找到链表正着数的第K个结点,而从链表头到正着数的第K个结点的距离,与从倒数第K个结点到链表尾的距离是相等的,所以,只需要让指向第一段距离的前后两个指针同时后移,当后一个指针指向表尾时,前一个指针就指向了倒数第K个结点,而这一段距离是没有变化的。
详细实现步骤:
考虑使用“双指针”法,首先令两个指针同时指向表头结点,先令其中一个指针向后移动K-1次,该指针即指向了链表的正着数的第K个结点,然后令前后两个指针同时后移,当后一个指针指向NULL(表尾)时,前一个指针即指向了链表倒数第K个结点。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果:
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
O(n), where n is the number of nodes in the list. In the worst case, we need to traverse the list once.
(2) 空间复杂度 :O(1), as we only use a constant amount of extra space regardless of the input size.
十、两个链表存储两个单词求共同后缀的起始位置 [2012真题]
0.题目:
假定采用带头结点的单链表保存单词,当两个单词有相同的后缀时,则可共享相同的后缀存储空间,例如,"loading"和"being"的存储映像如下图所示:
设str1和str2分别指向两个单词所在单链表的头结点,链表结点结构为一个data数据域和一个next指针域。请设计一个时间上尽可能高效的算法,找出由str1和str2所指向两个链表共同后缀的起始位置。要求:
1)给出算法的基本设计思想;
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释;
3)说明你所设计算法的时间复杂度。
1.算法设计思想:
如果两个链表的长度相同,那么我们只需要用两个指针同时遍历两个链表,当两个指针指向同一个结点时(注意是指针相同),说明找到了共同后缀的起始位置;
但是更一般地,由于两个单词的长度往往不相同,所以对应地两个链表的长度也不会相同,这时候需要先得到两个链表的长度之差,然后让较长的链表先遍历前面长度之差个结点,这时候两个指针又到了同一起跑线了(即两个指针到共同后缀起始位置的距离是一样的),同时令两个指针后移即可找到共同后缀的起始位置。
PS : 其实这道题和我们在 中做过的第七题一样。(当时讲了两种方法,感兴趣的朋友们可以去看看)
2.C语言描述:
完整代码如下:(注意看注释)
运行结果:
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
findTheStartOfCommonSuffix function:
- It calls getLength twice, once for each list: O(n + m), where n and m are the lengths of the two lists.
- It then traverses the longer list from the beginning to the potential start of the common suffix: O(max(n, m))
- Finally, it compares the nodes of both lists until it finds the common node or reaches the end: O(min(n, m))
Overall time complexity: O(n + m)
(2) 空间复杂度:The space complexity is O(1) (constant space). The algorithm uses a fixed amount of extra space regardless of the input size.
十一、链表绝对值的去重 [2015真题]
0.题目:
用单链表保存m个整数,结点的结构为一个data数据域和一个link指针域,且要求|data| ≤ n(n为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中data的绝对值相等的结点,仅保留第一次出现的结点而删除其余绝对值相等的结点。例如,若给定的单链表HEAD,其删除结点的情况如下图所示 :
要求:
1)给出算法的基本设计思想。
2)使用C++或C语言,给出单链表结点的数据类型定义。
3)根据设计思想,采用C或者C++语言描述算法,关键之处给出注释。
4)说明你所设计算法的时间复杂度和空间复杂度。
1.算法设计思想:
题干中提到“时间复杂度尽可能高效”,却没有要求空间复杂度,其实就是在提醒我们,要考虑“以空间换时间”了。
考虑如何利用辅助空间,使得链表中每一个元素都能与之对应,并且可以很快查找到已保存的元素。不难想到顺序表的”随机存取“的特点,因此,可以将链表结点的|data|值转化为用数组的下标表示,由于链表结点|data| ≤ n,所以要申请一片大小为n + 1的连续空间(确保链表中每一个结点的|data|都可以用数组的下标来记录,因为|data| = n的下标对应了第n + 1个元素),数组所有元素全部初始化为0(0表示该下标对应的绝对值的结点还没有被保存);遍历链表,如果是第一次扫描到该绝对值的结点,就把数组对应下标的元素设为1(1表示该下标对应的绝对值的结点已经被保存),那么当下一次扫描到相同绝对值的结点时,通过数组下标对应的1就可以检测到该绝对值的结点已经存在了,此时需要删除当前结点。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果:
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
O(m), where m is the length of the list, as we traverse the list once.
(2) 空间复杂度 :O(n), where n is the maximum absolute value, due to the auxiliary array.
十二、链表元素的重新交错排列 [2019真题]
0.题目:
设线性表L = (a1, a2, a3, ..., an-2, an-1, an) 采用带头结点的单链表保存,链表中的结点定义如下:
typedef struct node {int data;
struct node * next;
} NODE;
请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L' = (a1, an, a2, an-1, a3, an-2, ...)。要求:
1)给出算法的基本设计思想;
2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释;
3)说明你所设计的算法的时间复杂度。
1.算法设计思想:
(a1, a2, a3, ..., an-2, an-1, an)要想变成(a1, an, a2, an-1, a3, an-2, ...),不难看出就是依次从表头和表尾开始挨个选元素,表头选一个表尾选一个,依次类推,直到将链表中的元素重新排列成一个新的序列;通过观察我们可知,我们的目标序列按照奇偶位序可以划分为两个子序列,分别为(a1, a2, a3, ..., amid) 和 (an, an-1, an-2, ..., amid+1),第二个子序列恰好就是原来链表后半部分的逆置,如果能得到后半部分序列的逆置,再和第一个子序列交替进行尾插,即得到目标序列。那么第一个问题就是如何得到后半部分序列的逆置?不难想到要先拿到链表中间元素amid的指针,然后对后半部分进行头插即可逆置。
在上面第九题“寻找链表的倒数第K个结点”时,我们使用了”双指针”法,只不过当时我们是保持两个指针之间的距离不变(保持了K的距离),这样当前面的指针遍历到表尾时,后面的指针就指向了倒数第K个结点。这道题我们要用到双指针法的一个特殊情况——“快慢指针”法,见名知意,也就是说前后两个指针在遍历过程中不再保持固定的距离了,而是一前一后两个指针移动的速度并不一样快。
为什么要提到快慢指针法?用快慢指针是为了找到链表的中间结点。为什么要找到链表的中间结点?找到中间结点是为了将链表的后半部分进行逆置。针对于本题,只需要让快指针每次移动两个结点,慢指针每次移动一个结点,当快指针指到达链表尾时,慢指针指向的即是链表的中间结点。
所以,本题可以分解为三个子问题——①得到链表的中间结点的指针;②将链表后半部分的结点进行逆置;③链表前半部分和已经逆置的后半部分分别交替进行尾插即可得到目标序列。
2.C语言描述:
完整代码如下:(注意看注释)
运行结果:
3.算法的时间和空间复杂度:
(1) 时间复杂度 :
Finding the middle of the list (Step 1):
- This uses the fast and slow pointer technique, which traverses the list once.
- Time complexity: O(n), where n is the number of nodes in the list.
Reversing the rear half of the list (Step 2):
- This step traverses approximately half of the list once.
- Time complexity: O(n/2), which simplifies to O(n).
Merging the two halves (Step 3):
- This step iterates through all nodes once.
- Time complexity: O(n).
Overall Time Complexity: O(n) + O(n) + O(n) = O(n)
(2) 空间复杂度 :
We create a constant number of additional nodes:
- One dummy node for the rear half (head).
- One temporary node (tempNodeContainer).
- A few pointer variables (temp, tempNext, rear, fore, etc.).
All these additional space requirements are constant and do not grow with the input size.
Overall Space Complexity: O(1)
Δ总结
- 算法题基础篇——链表,上下两篇博文共计22道题目,除了最后一道题有点难度外基本考的都是基本功,需要我们对链表有扎实的理解。
- 大家回顾一下,一开始我们接触了头插法尾插法,后来又碰到了单链表的逆置,再到后面单链表的逆置只是作为了整个算法的一部分实现,整个过程是循序渐进的,希望大家可以多练习多回顾,举一反三。
- 之后可能还会给大家分享“树”和“图”相关的代码题,敬请期待。
- 🆗,以上就是本篇博文的全部内容了,感谢阅读!
System.out.println("END------------------------------------------");
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
作者其他文章
评论(0)